mirror of
https://github.com/3proxy/3proxy.git
synced 2026-10-24 12:05:47 +08:00
- readconfig() kept its line buffer and argument array on every error
return, so each refused reload leaked them.
- A filter refusing a connection in filter_client (TLS: a failed
handshake) was not kept, so its filter_clear never ran and the state
it made was lost. makefilters() now clears it, and empties the slot
first: a pooled worker's slot may hold the previous connection's.
- ssl_filter_close() freed the client CA paths but not the server ones,
for every TLS service set up.
- Compiled patterns were never freed: an ACL's (pcre: and regex: host
names), an httpsrv rule's. copyacl() shared them between the
configuration's ACL and each service's copy; each copy now compiles
its own, and freeacl() and freehttprules() free them.
What LeakSanitizer still reports is OpenSSL's per-thread random
generator state of pooled threads, released only by OpenSSL at exit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 796e9f8156)
1641 lines
48 KiB
C
1641 lines
48 KiB
C
/*
|
|
3proxy - HTTP server
|
|
|
|
A small HTTP/1.0 server. The request is parsed into a struct httpreq and
|
|
handed to a handler chosen from a table by path, so the transport, request
|
|
parsing and response helpers are shared and a new endpoint is one row in
|
|
httphandlers[] plus a function.
|
|
|
|
The handlers built today generate deterministic responses for the regression
|
|
tests: /echo describes the connection as seen by the server, which is how a
|
|
test tells which source address and port a request arrived from, and /data
|
|
produces a requested amount of output.
|
|
|
|
Parsing here is deliberately blunt - fixed buffers, bounded reads, no shared
|
|
request parser - so that a fault in the code under test cannot be cancelled
|
|
out by the same fault in the server used to observe it.
|
|
*/
|
|
|
|
#include "proxy.h"
|
|
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
/* Handing a file to the socket without copying it through this process. The
|
|
call differs on each platform that has one, and Windows has its own. */
|
|
#if defined(__linux__)
|
|
#define HTTPSRV_SENDFILE
|
|
#include <sys/sendfile.h>
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__)
|
|
#define HTTPSRV_SENDFILE
|
|
#include <sys/uio.h>
|
|
#include <sys/socket.h>
|
|
#elif defined(_WIN32)
|
|
#include <io.h>
|
|
#include <mswsock.h>
|
|
#endif
|
|
|
|
#ifdef WITH_HTTPSRV
|
|
|
|
#include <stdarg.h>
|
|
|
|
#define RETURN(xxx) { param->res = xxx; goto CLEANRET; }
|
|
|
|
#define HTTPSRV_LINE 1024
|
|
#define HTTPSRV_BLOCK 8192
|
|
#define HTTPSRV_MAXHDR 64
|
|
#define HTTPSRV_MAXCACHED 1048576 /* larger files are streamed instead */
|
|
#ifdef _WIN32
|
|
#define HTTPSRV_O_BINARY O_BINARY
|
|
#else
|
|
#define HTTPSRV_O_BINARY 0
|
|
#endif
|
|
#define HTTPSRV_MAXREWRITE 16
|
|
/* An operation returns this to say the request was changed and the rules
|
|
after it should be tried again. */
|
|
#define HTTPSRV_REWRITTEN 2
|
|
#define HTTPSRV_MAXBODY 1048576
|
|
/* the most of a request this server keeps in case it hands it on */
|
|
#define HTTPSRV_MAXRAW 65536
|
|
/* lengths a reply is written with: a count, or one of these */
|
|
#define HTTPSRV_LEN_CHUNKED (-1)
|
|
#define HTTPSRV_LEN_NONE (-2)
|
|
/* one sendfile()/TransmitFile() call takes no more than this */
|
|
#define HTTPSRV_SENDMAX 0x40000000
|
|
|
|
|
|
/* Returns the value of a query parameter, or def when it is missing or not a
|
|
number. Values are clamped by the caller, not here. */
|
|
/* Copies a request field, refusing anything that does not fit rather than
|
|
storing a shortened copy. strncpy would leave the result unterminated at
|
|
exactly the length that overflows - it is only safe with a zeroed struct -
|
|
and a truncated path or host is worse than a rejected one, since it would be
|
|
matched against the rules as though the client had sent the shorter string.
|
|
*/
|
|
static int hexval(int c)
|
|
{
|
|
if(c >= '0' && c <= '9') return c - '0';
|
|
if(c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if(c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
/* Decodes %XX sequences. A malformed sequence, or one decoding to a NUL that
|
|
would cut the path short, is refused rather than passed on. */
|
|
static int urldecode(char *dst, size_t size, const char *src)
|
|
{
|
|
size_t o = 0;
|
|
|
|
while(*src){
|
|
int c = (unsigned char)*src++;
|
|
|
|
if(c == '%'){
|
|
int hi, lo;
|
|
|
|
hi = hexval((unsigned char)src[0]);
|
|
if(hi < 0) return 1;
|
|
lo = hexval((unsigned char)src[1]);
|
|
if(lo < 0) return 1;
|
|
c = (hi << 4) | lo;
|
|
src += 2;
|
|
}
|
|
|
|
if(!c) return 1;
|
|
if(o + 1 >= size) return 1;
|
|
dst[o++] = (char)c;
|
|
}
|
|
|
|
dst[o] = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* Checked after decoding, because the encoded form hides both of these. */
|
|
static int pathunsafe(const char *path)
|
|
{
|
|
if(strstr(path, "/..")) return 1;
|
|
if(strchr(path, '\r') || strchr(path, '\n')) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int copyfield(char *dst, size_t size, const char *src)
|
|
{
|
|
size_t len = strlen(src);
|
|
|
|
if(len >= size) return 1;
|
|
memcpy(dst, src, len + 1);
|
|
return 0;
|
|
}
|
|
|
|
static int64_t qparam(const char *query, const char *name, int64_t def)
|
|
{
|
|
const char *p;
|
|
size_t len;
|
|
int64_t val;
|
|
int used;
|
|
|
|
if(!query || !*query) return def;
|
|
len = strlen(name);
|
|
|
|
for(p = query; *p; ){
|
|
if(!strncmp(p, name, len) && p[len] == '='){
|
|
used = 0;
|
|
if(sscanf(p + len + 1, "%"SCNd64"%n", &val, &used) != 1 || !used)
|
|
return def;
|
|
return val;
|
|
}
|
|
p = strchr(p, '&');
|
|
if(!p) break;
|
|
p++;
|
|
}
|
|
return def;
|
|
}
|
|
|
|
static int httpsrv_send(struct httpreq *r, const char *buf, int len)
|
|
{
|
|
return socksend(r->param, r->param->clisock, (unsigned char *)buf, len,
|
|
conf.timeouts[STRING_S]) != len;
|
|
}
|
|
|
|
static int httpsrv_printf(struct httpreq *r, const char *fmt, ...)
|
|
{
|
|
char buf[HTTPSRV_LINE];
|
|
int len;
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
len = vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
|
|
if(len < 0) return 1;
|
|
if(len > (int)sizeof(buf) - 1) len = (int)sizeof(buf) - 1;
|
|
|
|
return httpsrv_send(r, buf, len);
|
|
}
|
|
|
|
/* Dates on the wire are in GMT and in English whatever the machine is set to,
|
|
so they are built and read here instead of through strftime and gmtime: one
|
|
follows the locale, the other answers from a buffer shared by every thread. */
|
|
static const char httpwdays[7][4] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
|
static const char httpmonths[12][4] = {
|
|
"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"
|
|
};
|
|
|
|
static int64_t civildays(int y, int m, int d)
|
|
{
|
|
int64_t era;
|
|
unsigned yoe, doy, doe;
|
|
|
|
y -= m <= 2;
|
|
era = (y >= 0? y : y - 399) / 400;
|
|
yoe = (unsigned)(y - era * 400);
|
|
doy = (unsigned)((153 * (m + (m > 2? -3 : 9)) + 2) / 5 + d - 1);
|
|
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
return era * 146097 + (int64_t)doe - 719468;
|
|
}
|
|
|
|
static void civilfromdays(int64_t z, int *y, int *m, int *d)
|
|
{
|
|
int64_t era;
|
|
unsigned doe, yoe, doy, mp;
|
|
|
|
z += 719468;
|
|
era = (z >= 0? z : z - 146096) / 146097;
|
|
doe = (unsigned)(z - era * 146097);
|
|
yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
mp = (5 * doy + 2) / 153;
|
|
*d = (int)(doy - (153 * mp + 2) / 5 + 1);
|
|
*m = (int)(mp + (mp < 10? 3 : -9));
|
|
*y = (int)((int64_t)yoe + era * 400) + (*m <= 2);
|
|
}
|
|
|
|
/* buf takes at least 32 characters */
|
|
static void httpdate(time_t t, char *buf)
|
|
{
|
|
int64_t days = (int64_t)t / 86400;
|
|
int secs = (int)((int64_t)t - days * 86400);
|
|
int y, m, d, wday;
|
|
|
|
if(secs < 0){
|
|
secs += 86400;
|
|
days--;
|
|
}
|
|
civilfromdays(days, &y, &m, &d);
|
|
wday = (int)(((days % 7) + 11) % 7); /* the epoch was a Thursday */
|
|
sprintf(buf, "%s, %02d %s %04d %02d:%02d:%02d GMT",
|
|
httpwdays[wday], d, httpmonths[m - 1], y,
|
|
secs / 3600, (secs / 60) % 60, secs % 60);
|
|
}
|
|
|
|
static int monthbyname(const char *name)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < 12; i++) if(!strncasecmp(name, httpmonths[i], 3)) return i + 1;
|
|
return 0;
|
|
}
|
|
|
|
/* The three date formats a client is allowed to send. Returns 0 for anything
|
|
which cannot be read, which a caller takes as no date at all. */
|
|
static time_t parsehttpdate(const char *s)
|
|
{
|
|
char mon[16];
|
|
int d, y, hh, mm, ss, m;
|
|
|
|
while(*s == ' ') s++;
|
|
if(sscanf(s, "%*3s, %d %15s %d %d:%d:%d", &d, mon, &y, &hh, &mm, &ss) == 6
|
|
|| sscanf(s, "%*[^,], %d-%15[^-]-%d %d:%d:%d", &d, mon, &y, &hh, &mm, &ss) == 6){
|
|
if(y < 100) y += (y < 70)? 2000 : 1900;
|
|
}
|
|
else if(sscanf(s, "%*3s %15s %d %d:%d:%d %d", mon, &d, &hh, &mm, &ss, &y) == 6){
|
|
/* asctime, as in "Sun Nov 6 08:49:37 1994" */
|
|
}
|
|
else return 0;
|
|
|
|
m = monthbyname(mon);
|
|
if(!m || d < 1 || d > 31 || hh < 0 || hh > 23 || mm < 0 || mm > 59
|
|
|| ss < 0 || ss > 60 || y < 1970) return 0;
|
|
return (time_t)(civildays(y, m, d) * 86400 + hh * 3600 + mm * 60 + ss);
|
|
}
|
|
|
|
/* A header such as Connection carries a list, so a name is looked for as one
|
|
of its items rather than as the whole value. */
|
|
static int headertoken(const char *value, const char *name)
|
|
{
|
|
size_t len = strlen(name);
|
|
|
|
for(; *value; value++){
|
|
if(strncasecmp(value, name, len)) continue;
|
|
if(value[len] && value[len] != ',' && value[len] != ' '
|
|
&& value[len] != '\t') continue;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static const char * statustext(int status)
|
|
{
|
|
switch(status){
|
|
case 200: return "OK";
|
|
case 201: return "Created";
|
|
case 202: return "Accepted";
|
|
case 204: return "No Content";
|
|
case 301: return "Moved Permanently";
|
|
case 302: return "Found";
|
|
case 303: return "See Other";
|
|
case 304: return "Not Modified";
|
|
case 307: return "Temporary Redirect";
|
|
case 308: return "Permanent Redirect";
|
|
case 400: return "Bad Request";
|
|
case 401: return "Unauthorized";
|
|
case 403: return "Forbidden";
|
|
case 404: return "Not Found";
|
|
case 405: return "Method Not Allowed";
|
|
case 410: return "Gone";
|
|
case 500: return "Internal Server Error";
|
|
case 502: return "Bad Gateway";
|
|
case 503: return "Service Unavailable";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
/* Writes the status line and headers. HTTPSRV_LEN_CHUNKED asks for chunked
|
|
encoding, which is how a response of unknown or deliberately unstated size
|
|
is produced, and HTTPSRV_LEN_NONE leaves the length out altogether, for the
|
|
statuses which carry no body at all. A NULL type is left out the same way.
|
|
extra, when it is given, is a header line of its own and goes before what
|
|
the rule adds. What the rule adds goes with whatever status the rule asked
|
|
for, a refusal included; a refusal the server itself decided on drops them
|
|
first, since they describe an answer which is not being given. */
|
|
static int httpsrv_head(struct httpreq *r, int status, const char *ctype, int64_t len,
|
|
const char *extra)
|
|
{
|
|
/* A client speaking 1.0 has no chunked encoding to read, so an answer of
|
|
unstated length ends the connection instead. */
|
|
if(len == HTTPSRV_LEN_CHUNKED && !r->version) r->keepalive = 0;
|
|
|
|
if(httpsrv_printf(r, "HTTP/1.%d %d %s\r\n", r->version, status,
|
|
statustext(status))) return 1;
|
|
if(ctype && httpsrv_printf(r, "Content-Type: %s\r\n", ctype)) return 1;
|
|
if(len >= 0){
|
|
if(httpsrv_printf(r, "Content-Length: %"PRId64"\r\n", len)) return 1;
|
|
}
|
|
else if(len == HTTPSRV_LEN_CHUNKED
|
|
&& httpsrv_printf(r, "Transfer-Encoding: chunked\r\n")) return 1;
|
|
|
|
if(extra && httpsrv_send(r, extra, (int)strlen(extra))) return 1;
|
|
if(r->maxage >= 0
|
|
&& httpsrv_printf(r, "Cache-Control: max-age=%d\r\n", r->maxage)) return 1;
|
|
if(r->hdrs && httpsrv_send(r, r->hdrs, (int)strlen(r->hdrs))) return 1;
|
|
|
|
return httpsrv_printf(r, "Connection: %s\r\n\r\n",
|
|
r->keepalive? "keep-alive" : "close");
|
|
}
|
|
|
|
/* Wraps one block as a chunk, a zero length writing the terminating chunk.
|
|
Takes the client rather than a request so that anything writing a chunked
|
|
response can use it. */
|
|
int httpchunk(struct clientparam *param, const char *buf, int len)
|
|
{
|
|
char hdr[16];
|
|
int hlen;
|
|
|
|
if(len <= 0){
|
|
return socksend(param, param->clisock, (unsigned char *)"0\r\n\r\n", 5,
|
|
conf.timeouts[STRING_S]) != 5;
|
|
}
|
|
|
|
hlen = sprintf(hdr, "%x\r\n", len);
|
|
if(socksend(param, param->clisock, (unsigned char *)hdr, hlen,
|
|
conf.timeouts[STRING_S]) != hlen) return 1;
|
|
if(socksend(param, param->clisock, (unsigned char *)buf, len,
|
|
conf.timeouts[STRING_S]) != len) return 1;
|
|
|
|
return socksend(param, param->clisock, (unsigned char *)"\r\n", 2,
|
|
conf.timeouts[STRING_S]) != 2;
|
|
}
|
|
|
|
/* Fills buf with a repeating pattern carrying its own offset, so a truncated or
|
|
reordered body is visible in the output rather than looking like a short
|
|
read. */
|
|
static void httpsrv_fill(char *buf, int len, uint64_t offset)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < len; i++){
|
|
uint64_t pos = offset + (uint64_t)i;
|
|
|
|
buf[i] = (pos % 64 == 63)? '\n' : (char)('0' + (int)((pos / 64) % 10));
|
|
}
|
|
}
|
|
|
|
static int op_forbidden(struct httpreq *r);
|
|
static int op_notfound(struct httpreq *r);
|
|
|
|
/* Build a string from a template, putting in what the stars or groups stood
|
|
for. $0 is the whole path, $1 upwards the captures, $$ a literal dollar.
|
|
Returns 1 when the result would not fit. */
|
|
static int expand(char *dst, int dstsize, const unsigned char *tmpl,
|
|
const char *subject, const struct capture *caps, int ncaps)
|
|
{
|
|
int out = 0;
|
|
|
|
for(; *tmpl; tmpl++){
|
|
if(*tmpl == '$' && tmpl[1] == '$'){
|
|
if(out + 1 >= dstsize) return 1;
|
|
dst[out++] = '$';
|
|
tmpl++;
|
|
continue;
|
|
}
|
|
if(*tmpl == '$' && isdigit(tmpl[1])){
|
|
int n = tmpl[1] - '0';
|
|
|
|
tmpl++;
|
|
if(n >= ncaps || n >= MAXCAPTURES) continue;
|
|
if(out + caps[n].len >= dstsize) return 1;
|
|
memcpy(dst + out, subject + caps[n].start, caps[n].len);
|
|
out += caps[n].len;
|
|
continue;
|
|
}
|
|
if(out + 1 >= dstsize) return 1;
|
|
dst[out++] = *tmpl;
|
|
}
|
|
dst[out] = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* A path built from a request is refused rather than corrected: what a
|
|
client sends decides part of it, and a name that walks out of the tree, or
|
|
carries a line ending or a star, is not something to guess about.
|
|
|
|
Only a full path is taken. A relative one would be read against whatever
|
|
directory the service happens to be in, which is not something a
|
|
configuration should depend on. */
|
|
static int targetunsafe(const char *path)
|
|
{
|
|
const char *p;
|
|
|
|
if(!*path) return 1;
|
|
#ifdef _WIN32
|
|
/* a drive, or a share, and nothing else */
|
|
if(!(isalpha((unsigned char)path[0]) && path[1] == ':'
|
|
&& (path[2] == '\\' || path[2] == '/'))
|
|
&& !(path[0] == '\\' && path[1] == '\\')) return 1;
|
|
#else
|
|
if(path[0] != '/') return 1;
|
|
#endif
|
|
for(p = path; *p; p++){
|
|
if(*p == '\r' || *p == '\n' || *p == '*') return 1;
|
|
#ifdef _WIN32
|
|
if(*p == '"' || *p == '<' || *p == '>' || *p == '|' || *p == '?') return 1;
|
|
#endif
|
|
}
|
|
if(!strncmp(path, "./", 2) || !strncmp(path, "../", 3)) return 1;
|
|
if(strstr(path, "/./") || strstr(path, "/../")) return 1;
|
|
if(strstr(path, "\\.\\") || strstr(path, "\\..\\")) return 1;
|
|
p = path + strlen(path);
|
|
if(p - path >= 2 && !strcmp(p - 2, "/.")) return 1;
|
|
if(p - path >= 3 && !strcmp(p - 3, "/..")) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* Work out the file a rule points at: expand the template, then refuse
|
|
anything that does not look like a plain path below a root. */
|
|
static int targetpath(struct httpreq *r, const unsigned char *params, char *out, int outsize)
|
|
{
|
|
if(!params || !*params) return 1;
|
|
if(expand(out, outsize, params, r->path, r->caps, r->ncaps)) return 1;
|
|
if(targetunsafe(out)) return 1;
|
|
return 0;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
/* Windows reads a long path only in its extended form, and only through the
|
|
wide interface, so a full path is converted to \\?\ before it is opened:
|
|
a drive becomes \\?\C:\..., a share \\?\UNC\server\share\...
|
|
*/
|
|
static int widepath(const char *path, wchar_t *out, int outchars)
|
|
{
|
|
char prefixed[HTTPSRV_LINE + 8];
|
|
char *p;
|
|
|
|
if(!strncmp(path, "\\\\?\\", 4)) snprintf(prefixed, sizeof(prefixed), "%s", path);
|
|
else if(path[0] == '\\' && path[1] == '\\')
|
|
snprintf(prefixed, sizeof(prefixed), "\\\\?\\UNC\\%s", path + 2);
|
|
else snprintf(prefixed, sizeof(prefixed), "\\\\?\\%s", path);
|
|
|
|
/* the extended form takes no forward slashes */
|
|
for(p = prefixed; *p; p++) if(*p == '/') *p = '\\';
|
|
|
|
return MultiByteToWideChar(CP_UTF8, 0, prefixed, -1, out, outchars) > 0? 0 : 1;
|
|
}
|
|
#endif
|
|
|
|
/* Opening and measuring a file, in the terms each platform wants. */
|
|
struct filemeta {
|
|
uint64_t size;
|
|
time_t mtime;
|
|
int isreg;
|
|
};
|
|
|
|
static int filemeta(const char *path, struct filemeta *m)
|
|
{
|
|
#ifdef _WIN32
|
|
wchar_t wide[HTTPSRV_LINE];
|
|
#ifdef __WATCOMC__
|
|
/* Open Watcom has the 64-bit stat under its older name */
|
|
#ifndef _S_IFREG
|
|
#define _S_IFREG S_IFREG
|
|
#endif
|
|
struct _stati64 st;
|
|
|
|
if(widepath(path, wide, HTTPSRV_LINE) || _wstati64(wide, &st)) return 1;
|
|
#else
|
|
struct _stat64 st;
|
|
|
|
if(widepath(path, wide, HTTPSRV_LINE) || _wstat64(wide, &st)) return 1;
|
|
#endif
|
|
m->size = (uint64_t)st.st_size;
|
|
m->mtime = st.st_mtime;
|
|
m->isreg = (st.st_mode & _S_IFREG) != 0;
|
|
#else
|
|
struct stat st;
|
|
|
|
if(stat(path, &st)) return 1;
|
|
m->size = (uint64_t)st.st_size;
|
|
m->mtime = st.st_mtime;
|
|
m->isreg = S_ISREG(st.st_mode);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static int fileopen(const char *path)
|
|
{
|
|
#ifdef _WIN32
|
|
wchar_t wide[HTTPSRV_LINE];
|
|
|
|
if(widepath(path, wide, HTTPSRV_LINE)) return -1;
|
|
return _wopen(wide, _O_RDONLY | _O_BINARY);
|
|
#else
|
|
return open(path, O_RDONLY);
|
|
#endif
|
|
}
|
|
|
|
/* Types named in the configuration, tried before the built in list so an
|
|
installation can add what it serves without waiting for a release. */
|
|
struct ctypeentry {
|
|
struct ctypeentry *next;
|
|
char *ext;
|
|
char *type;
|
|
};
|
|
|
|
static struct ctypeentry *ctypes = NULL;
|
|
|
|
int h_http_content_type(int argc, unsigned char **argv)
|
|
{
|
|
struct ctypeentry *e;
|
|
|
|
e = malloc(sizeof(struct ctypeentry));
|
|
if(!e) return 21;
|
|
e->ext = strdup((char *)argv[1]);
|
|
e->type = strdup((char *)argv[2]);
|
|
if(!e->ext || !e->type){
|
|
free(e->ext);
|
|
free(e->type);
|
|
free(e);
|
|
return 21;
|
|
}
|
|
e->next = ctypes;
|
|
ctypes = e;
|
|
return 0;
|
|
}
|
|
|
|
static void freecontenttypes(void)
|
|
{
|
|
struct ctypeentry *e, *next;
|
|
|
|
for(e = ctypes; e; e = next){
|
|
next = e->next;
|
|
free(e->ext);
|
|
free(e->type);
|
|
free(e);
|
|
}
|
|
ctypes = NULL;
|
|
}
|
|
|
|
static const char * contenttype(const char *path)
|
|
{
|
|
struct ctypeentry *e;
|
|
static const struct { const char *ext; const char *type; } types[] = {
|
|
{".html", "text/html"}, {".htm", "text/html"},
|
|
{".css", "text/css"}, {".js", "application/javascript"},
|
|
{".txt", "text/plain"}, {".xml", "text/xml"},
|
|
{".json", "application/json"},
|
|
{".gif", "image/gif"}, {".jpeg", "image/jpeg"}, {".jpg", "image/jpeg"},
|
|
{".png", "image/png"}, {".svg", "image/svg+xml"}, {".ico", "image/x-icon"},
|
|
{".pdf", "application/pdf"}, {NULL, NULL}
|
|
};
|
|
const char *dot = strrchr(path, '.');
|
|
int i;
|
|
|
|
if(!dot) return "application/octet-stream";
|
|
for(e = ctypes; e; e = e->next)
|
|
if(!strcasecmp(dot, e->ext) || !strcasecmp(dot + 1, e->ext)) return e->type;
|
|
for(i = 0; types[i].ext; i++)
|
|
if(!strcasecmp(dot, types[i].ext)) return types[i].type;
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
/* Hand a file to the client without carrying it through this process where
|
|
the platform can do that, and read it in the usual way where it cannot.
|
|
TLS is the case where it cannot: the bytes have to be encrypted on the way
|
|
out, so the kernel cannot be left to copy them. */
|
|
static int sendfilecontent(struct httpreq *r, int fd, uint64_t size)
|
|
{
|
|
struct clientparam *param = r->param;
|
|
char buf[HTTPSRV_BLOCK];
|
|
uint64_t sent = 0;
|
|
|
|
#ifdef HTTPSRV_SENDFILE
|
|
/* Only where the bytes leave as they are. TLS has to see them, so the
|
|
kernel cannot be left to copy the file straight to the socket. */
|
|
if(param->srv->so._send == so._send){
|
|
while(sent < size){
|
|
uint64_t left = size - sent;
|
|
ssize_t n = -1;
|
|
|
|
if(left > HTTPSRV_SENDMAX) left = HTTPSRV_SENDMAX;
|
|
#if defined(__linux__)
|
|
off_t off = (off_t)sent;
|
|
|
|
n = sendfile(param->clisock, fd, &off, (size_t)left);
|
|
#elif defined(__APPLE__)
|
|
off_t len = (off_t)left;
|
|
|
|
if(!sendfile(fd, param->clisock, (off_t)sent, &len, NULL, 0) || errno == EAGAIN)
|
|
n = (ssize_t)len;
|
|
#elif defined(__FreeBSD__)
|
|
off_t written = 0;
|
|
|
|
if(!sendfile(fd, param->clisock, (off_t)sent, (size_t)left,
|
|
NULL, &written, 0) || errno == EAGAIN)
|
|
n = (ssize_t)written;
|
|
#endif
|
|
if(n <= 0) break; /* whatever the reason, read it instead */
|
|
sent += (uint64_t)n;
|
|
}
|
|
if(sent >= size) return 0;
|
|
}
|
|
#elif defined(_WIN32)
|
|
if(param->srv->so._send == so._send && size <= HTTPSRV_SENDMAX){
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
|
|
if(h != INVALID_HANDLE_VALUE
|
|
&& TransmitFile(param->clisock, h, (DWORD)size, 0, NULL, NULL, 0))
|
|
return 0;
|
|
}
|
|
#endif
|
|
if(lseek(fd, (off_t)sent, SEEK_SET) == (off_t)-1) return 1;
|
|
while(sent < size){
|
|
uint64_t left = size - sent;
|
|
int want = left > (uint64_t)sizeof(buf)? (int)sizeof(buf) : (int)left;
|
|
int got;
|
|
|
|
got = (int)read(fd, buf, want);
|
|
if(got <= 0) return 1;
|
|
if(httpsrv_send(r, buf, got)) return 1;
|
|
sent += (uint64_t)got;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* buf takes at least 64 characters */
|
|
static void lastmodhdr(time_t mtime, char *buf)
|
|
{
|
|
memcpy(buf, "Last-Modified: ", 15);
|
|
httpdate(mtime, buf + 15);
|
|
strcat(buf, "\r\n");
|
|
}
|
|
|
|
/* A client which has the file already sends the time it has, and gets told to
|
|
keep it. Only an answer which would have been 200 can be turned into one:
|
|
a rule answering with a status of its own is answering something else. */
|
|
static int notmodified(struct httpreq *r, time_t mtime)
|
|
{
|
|
return r->ims && mtime <= r->ims && (!r->code || r->code == 200)
|
|
&& strcasecmp(r->method, "POST");
|
|
}
|
|
|
|
static int op_file(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
char path[HTTPSRV_LINE];
|
|
char lastmod[64];
|
|
struct filemeta meta;
|
|
int fd;
|
|
|
|
if(targetpath(r, params, path, sizeof(path))) return op_forbidden(r);
|
|
|
|
if(filemeta(path, &meta) || !meta.isreg) return op_notfound(r);
|
|
lastmodhdr(meta.mtime, lastmod);
|
|
if(notmodified(r, meta.mtime))
|
|
return httpsrv_head(r, 304, NULL, HTTPSRV_LEN_NONE, lastmod);
|
|
|
|
fd = fileopen(path);
|
|
if(fd < 0) return op_notfound(r);
|
|
if(httpsrv_head(r, r->code? r->code : 200,
|
|
r->ctype? r->ctype : contenttype(path), (int64_t)meta.size, lastmod)){
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
if(!strcasecmp(r->method, "HEAD")){
|
|
close(fd);
|
|
return 0;
|
|
}
|
|
if(sendfilecontent(r, fd, meta.size)){
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
close(fd);
|
|
return 0;
|
|
}
|
|
|
|
/* Files read once and kept. A hit checks that the file has not been replaced
|
|
since, which costs one stat and keeps a running server from serving what
|
|
an editor has already changed. A rule which gives a max-age has already told
|
|
clients how long the file may be treated as unchanged, so within that time
|
|
the server may equally trust the copy it holds, and the stat is skipped.
|
|
|
|
What was read is counted rather than locked: a request takes a reference to
|
|
the content under the mutex and lets it go when it has been written out, so
|
|
the file goes to the socket with nothing held, and a copy which has been
|
|
replaced meanwhile lives until the last request using it is done with it.
|
|
The size and the time belong to the content and not to the entry, so the
|
|
length in the header and the bytes after it can never come from different
|
|
copies of the file. */
|
|
struct cachedata {
|
|
int refs;
|
|
uint64_t size;
|
|
time_t mtime;
|
|
char data[1];
|
|
};
|
|
|
|
struct cachedfile {
|
|
struct cachedfile *next;
|
|
char *path;
|
|
struct cachedata *content;
|
|
time_t checked;
|
|
};
|
|
|
|
static struct cachedfile *cachedfiles = NULL;
|
|
static _3proxy_mutex_t cache_mutex;
|
|
static int cache_ready = 0;
|
|
|
|
void httpsrv_init(void)
|
|
{
|
|
freecontenttypes();
|
|
if(!cache_ready){
|
|
cache_ready = 1;
|
|
_3proxy_mutex_init(&cache_mutex);
|
|
}
|
|
}
|
|
|
|
static struct cachedata * cache_read(const char *path, const struct filemeta *meta)
|
|
{
|
|
struct cachedata *cd;
|
|
uint64_t got = 0;
|
|
int fd;
|
|
|
|
fd = fileopen(path);
|
|
if(fd < 0) return NULL;
|
|
cd = malloc(sizeof(struct cachedata) + (size_t)meta->size);
|
|
if(!cd){
|
|
close(fd);
|
|
return NULL;
|
|
}
|
|
while(got < meta->size){
|
|
int n = (int)read(fd, cd->data + got, (size_t)(meta->size - got));
|
|
|
|
if(n <= 0) break;
|
|
got += (uint64_t)n;
|
|
}
|
|
close(fd);
|
|
if(got != meta->size){
|
|
free(cd);
|
|
return NULL;
|
|
}
|
|
cd->refs = 1; /* the one the caller is given */
|
|
cd->size = meta->size;
|
|
cd->mtime = meta->mtime;
|
|
return cd;
|
|
}
|
|
|
|
/* both called with the mutex held */
|
|
static struct cachedata * cache_hold(struct cachedata *cd)
|
|
{
|
|
cd->refs++;
|
|
return cd;
|
|
}
|
|
|
|
static void cache_drop(struct cachedata *cd)
|
|
{
|
|
if(cd && --cd->refs <= 0) free(cd);
|
|
}
|
|
|
|
static void cache_release(struct cachedata *cd)
|
|
{
|
|
_3proxy_mutex_lock(&cache_mutex);
|
|
cache_drop(cd);
|
|
_3proxy_mutex_unlock(&cache_mutex);
|
|
}
|
|
|
|
static int op_cache(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
char path[HTTPSRV_LINE];
|
|
char lastmod[64];
|
|
struct cachedfile *cf;
|
|
struct cachedata *content = NULL, *fresh;
|
|
struct filemeta meta;
|
|
time_t now = time(NULL);
|
|
int res;
|
|
|
|
if(targetpath(r, params, path, sizeof(path))) return op_forbidden(r);
|
|
|
|
/* Within the time the rule promised, what is held is answered with as it
|
|
is: the file system is not asked again. */
|
|
if(r->maxage > 0){
|
|
_3proxy_mutex_lock(&cache_mutex);
|
|
for(cf = cachedfiles; cf; cf = cf->next){
|
|
if(!strcmp(cf->path, path) && now - cf->checked < (time_t)r->maxage){
|
|
content = cache_hold(cf->content);
|
|
break;
|
|
}
|
|
}
|
|
_3proxy_mutex_unlock(&cache_mutex);
|
|
}
|
|
|
|
if(!content){
|
|
if(filemeta(path, &meta) || !meta.isreg) return op_notfound(r);
|
|
if(meta.size > HTTPSRV_MAXCACHED) return op_file(r, params);
|
|
|
|
_3proxy_mutex_lock(&cache_mutex);
|
|
for(cf = cachedfiles; cf; cf = cf->next){
|
|
if(!strcmp(cf->path, path) && cf->content->mtime == meta.mtime
|
|
&& cf->content->size == meta.size){
|
|
cf->checked = now;
|
|
content = cache_hold(cf->content);
|
|
break;
|
|
}
|
|
}
|
|
_3proxy_mutex_unlock(&cache_mutex);
|
|
}
|
|
|
|
if(!content){
|
|
fresh = cache_read(path, &meta);
|
|
if(!fresh) return op_file(r, params);
|
|
|
|
_3proxy_mutex_lock(&cache_mutex);
|
|
for(cf = cachedfiles; cf; cf = cf->next) if(!strcmp(cf->path, path)) break;
|
|
if(cf){
|
|
cache_drop(cf->content); /* it was replaced on disk */
|
|
cf->content = cache_hold(fresh);
|
|
cf->checked = now;
|
|
}
|
|
else if((cf = malloc(sizeof(struct cachedfile)))){
|
|
cf->path = strdup(path);
|
|
cf->content = cache_hold(fresh);
|
|
cf->checked = now;
|
|
cf->next = cachedfiles;
|
|
if(cf->path) cachedfiles = cf;
|
|
else {
|
|
cache_drop(fresh); /* the entry never went in */
|
|
free(cf);
|
|
}
|
|
}
|
|
_3proxy_mutex_unlock(&cache_mutex);
|
|
content = fresh; /* read with a reference of its own */
|
|
}
|
|
|
|
lastmodhdr(content->mtime, lastmod);
|
|
if(notmodified(r, content->mtime)){
|
|
res = httpsrv_head(r, 304, NULL, HTTPSRV_LEN_NONE, lastmod);
|
|
cache_release(content);
|
|
return res;
|
|
}
|
|
|
|
res = httpsrv_head(r, r->code? r->code : 200,
|
|
r->ctype? r->ctype : contenttype(path), (int64_t)content->size, lastmod);
|
|
if(!res && strcasecmp(r->method, "HEAD"))
|
|
res = httpsrv_send(r, content->data, (int)content->size);
|
|
cache_release(content);
|
|
return res;
|
|
}
|
|
|
|
/* Send the client somewhere else. The parameters are a location, optionally
|
|
preceded by the status to use. */
|
|
static int op_redir(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
char location[HTTPSRV_LINE];
|
|
char hdr[HTTPSRV_LINE];
|
|
const unsigned char *p = params;
|
|
int code = r->code? r->code : 302;
|
|
|
|
if(!p || !*p) return op_forbidden(r);
|
|
if(isdigit(*p)){
|
|
code = atoi((char *)p);
|
|
while(isdigit(*p)) p++;
|
|
while(*p == ' ' || *p == '\t') p++;
|
|
if(code < 300 || code > 399) code = 302;
|
|
}
|
|
if(expand(location, sizeof(location), p, r->path, r->caps, r->ncaps))
|
|
return op_forbidden(r);
|
|
if(strchr(location, '\r') || strchr(location, '\n')) return op_forbidden(r);
|
|
|
|
if(strlen(location) + sizeof("Location: \r\n") > sizeof(hdr)) return op_forbidden(r);
|
|
sprintf(hdr, "Location: %s\r\n", location);
|
|
|
|
return httpsrv_head(r, code, NULL, 0, hdr);
|
|
}
|
|
|
|
/* Answer with a status and nothing else. The status comes from the rule, as
|
|
do any headers it adds. */
|
|
static int op_reply(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
int code = r->code? r->code : 200;
|
|
|
|
(void)params;
|
|
if(code / 100 == 1 || code == 204 || code == 304)
|
|
return httpsrv_head(r, code, NULL, HTTPSRV_LEN_NONE, NULL);
|
|
return httpsrv_head(r, code, NULL, 0, NULL);
|
|
}
|
|
|
|
/* A name a rule builds has to be one, since it decides which rules are taken
|
|
after it and is written into the log. */
|
|
static int hostunsafe(const char *host)
|
|
{
|
|
const char *p;
|
|
|
|
if(!*host || strlen(host) > 255) return 1;
|
|
for(p = host; *p; p++){
|
|
if(isalnum((unsigned char)*p)) continue;
|
|
if(*p == '.' || *p == '-' || *p == '_' || *p == ':'
|
|
|| *p == '[' || *p == ']') continue;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Change the host and let the rules after this one decide what to do with the
|
|
request. What the stars of the host pattern stood for are what $1 upwards
|
|
mean here, the way they mean the stars of the URL in a rewrite. */
|
|
static int op_rewrite_host(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
char host[sizeof(r->host)];
|
|
|
|
if(!params || !*params) return op_forbidden(r);
|
|
if(expand(host, sizeof(host), params, r->host, r->hostcaps, r->nhostcaps))
|
|
return op_forbidden(r);
|
|
if(hostunsafe(host)) return op_forbidden(r);
|
|
strcpy(r->host, host);
|
|
return HTTPSRV_REWRITTEN;
|
|
}
|
|
|
|
/* Change the path and let the rules after this one decide what to do with
|
|
the request. */
|
|
static int op_rewrite(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
char path[sizeof(r->path)];
|
|
|
|
if(!params || !*params) return op_forbidden(r);
|
|
if(expand(path, sizeof(path), params, r->path, r->caps, r->ncaps))
|
|
return op_forbidden(r);
|
|
/* What comes out is a request path and is checked as one. A path on this
|
|
machine is a different thing, and asking a rewrite to look like one
|
|
would leave nothing writable on Windows, where such a path names a
|
|
drive or a share. */
|
|
if(path[0] != '/' || pathunsafe(path)) return op_forbidden(r);
|
|
strcpy(r->path, path);
|
|
return HTTPSRV_REWRITTEN;
|
|
}
|
|
|
|
static int op_echo(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
struct clientparam *param = r->param;
|
|
char addr[64];
|
|
char body[HTTPSRV_LINE * 2];
|
|
int len;
|
|
PROXYSOCKADDRTYPE sa;
|
|
SASIZETYPE sasize = sizeof(sa);
|
|
|
|
memset(&sa, 0, sizeof(sa));
|
|
if(param->srv->so._getpeername(param->sostate, param->clisock,
|
|
(struct sockaddr *)&sa, &sasize) ||
|
|
!myinet_ntop(*SAFAMILY(&sa), SAADDR(&sa), addr, sizeof(addr))){
|
|
strcpy(addr, "unknown");
|
|
}
|
|
|
|
len = snprintf(body, sizeof(body),
|
|
"peer.addr=%s\n"
|
|
"peer.port=%hu\n"
|
|
"method=%s\n"
|
|
"path=%s\n"
|
|
"query=%s\n"
|
|
"host=%s\n"
|
|
"content.length=%"PRIu64"\n"
|
|
"glob.start=%d\n"
|
|
"glob.len=%d\n"
|
|
"glob=%.*s\n",
|
|
addr, ntohs(*SAPORT(&sa)), r->method, r->path, r->query,
|
|
r->host, r->contentlen, r->globstart, r->globlen,
|
|
r->globlen, r->path + r->globstart);
|
|
|
|
if(len < 0) return 1;
|
|
if(len > (int)sizeof(body) - 1) len = (int)sizeof(body) - 1;
|
|
|
|
if(httpsrv_head(r, 200, "text/plain", (int64_t)len, NULL)) return 1;
|
|
return httpsrv_send(r, body, len);
|
|
}
|
|
|
|
/* /data?size=N&chunked=0|1&status=NNN&block=N&delay=ms
|
|
Produces exactly N bytes of body. */
|
|
static int op_data(struct httpreq *r, const unsigned char *params)
|
|
{
|
|
char buf[HTTPSRV_BLOCK];
|
|
int64_t size, block, delay, status;
|
|
int chunked;
|
|
uint64_t sent = 0;
|
|
|
|
size = qparam((const char *)params, "size", 0);
|
|
size = qparam(r->query, "size", size);
|
|
if(size < 0) size = 0;
|
|
block = qparam((const char *)params, "block", HTTPSRV_BLOCK);
|
|
block = qparam(r->query, "block", block);
|
|
if(block < 1 || block > HTTPSRV_BLOCK) block = HTTPSRV_BLOCK;
|
|
status = qparam((const char *)params, "status", 200);
|
|
status = qparam(r->query, "status", status);
|
|
if(status < 100 || status > 599) status = 200;
|
|
chunked = qparam(r->query, "chunked", qparam((const char *)params, "chunked", 0)) != 0;
|
|
delay = qparam(r->query, "delay", qparam((const char *)params, "delay", 0));
|
|
|
|
if(httpsrv_head(r, (int)status, "application/octet-stream",
|
|
chunked? (int64_t)HTTPSRV_LEN_CHUNKED : (int64_t)size, NULL)) return 1;
|
|
|
|
while(sent < (uint64_t)size){
|
|
uint64_t left = (uint64_t)size - sent;
|
|
int len = (left < (uint64_t)block)? (int)left : (int)block;
|
|
|
|
httpsrv_fill(buf, len, sent);
|
|
|
|
if(delay > 0){
|
|
#ifdef _WIN32
|
|
usleep(delay);
|
|
#else
|
|
usleep(delay * 1000);
|
|
#endif
|
|
}
|
|
|
|
if(chunked){
|
|
if(httpchunk(r->param, buf, len)) return 1;
|
|
}
|
|
else if(httpsrv_send(r, buf, len)) return 1;
|
|
|
|
sent += (uint64_t)len;
|
|
}
|
|
|
|
if(chunked) return httpchunk(r->param, NULL, 0);
|
|
return 0;
|
|
}
|
|
|
|
static int op_authrequired(struct httpreq *r)
|
|
{
|
|
static const char body[] = "authentication required\n";
|
|
|
|
r->hdrs = NULL;
|
|
r->maxage = -1;
|
|
r->keepalive = 0;
|
|
if(httpsrv_printf(r, "HTTP/1.0 %s\r\n"
|
|
"%s: Basic realm=\"3proxy\"\r\n"
|
|
"Content-Type: text/plain\r\n"
|
|
"Content-Length: %d\r\n"
|
|
"Connection: close\r\n\r\n",
|
|
r->proxy? "407 Proxy Authentication Required" : "401 Authentication Required",
|
|
r->proxy? "Proxy-Authenticate" : "WWW-Authenticate",
|
|
(int)sizeof(body) - 1)) return 1;
|
|
return httpsrv_send(r, body, (int)sizeof(body) - 1);
|
|
}
|
|
|
|
static int op_forbidden(struct httpreq *r)
|
|
{
|
|
static const char body[] = "forbidden\n";
|
|
|
|
r->hdrs = NULL;
|
|
r->maxage = -1;
|
|
if(httpsrv_head(r, 403, "text/plain", (int64_t)sizeof(body) - 1, NULL)) return 1;
|
|
return httpsrv_send(r, body, (int)sizeof(body) - 1);
|
|
}
|
|
|
|
static int op_badrequest(struct httpreq *r)
|
|
{
|
|
static const char body[] = "bad request\n";
|
|
|
|
r->hdrs = NULL;
|
|
r->maxage = -1;
|
|
if(httpsrv_head(r, 400, "text/plain", (int64_t)sizeof(body) - 1, NULL)) return 1;
|
|
return httpsrv_send(r, body, (int)sizeof(body) - 1);
|
|
}
|
|
|
|
static int op_notfound(struct httpreq *r)
|
|
{
|
|
static const char body[] = "not found\n";
|
|
|
|
r->hdrs = NULL;
|
|
r->maxage = -1;
|
|
if(httpsrv_head(r, 404, "text/plain", (int64_t)sizeof(body) - 1, NULL)) return 1;
|
|
return httpsrv_send(r, body, (int)sizeof(body) - 1);
|
|
}
|
|
|
|
/* Operations an http line can name. The rule supplies the parameters, so the
|
|
same operation serves different content on different urls. */
|
|
static struct httpop {
|
|
const char *name;
|
|
int (*fn)(struct httpreq *, const unsigned char *params);
|
|
int framed; /* the answer says how long it is, so the connection may be kept */
|
|
int handoff; /* the request is answered by the proxy code, not here */
|
|
} httpops[] = {
|
|
{"echo", op_echo, 1, 0},
|
|
{"data", op_data, 1, 0},
|
|
{"file", op_file, 1, 0},
|
|
{"cache", op_cache, 1, 0},
|
|
{"redir", op_redir, 1, 0},
|
|
{"reply", op_reply, 1, 0},
|
|
{"rewrite", op_rewrite, 1, 0},
|
|
{"rewrite_host", op_rewrite_host, 1, 0},
|
|
{"admin", op_admin, 0, 0},
|
|
{"admin_counters", op_admin_counters, 0, 0},
|
|
{"admin_reload", op_admin_reload, 0, 0},
|
|
{"admin_services", op_admin_services, 0, 0},
|
|
{"proxypass", NULL, 0, 1},
|
|
{NULL, NULL, 0, 0}
|
|
};
|
|
|
|
void freehttprules(struct httprule *rule)
|
|
{
|
|
struct httprule *next;
|
|
|
|
while(rule){
|
|
next = rule->next;
|
|
if(rule->host.name) free(rule->host.name);
|
|
if(rule->url.name) free(rule->url.name);
|
|
#ifdef WITH_PCRE
|
|
if(rule->host.re) pcre_pattern_free(rule->host.re);
|
|
if(rule->url.re) pcre_pattern_free(rule->url.re);
|
|
#endif
|
|
if(rule->params) free(rule->params);
|
|
if(rule->ctype) free(rule->ctype);
|
|
if(rule->hdrs) free(rule->hdrs);
|
|
free(rule);
|
|
rule = next;
|
|
}
|
|
}
|
|
|
|
int httpopbyname(const unsigned char *name)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; httpops[i].name; i++){
|
|
if(!strcmp((char *)name, httpops[i].name)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* Read and discard a request body.
|
|
|
|
The reply is followed by a close, and closing a socket that still holds
|
|
unread data resets the connection rather than ending it, which costs the
|
|
client the reply it was about to read. Bounded, so a client cannot keep
|
|
the server reading.
|
|
*/
|
|
static void httpsrv_drain(struct clientparam *param, uint64_t len)
|
|
{
|
|
char buf[HTTPSRV_BLOCK];
|
|
|
|
if(len > HTTPSRV_MAXBODY) len = HTTPSRV_MAXBODY;
|
|
while(len){
|
|
int want = (len > (uint64_t)sizeof(buf))? (int)sizeof(buf) : (int)len;
|
|
int got = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, want, EOF,
|
|
conf.timeouts[STRING_S]);
|
|
|
|
if(got <= 0) break;
|
|
len -= (uint64_t)got;
|
|
}
|
|
}
|
|
|
|
/* Ends a connection this server is closing while the client may still be
|
|
sending: a body which was not read, or a request sent behind one answered
|
|
with a close. Closing with that unread resets the connection, and the
|
|
client loses the answer. The sending side is shut down instead and what
|
|
arrives is thrown away until the client closes too, for a bounded time
|
|
and amount, which is what lets the answer be read. */
|
|
static void httpsrv_linger(struct clientparam *param)
|
|
{
|
|
char buf[HTTPSRV_BLOCK];
|
|
struct pollfd fd;
|
|
uint64_t left = HTTPSRV_MAXBODY;
|
|
time_t deadline = time(NULL) + conf.timeouts[LINGER_TO];
|
|
|
|
if(param->clisock == INVALID_SOCKET) return;
|
|
if(param->srv->so._shutdown(param->sostate, param->clisock, SHUT_WR)) return;
|
|
while(left){
|
|
int timeo = (int)(deadline - time(NULL));
|
|
int got;
|
|
|
|
if(timeo <= 0) break;
|
|
memset(&fd, 0, sizeof(fd));
|
|
fd.fd = param->clisock;
|
|
fd.events = POLLIN;
|
|
if(param->srv->so._poll(param->sostate, &fd, 1, timeo * 1000) <= 0) break;
|
|
got = (int)param->srv->so._recv(param->sostate, param->clisock, buf,
|
|
(left > sizeof(buf))? (int)sizeof(buf) : (int)left, 0);
|
|
if(got <= 0) break;
|
|
left -= (uint64_t)got;
|
|
}
|
|
}
|
|
|
|
/* Keeps the request as the client wrote it. Only what a handoff needs: the
|
|
request line and the headers, exactly as they came, since the proxy code
|
|
reads them again from the beginning. */
|
|
static int rawkeep(struct httpreq *r, const char *line, int len)
|
|
{
|
|
if(r->rawlen + len + 3 > r->rawsize){
|
|
int want = r->rawsize? r->rawsize * 2 : 2048;
|
|
unsigned char *grown;
|
|
|
|
while(want < r->rawlen + len + 3) want *= 2;
|
|
if(want > HTTPSRV_MAXRAW) return 1;
|
|
grown = realloc(r->raw, (size_t)want);
|
|
if(!grown) return 1;
|
|
r->raw = grown;
|
|
r->rawsize = want;
|
|
}
|
|
memcpy(r->raw + r->rawlen, line, (size_t)len);
|
|
r->rawlen += len;
|
|
return 0;
|
|
}
|
|
|
|
/* Hands the request to the proxy code, which reads it again from the client
|
|
buffer and answers it as a proxy would, asking for its own credentials if
|
|
the configuration wants them. The connection stays with this service. */
|
|
static int proxypass(struct clientparam *param, struct httpreq *r)
|
|
{
|
|
void *next;
|
|
int state, stale = 0;
|
|
|
|
/* This is the one place a child is called rather than returned, so it
|
|
is the one place a chain of them could nest. It cannot: the proxy
|
|
returns whatever child it would redirect to instead of calling it, and
|
|
a request already being answered on behalf of another child is never
|
|
handed on again. */
|
|
if(param->onerequest) return 1;
|
|
if(!r->raw || rawkeep(r, "\r\n", 2)) return 1; /* the end of the headers */
|
|
|
|
/* What is open towards the server belongs to the request before this
|
|
one. It is no use for this request if it went somewhere else, and no
|
|
use at all if the server has since closed it: the proxy watches for
|
|
that between its own requests, and this is where that falls to when it
|
|
is entered one request at a time. */
|
|
if(param->remsock != INVALID_SOCKET){
|
|
struct pollfd fd;
|
|
|
|
memset(&fd, 0, sizeof(fd));
|
|
fd.fd = param->remsock;
|
|
fd.events = POLLIN;
|
|
if(param->srv->so._poll(param->sostate, &fd, 1, 0) > 0
|
|
&& (fd.revents & (POLLIN|POLLHUP|POLLERR|POLLNVAL))){
|
|
/* anything arriving now belongs to no request */
|
|
stale = 1;
|
|
}
|
|
}
|
|
if(param->remsock != INVALID_SOCKET && (stale || (r->lasthost
|
|
&& strcasecmp(r->lasthost, r->host)))){
|
|
param->srv->so._shutdown(param->sostate, param->remsock, SHUT_RDWR);
|
|
param->srv->so._closesocket(param->sostate, param->remsock);
|
|
param->remsock = INVALID_SOCKET;
|
|
param->redirected = 0;
|
|
param->redirtype = 0;
|
|
memset(¶m->sinsl, 0, sizeof(param->sinsl));
|
|
memset(¶m->sinsr, 0, sizeof(param->sinsr));
|
|
memset(¶m->req, 0, sizeof(param->req));
|
|
}
|
|
if(r->lasthost){
|
|
strncpy(r->lasthost, r->host, 255);
|
|
r->lasthost[255] = 0;
|
|
}
|
|
|
|
if(pushbackcli(param, r->raw, r->rawlen)) return 1;
|
|
|
|
param->onerequest = 1;
|
|
next = proxychild(param);
|
|
state = param->onerequest;
|
|
param->onerequest = 0;
|
|
|
|
/* The proxy asked for another child to take the connection over: it is
|
|
no longer this service's to keep. */
|
|
if(next){
|
|
r->handoff = next;
|
|
return 2;
|
|
}
|
|
if(state != 2) r->keepalive = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* Reads one request and answers it. Returns 0 when nothing more came on a
|
|
connection which was being kept open, which is not a request and not an
|
|
error, so there is nothing to answer and nothing to log. */
|
|
static int httpsrv_request(struct clientparam *param, struct httpreq *r)
|
|
{
|
|
char buf[HTTPSRV_LINE];
|
|
char rootpath[2];
|
|
char *sp, *q;
|
|
struct httprule *rule;
|
|
int i, hdrs = 0;
|
|
|
|
i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1, '\n',
|
|
conf.timeouts[STRING_S]);
|
|
if(i <= 0 && !r->first) return 0; /* the client is done with us */
|
|
if(i < 5) RETURN(701);
|
|
if(rawkeep(r, buf, i)) RETURN(710);
|
|
buf[i] = 0;
|
|
|
|
sp = strchr(buf, ' ');
|
|
if(!sp) RETURN(702);
|
|
*sp = 0;
|
|
if(copyfield(r->method, sizeof(r->method), buf)) RETURN(703);
|
|
|
|
if(!strcasecmp(r->method, "GET")) param->operation = HTTP_GET;
|
|
else if(!strcasecmp(r->method, "POST")) param->operation = HTTP_POST;
|
|
else if(!strcasecmp(r->method, "PUT")) param->operation = HTTP_PUT;
|
|
else if(!strcasecmp(r->method, "HEAD")) param->operation = HTTP_HEAD;
|
|
else param->operation = HTTP_OTHER;
|
|
|
|
while(*++sp == ' ');
|
|
q = strchr(sp, ' ');
|
|
if(q){
|
|
char *v = q + 1;
|
|
|
|
*q = 0;
|
|
while(*v == ' ') v++;
|
|
if(!strncasecmp(v, "HTTP/1.1", 8)) r->version = 1;
|
|
}
|
|
q = sp + strcspn(sp, "\r\n");
|
|
*q = 0;
|
|
|
|
/* 1.1 keeps the connection unless the client says otherwise, 1.0 only
|
|
when the client asks for it. */
|
|
r->keepalive = r->version;
|
|
|
|
/* A client talking to a proxy names the whole URL, or, for a tunnel, the
|
|
host alone. The name in the request is the one that counts then, and
|
|
the credentials arrive in Proxy-Authorization, because the client is
|
|
identifying itself to a proxy and not to a site. */
|
|
if(!strncasecmp(sp, "http://", 7)){
|
|
char *slash;
|
|
|
|
r->proxy = 1;
|
|
sp += 7;
|
|
slash = strchr(sp, '/');
|
|
if(slash) *slash = 0;
|
|
if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706);
|
|
if(slash){
|
|
*slash = '/';
|
|
sp = slash;
|
|
}
|
|
else {
|
|
strcpy(rootpath, "/");
|
|
sp = rootpath;
|
|
}
|
|
}
|
|
else if(!strcasecmp(r->method, "CONNECT")){
|
|
r->proxy = r->connect = 1;
|
|
if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706);
|
|
strcpy(rootpath, "/");
|
|
sp = rootpath;
|
|
}
|
|
else if(*sp != '/') RETURN(702);
|
|
|
|
q = strchr(sp, '?');
|
|
if(q){
|
|
*q = 0;
|
|
if(copyfield(r->query, sizeof(r->query), q + 1)) RETURN(704);
|
|
}
|
|
{
|
|
char decoded[sizeof(r->path)];
|
|
|
|
/* Keep the raw path first so a refused request still records what
|
|
was asked for. */
|
|
if(copyfield(r->path, sizeof(r->path), sp)) RETURN(705);
|
|
if(urldecode(decoded, sizeof(decoded), sp)) RETURN(707);
|
|
if(pathunsafe(decoded)) RETURN(708);
|
|
strcpy(r->path, decoded);
|
|
}
|
|
|
|
/* A header longer than the buffer arrives as several lines, so the count
|
|
bounds what is read and not what a client may send in one header. */
|
|
while(hdrs < HTTPSRV_MAXHDR &&
|
|
(i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1,
|
|
'\n', conf.timeouts[STRING_S])) > 2){
|
|
hdrs++;
|
|
if(rawkeep(r, buf, i)) RETURN(710);
|
|
buf[i] = 0;
|
|
if(!strncasecmp(buf, "host:", 5) && !r->proxy){
|
|
sp = buf + 5;
|
|
while(isspace((unsigned char)*sp)) sp++;
|
|
sp[strcspn(sp, "\r\n")] = 0;
|
|
if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706);
|
|
}
|
|
else if((!r->proxy && !strncasecmp(buf, "authorization:", 14))
|
|
|| (r->proxy && !strncasecmp(buf, "proxy-authorization:", 20))){
|
|
char creds[256];
|
|
int clen;
|
|
|
|
sp = buf + (r->proxy? 20 : 14);
|
|
while(isspace((unsigned char)*sp)) sp++;
|
|
if(strncasecmp(sp, "basic", 5)) continue;
|
|
sp += 5;
|
|
while(isspace((unsigned char)*sp)) sp++;
|
|
sp[strcspn(sp, "\r\n")] = 0;
|
|
|
|
clen = de64((unsigned char *)sp, (unsigned char *)creds, sizeof(creds) - 1);
|
|
if(clen <= 0) continue;
|
|
creds[clen] = 0;
|
|
|
|
q = strchr(creds, ':');
|
|
if(q){
|
|
*q = 0;
|
|
if(param->password) free(param->password);
|
|
param->password = (unsigned char *)strdup(q + 1);
|
|
}
|
|
if(param->username) free(param->username);
|
|
param->username = (unsigned char *)strdup(creds);
|
|
}
|
|
else if(!strncasecmp(buf, "connection:", 11)){
|
|
sp = buf + 11;
|
|
while(isspace((unsigned char)*sp)) sp++;
|
|
sp[strcspn(sp, "\r\n")] = 0;
|
|
if(headertoken(sp, "close")) r->keepalive = 0;
|
|
else if(headertoken(sp, "keep-alive")) r->keepalive = 1;
|
|
}
|
|
else if(!strncasecmp(buf, "transfer-encoding:", 18)){
|
|
/* A body this server does not know how to read leaves the
|
|
stream at an unknown place, so the connection ends with
|
|
this request. */
|
|
r->chunkedreq = 1;
|
|
}
|
|
else if(!strncasecmp(buf, "if-modified-since:", 18)){
|
|
r->ims = parsehttpdate(buf + 18);
|
|
}
|
|
else if(!strncasecmp(buf, "content-length:", 15)){
|
|
sscanf(buf + 15, "%"SCNu64"", &r->contentlen);
|
|
}
|
|
}
|
|
|
|
/* The headers ran past what this server reads, so the rest of the request
|
|
is still in the stream and there is no answering it: what follows would
|
|
be read as the request after this one. */
|
|
if(hdrs >= HTTPSRV_MAXHDR) RETURN(712);
|
|
|
|
/* The next request begins where this body ends, so a body which cannot
|
|
be read to its end - one this server does not frame, or one longer
|
|
than it is willing to read - closes the connection instead.
|
|
|
|
The body itself is left where it is until this server knows it is the
|
|
one answering: a request handed to the proxy carries its body there. */
|
|
if(r->chunkedreq || r->contentlen > HTTPSRV_MAXBODY) r->keepalive = 0;
|
|
|
|
if(r->host[0]){
|
|
char host[sizeof(r->host)];
|
|
char *colon;
|
|
|
|
/* Access rules match a bare name, so drop the port the client sent.
|
|
An address in brackets keeps its colons. */
|
|
strcpy(host, r->host);
|
|
colon = (*host == '[')? strchr(host, ']') : host;
|
|
if(colon){
|
|
colon = strchr(colon, ':');
|
|
if(colon) *colon = 0;
|
|
}
|
|
if(*host == '['){
|
|
memmove(host, host + 1, strlen(host));
|
|
colon = strchr(host, ']');
|
|
if(colon) *colon = 0;
|
|
}
|
|
|
|
if(param->hostname) free(param->hostname);
|
|
param->hostname = (unsigned char *)strdup(host);
|
|
}
|
|
|
|
/* The request is answered here, so the address it was sent to is the
|
|
destination an access rule should match. Authorization skips doconnect
|
|
for this service, so naming a destination cannot start a connection. */
|
|
param->req = param->sincl;
|
|
|
|
i = (*param->srv->authfunc)(param);
|
|
/* A rule which redirects is answered by another child: authorization
|
|
names it and reports success, or, where the destination was not needed
|
|
to decide, reports the redirect itself. Where that child is the local
|
|
proxy, this server still answers whatever it has a rule for and lets
|
|
the proxy have the rest, which is what makes one service both a site
|
|
and a proxy. Any other child takes the connection over as it always
|
|
has. */
|
|
if(i == REDIRECT) i = 0;
|
|
if(!i && param->redirectfunc){
|
|
if(param->redirectfunc == (REDIRECTFUNC)proxychild) r->mayproxy = 1;
|
|
/* A redirect back to this service would only ask the same rules
|
|
the same question, so it is left alone rather than bounced
|
|
between children until the count runs out. */
|
|
else if(param->redirectfunc != (REDIRECTFUNC)httpsrvchild){
|
|
r->handoff = (void *)param->redirectfunc;
|
|
return 1;
|
|
}
|
|
}
|
|
if(i && i != 10){
|
|
/* The body goes before the answer, as for any answer this server
|
|
gives: left in the stream it would be read as the next request. */
|
|
if(!r->drained){
|
|
if(r->contentlen) httpsrv_drain(param, r->contentlen);
|
|
r->drained = 1;
|
|
}
|
|
/* 4 no credentials, 5 unknown user, 6 wrong password: all of them
|
|
should let the client offer credentials again. */
|
|
if(i >= 4 && i <= 6) op_authrequired(r);
|
|
else op_forbidden(r);
|
|
RETURN(i);
|
|
}
|
|
|
|
/* A rewrite changes the path and hands the request to the rules that
|
|
follow it, so the walk restarts. The count bounds a set of rules that
|
|
rewrite in a circle. */
|
|
rule = param->srv->httprules;
|
|
for(hdrs = 0; rule && hdrs < HTTPSRV_MAXREWRITE; rule = rule->next){
|
|
if(!patternmatchcaps(&rule->host, (unsigned char *)r->host,
|
|
r->hostcaps, &r->nhostcaps)) continue;
|
|
if(!patternmatchcaps(&rule->url, (unsigned char *)r->path, r->caps, &r->ncaps))
|
|
continue;
|
|
|
|
/* what the first star stood for, which is what admin reads */
|
|
r->globstart = r->ncaps > 1? r->caps[1].start : 0;
|
|
r->globlen = r->ncaps > 1? r->caps[1].len : 0;
|
|
|
|
/* A rule which hands the request on answers nothing itself, and
|
|
the body has to still be there when it does. */
|
|
if(httpops[rule->op].handoff){
|
|
i = proxypass(param, r);
|
|
if(i == 1){
|
|
r->keepalive = 0;
|
|
op_badrequest(r);
|
|
RETURN(711);
|
|
}
|
|
RETURN(0);
|
|
}
|
|
|
|
/* This server is answering, so the body is read and thrown away
|
|
before the answer goes out. */
|
|
if(!r->drained){
|
|
if(r->contentlen) httpsrv_drain(param, r->contentlen);
|
|
r->drained = 1;
|
|
}
|
|
|
|
/* Only an answer which says how long it is may be followed by
|
|
another request on the same connection. */
|
|
if(!httpops[rule->op].framed) r->keepalive = 0;
|
|
|
|
r->ctype = (const char *)rule->ctype;
|
|
r->hdrs = (const char *)rule->hdrs;
|
|
r->maxage = rule->maxage;
|
|
r->code = rule->code;
|
|
if(httpops[rule->op].fn(r, rule->params) == HTTPSRV_REWRITTEN){
|
|
hdrs++;
|
|
continue;
|
|
}
|
|
RETURN(0);
|
|
}
|
|
if(hdrs >= HTTPSRV_MAXREWRITE){
|
|
param->srv->logfunc(param, (unsigned char *)"http: too many rewrites");
|
|
op_badrequest(r);
|
|
RETURN(709);
|
|
}
|
|
/* Nothing here answers it. An access rule may have said the local proxy
|
|
should, which is what makes a service both a site and a proxy. */
|
|
if(r->mayproxy){
|
|
i = proxypass(param, r);
|
|
if(i == 1){
|
|
r->keepalive = 0;
|
|
op_badrequest(r);
|
|
RETURN(711);
|
|
}
|
|
RETURN(0);
|
|
}
|
|
if(!r->drained){
|
|
if(r->contentlen) httpsrv_drain(param, r->contentlen);
|
|
r->drained = 1;
|
|
}
|
|
op_notfound(r);
|
|
RETURN(404);
|
|
|
|
CLEANRET:
|
|
if(param->res >= 700 && param->res < 800){
|
|
r->keepalive = 0;
|
|
op_badrequest(r);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* One connection, and as many requests as the client and the answers allow. */
|
|
void * httpsrvchild(struct clientparam *param)
|
|
{
|
|
struct httpreq r;
|
|
char lasthost[256];
|
|
void *handoff = NULL;
|
|
int first = 1;
|
|
|
|
lasthost[0] = 0;
|
|
for(;;){
|
|
int answered;
|
|
|
|
memset(&r, 0, sizeof(r));
|
|
r.maxage = -1; /* until a rule says otherwise */
|
|
r.param = param;
|
|
r.first = first;
|
|
r.lasthost = lasthost;
|
|
param->res = 0;
|
|
|
|
answered = httpsrv_request(param, &r);
|
|
handoff = r.handoff;
|
|
if(r.raw) free(r.raw);
|
|
if(!answered) break;
|
|
|
|
/* Log the request the way the proxy does: the parameters decide
|
|
what was served, so a bare path is not enough to explain a
|
|
response. */
|
|
{
|
|
char logbuf[sizeof(r.method) + sizeof(r.host) + sizeof(r.path) +
|
|
sizeof(r.query) + 8];
|
|
|
|
sprintf(logbuf, "%s %s %s%s%s", r.method[0]? r.method : "-",
|
|
r.host[0]? r.host : "-", r.path,
|
|
r.query[0]? "?" : "", r.query);
|
|
dolog(param, (unsigned char *)logbuf);
|
|
}
|
|
|
|
if(handoff) break;
|
|
if(!r.keepalive){
|
|
/* the client may still be sending; let it read the answer */
|
|
httpsrv_linger(param);
|
|
break;
|
|
}
|
|
first = 0;
|
|
}
|
|
/* A child named by an access rule takes the connection over, which the
|
|
caller arranges rather than this service calling it. */
|
|
return handoff;
|
|
}
|
|
|
|
#endif
|