/*
** 2001-09-15
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
**
** This source code file implements a small, simple, stand-alone HTTP
** server.
**
** Features:
**
** * Launched from inetd/xinetd/stunnel4, or as a stand-alone server
** * One process per request
** * Deliver static content or run CGI or SCGI
** * Virtual sites based on the "Host:" property of the HTTP header
** * Runs in a chroot jail
** * Unified log file in a CSV format
** * Small code base (this 1 file) to facilitate security auditing
** * Simple setup - no configuration files to misconfigure
**
** This file implements a small and simple but secure and effective web
** server. There are no frills. Anything that could be reasonably
** omitted has been.
**
** Setup rules:
**
** (1) Launch as root from inetd like this:
**
** httpd -logfile logfile -root /home/www -user nobody
**
** It will automatically chroot to /home/www and become user "nobody".
** The logfile name should be relative to the chroot jail.
**
** (2) Directories of the form "*.website" (ex: www_sqlite_org.website)
** contain content. The directory is chosen based on the HTTP_HOST
** request header. If there is no HTTP_HOST header or if the
** corresponding host directory does not exist, then the
** "default.website" is used. If the HTTP_HOST header contains any
** charaters other than [a-zA-Z0-9_.,*~/] then a 403 error is
** generated.
**
** (3) Any file or directory whose name begins with "." or "-" is ignored,
** except if the URL begins with "/.well-known/" then initial "." and
** "-" characters are allowed, but not initial "..". The exception is
** for RFC-5785 to allow letsencrypt or certbot to generate a TLS cert
** using webroot.
**
** (4) Characters other than [0-9a-zA-Z,-./:_~] and any %HH characters
** escapes in the filename are all translated into "_". This is
** a defense against cross-site scripting attacks and other mischief.
**
** (5) Executable files are run as CGI. Files whose name ends with ".scgi"
** trigger and SCGI request (see item 10 below). All other files
** are delivered as is.
**
** (6) For SSL support use stunnel and add the -https 1 option on the
** httpd command-line.
**
** (7) If a file named "-auth" exists in the same directory as the file to
** be run as CGI or to be delivered, then it contains information
** for HTTP Basic authorization. See file format details below.
**
** (8) To run as a stand-alone server, simply add the "-port N" command-line
** option to define which TCP port to listen on.
**
** (9) For static content, the mimetype is determined by the file suffix
** using a table built into the source code below. If you have
** unusual content files, you might need to extend this table.
**
** (10) Content files that end with ".scgi" and that contain text of the
** form "SCGI hostname port" will format an SCGI request and send it
** to hostname:port, the relay back the reply. Error behavior is
** determined by subsequent lines of the .scgi file. See SCGI below
** for details.
**
** Command-line Options:
**
** --root DIR Defines the directory that contains the various
** $HOST.website subdirectories, each containing web content
** for a single virtual host. If launched as root and if
** "--user USER" also appears on the command-line and if
** "--jail 0" is omitted, then the process runs in a chroot
** jail rooted at this directory and under the userid USER.
** This option is required for xinetd launch but defaults
** to "." for a stand-alone web server.
**
** --port N Run in standalone mode listening on TCP port N
**
** --user USER Define the user under which the process should run if
** originally launched as root. This process will refuse to
** run as root (for security). If this option is omitted and
** the process is launched as root, it will abort without
** processing any HTTP requests.
**
** --logfile FILE Append a single-line, CSV-format, log file entry to FILE
** for each HTTP request. FILE should be a full pathname.
** The FILE name is interpreted inside the chroot jail. The
** FILE name is expanded using strftime() if it contains
** at least one '%' and is not too long.
**
** --https Indicates that input is coming over SSL and is being
** decoded upstream, perhaps by stunnel. (This program
** only understands plaintext.)
**
** --family ipv4 Only accept input from IPV4 or IPV6, respectively.
** --family ipv6 These options are only meaningful if althttpd is run
** as a stand-alone server.
**
** --jail BOOLEAN Indicates whether or not to form a chroot jail if
** initially run as root. The default is true, so the only
** useful variant of this option is "--jail 0" which prevents
** the formation of the chroot jail.
**
** --max-age SEC The value for "Cache-Control: max-age=%d". Defaults to
** 120 seconds.
**
** --debug Disables input timeouts. This is useful for debugging
** when inputs is being typed in manually.
**
** Command-line options can take either one or two initial "-" characters.
** So "--debug" and "-debug" mean the same thing, for example.
**
**
** Security Features:
**
** (1) This program automatically puts itself inside a chroot jail if
** it can and if not specifically prohibited by the "--jail 0"
** command-line option. The root of the jail is the directory that
** contains the various $HOST.website content subdirectories.
**
** (2) No input is read while this process has root privileges. Root
** privileges are dropped prior to reading any input (but after entering
** the chroot jail, of course). If root privileges cannot be dropped
** (for example because the --user command-line option was omitted or
** because the user specified by the --user option does not exist),
** then the process aborts with an error prior to reading any input.
**
** (3) The length of an HTTP request is limited to MAX_CONTENT_LENGTH bytes
** (default: 250 million). Any HTTP request longer than this fails
** with an error.
**
** (4) There are hard-coded time-outs on each HTTP request. If this process
** waits longer than the timeout for the complete request, or for CGI
** to finish running, then this process aborts. (The timeout feature
** can be disabled using the --debug command-line option.)
**
** (5) If the HTTP_HOST request header contains characters other than
** [0-9a-zA-Z,-./:_~] then the entire request is rejected.
**
** (6) Any characters in the URI pathname other than [0-9a-zA-Z,-./:_~]
** are converted into "_". This applies to the pathname only, not
** to the query parameters or fragment.
**
** (7) If the first character of any URI pathname component is "." or "-"
** then a 404 Not Found reply is generated. This prevents attacks
** such as including ".." or "." directory elements in the pathname
** and allows placing files and directories in the content subdirectory
** that are invisible to all HTTP requests, by making the first
** character of the file or subdirectory name "-" or ".".
**
** (8) The request URI must begin with "/" or else a 404 error is generated.
**
** (9) This program never sets the value of an environment variable to a
** string that begins with "() {".
**
** Security Auditing:
**
** This webserver mostly only serves static content. Any security risk will
** come from CGI and SCGI. To check an installation for security, then, it
** makes sense to focus on the CGI and SCGI scripts.
**
** To local all CGI files:
**
** find *.website -executable -type f -print
** OR: find *.website -perm +0111 -type f -print
**
** The first form of the "find" command is preferred, but is only supported
** by GNU find. On a Mac, you'll have to use the second form.
**
** To find all SCGI files:
**
** find *.website -name '*.scgi' -type f -print
**
** If any file is a security concern, it can be disabled on a live
** installation by turning off read permissions:
**
** chmod 0000 file-of-concern
**
** SCGI Specification Files:
**
** Content files (files without the execute bit set) that end with ".scgi"
** specify a connection to an SCGI server. The format of the .scgi file
** follows this template:
**
** SCGI hostname port
** fallback: fallback-filename
** relight: relight-command
**
** The first line specifies the location and TCP/IP port of the SCGI server
** that will handle the request. Subsequent lines determine what to do if
** the SCGI server cannot be contacted. If the "relight:" line is present,
** then the relight-command is run using system() and the connection is
** retried after a 1-second delay. Use "&" at the end of the relight-command
** to run it in the background. Make sure the relight-command does not
** send generate output, or that output will become part of the SCGI reply.
** Add a ">/dev/null" suffix (before the "&") to the relight-command if
** necessary to suppress output. If there is no relight-command, or if the
** relight is attempted but the SCGI server still cannot be contacted, then
** the content of the fallback-filename file is returned as a substitute for
** the SCGI request. The mimetype is determined by the suffix on the
** fallback-filename. The fallback-filename would typically be an error
** message indicating that the service is temporarily unavailable.
**
** Basic Authorization:
**
** If the file "-auth" exists in the same directory as the content file
** (for both static content and CGI) then it contains the information used
** for basic authorization. The file format is as follows:
**
** * Blank lines and lines that begin with '#' are ignored
** * "http-redirect" forces a redirect to HTTPS if not there already
** * "https-only" disallows operation in HTTP
** * "user NAME LOGIN:PASSWORD" checks to see if LOGIN:PASSWORD
** authorization credentials are provided, and if so sets the
** REMOTE_USER to NAME.
** * "realm TEXT" sets the realm to TEXT.
**
** There can be multiple "user" lines. If no "user" line matches, the
** request fails with a 401 error.
**
** Because of security rule (7), there is no way for the content of the "-auth"
** file to leak out via HTTP request.
*/
#include <stdio.h>
#include <ctype.h>
#include <syslog.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pwd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdarg.h>
#include <time.h>
#include <sys/times.h>
#include <netdb.h>
#include <errno.h>
#include <sys/resource.h>
#include <signal.h>
#ifdef linux
#include <sys/sendfile.h>
#endif
#include <assert.h>
/*
** Configure the server by setting the following macros and recompiling.
*/
#ifndef DEFAULT_PORT
#define DEFAULT_PORT "80" /* Default TCP port for HTTP */
#endif
#ifndef MAX_CONTENT_LENGTH
#define MAX_CONTENT_LENGTH 250000000 /* Max length of HTTP request content */
#endif
/*
** We record most of the state information as global variables. This
** saves having to pass information to subroutines as parameters, and
** makes the executable smaller...
*/
static char *zRoot = 0; /* Root directory of the website */
static char *zTmpNam = 0; /* Name of a temporary file */
static char zTmpNamBuf[500]; /* Space to hold the temporary filename */
static char *zProtocol = 0; /* The protocol being using by the browser */
static char *zMethod = 0; /* The method. Must be GET */
static char *zScript = 0; /* The object to retrieve */
static char *zRealScript = 0; /* The object to retrieve. Same as zScript
** except might have "/index.html" appended */
static char *zHome = 0; /* The directory containing content */
static char *zQueryString = 0; /* The query string on the end of the name */
static char *zFile = 0; /* The filename of the object to retrieve */
static int lenFile = 0; /* Length of the zFile name */
static char *zDir = 0; /* Name of the directory holding zFile */
static char *zPathInfo = 0; /* Part of the pathname past the file */
static char *zAgent = 0; /* What type if browser is making this query */
static char *zServerName = 0; /* The name after the http:// */
static char *zServerPort = 0; /* The port number */
static char *zCookie = 0; /* Cookies reported with the request */
static char *zHttpHost = 0; /* Name according to the web browser */
static char *zRealPort = 0; /* The real TCP port when running as daemon */
static char *zRemoteAddr = 0; /* IP address of the request */
static char *zReferer = 0; /* Name of the page that refered to us */
static char *zAccept = 0; /* What formats will be accepted */
static char *zAcceptEncoding =0; /* gzip or default */
static char *zContentLength = 0; /* Content length reported in the header */
static char *zContentType = 0; /* Content type reported in the header */
static char *zQuerySuffix = 0; /* The part of the URL after the first ? */
static char *zAuthType = 0; /* Authorization type (basic or digest) */
static char *zAuthArg = 0; /* Authorization values */
static char *zRemoteUser = 0; /* REMOTE_USER set by authorization module */
static char *zIfNoneMatch= 0; /* The If-None-Match header value */
static char *zIfModifiedSince=0; /* The If-Modified-Since header value */
static int nIn = 0; /* Number of bytes of input */
static int nOut = 0; /* Number of bytes of output */
static char zReplyStatus[4]; /* Reply status code */
static int statusSent = 0; /* True after status line is sent */
static char *zLogFile = 0; /* Log to this file */
static int debugFlag = 0; /* True if being debugged */
static struct timeval beginTime; /* Time when this process starts */
static int closeConnection = 0; /* True to send Connection: close in reply */
static int nRequest = 0; /* Number of requests processed */
static int omitLog = 0; /* Do not make logfile entries if true */
static int useHttps = 0; /* True to use HTTPS: instead of HTTP: */
static char *zHttp = "http"; /* http or https */
static int useTimeout = 1; /* True to use times */
static int standalone = 0; /* Run as a standalone server (no inetd) */
static int ipv6Only = 0; /* Use IPv6 only */
static int ipv4Only = 0; /* Use IPv4 only */
static struct rusage priorSelf; /* Previously report SELF time */
static struct rusage priorChild; /* Previously report CHILD time */
static int mxAge = 120; /* Cache-control max-age */
static char *default_path = "/bin:/usr/bin"; /* Default PATH variable */
static char *zScgi = 0; /* Value of the SCGI env variable */
static int rangeStart = 0; /* Start of a Range: request */
static int rangeEnd = 0; /* End of a Range: request */
/*
** Mapping between CGI variable names and values stored in
** global variables.
*/
static struct {
char *zEnvName;
char **pzEnvValue;
} cgienv[] = {
{ "CONTENT_LENGTH", &zContentLength }, /* Must be first for SCGI */
{ "AUTH_TYPE", &zAuthType },
{ "AUTH_CONTENT", &zAuthArg },
{ "CONTENT_TYPE", &zContentType },
{ "DOCUMENT_ROOT", &zHome },
{ "HTTP_ACCEPT", &zAccept },
{ "HTTP_ACCEPT_ENCODING", &zAcceptEncoding },
{ "HTTP_COOKIE", &zCookie },
{ "HTTP_HOST", &zHttpHost },
{ "HTTP_IF_MODIFIED_SINCE", &zIfModifiedSince },
{ "HTTP_IF_NONE_MATCH", &zIfNoneMatch },
{ "HTTP_REFERER", &zReferer },
{ "HTTP_USER_AGENT", &zAgent },
{ "PATH", &default_path },
{ "PATH_INFO", &zPathInfo },
{ "QUERY_STRING", &zQueryString },
{ "REMOTE_ADDR", &zRemoteAddr },
{ "REQUEST_METHOD", &zMethod },
{ "REQUEST_URI", &zScript },
{ "REMOTE_USER", &zRemoteUser },
{ "SCGI", &zScgi },
{ "SCRIPT_DIRECTORY", &zDir },
{ "SCRIPT_FILENAME", &zFile },
{ "SCRIPT_NAME", &zRealScript },
{ "SERVER_NAME", &zServerName },
{ "SERVER_PORT", &zServerPort },
{ "SERVER_PROTOCOL", &zProtocol },
};
/*
** Double any double-quote characters in a string.
*/
static char *Escape(char *z){
size_t i, j;
size_t n;
char c;
char *zOut;
for(i=0; (c=z[i])!=0 && c!='"'; i++){}
if( c==0 ) return z;
n = 1;
for(i++; (c=z[i])!=0; i++){ if( c=='"' ) n++; }
zOut = malloc( i+n+1 );
if( zOut==0 ) return "";
for(i=j=0; (c=z[i])!=0; i++){
zOut[j++] = c;
if( c=='"' ) zOut[j++] = c;
}
zOut[j] = 0;
return zOut;
}
/*
** Convert a struct timeval into an integer number of microseconds
*/
static long long int tvms(struct timeval *p){
return ((long long int)p->tv_sec)*1000000 + (long long int)p->tv_usec;
}
/*
** Make an entry in the log file. If the HTTP connection should be
** closed, then terminate this process. Otherwise return.
*/
static void MakeLogEntry(int exitCode, int lineNum){
FILE *log;
if( zTmpNam ){
unlink(zTmpNam);
}
if( zLogFile && !omitLog ){
struct timeval now;
struct tm *pTm;
struct rusage self, children;
int waitStatus;
char *zRM = zRemoteUser ? zRemoteUser : "";
char *zFilename;
size_t sz;
char zDate[200];
char zExpLogFile[500];
if( zScript==0 ) zScript = "";
if( zRealScript==0 ) zRealScript = "";
if( zRemoteAddr==0 ) zRemoteAddr = "";
if( zHttpHost==0 ) zHttpHost = "";
if( zReferer==0 ) zReferer = "";
if( zAgent==0 ) zAgent = "";
gettimeofday(&now, 0);
pTm = localtime(&now.tv_sec);
strftime(zDate, sizeof(zDate), "%Y-%m-%d %H:%M:%S", pTm);
sz = strftime(zExpLogFile, sizeof(zExpLogFile), zLogFile, pTm);
if( sz>0 && sz<sizeof(zExpLogFile)-2 ){
zFilename = zExpLogFile;
}else{
zFilename = zLogFile;
}
waitpid(-1, &waitStatus, WNOHANG);
getrusage(RUSAGE_SELF, &self);
getrusage(RUSAGE_CHILDREN, &children);
if( (log = fopen(zFilename,"a"))!=0 ){
#ifdef COMBINED_LOG_FORMAT
strftime(zDate, sizeof(zDate), "%d/%b/%Y:%H:%M:%S %z", pTm);
fprintf(log, "%s - - [%s] \"%s %s %s\" %s %d \"%s\" \"%s\"\n",
zRemoteAddr, zDate, zMethod, zScript, zProtocol,
zReplyStatus, nOut, zReferer, zAgent);
#else
strftime(zDate, sizeof(zDate), "%Y-%m-%d %H:%M:%S", pTm);
/* Log record files:
** (1) Date and time
** (2) IP address
** (3) URL being accessed
** (4) Referer
** (5) Reply status
** (6) Bytes received
** (7) Bytes sent
** (8) Self user time
** (9) Self system time
** (10) Children user time
** (11) Children system time
** (12) Total wall-clock time
** (13) Request number for same TCP/IP connection
** (14) User agent
** (15) Remote user
** (16) Bytes of URL that correspond to the SCRIPT_NAME
** (17) Line number in source file
*/
fprintf(log,
"%s,%s,\"%s://%s%s\",\"%s\","
"%s,%d,%d,%lld,%lld,%lld,%lld,%lld,%d,\"%s\",\"%s\",%d,%d\n",
zDate, zRemoteAddr, zHttp, Escape(zHttpHost), Escape(zScript),
Escape(zReferer), zReplyStatus, nIn, nOut,
tvms(&self.ru_utime) - tvms(&priorSelf.ru_utime),
tvms(&self.ru_stime) - tvms(&priorSelf.ru_stime),
tvms(&children.ru_utime) - tvms(&priorChild.ru_utime),
tvms(&children.ru_stime) - tvms(&priorChild.ru_stime),
tvms(&now) - tvms(&beginTime),
nRequest, Escape(zAgent), Escape(zRM),
(int)(strlen(zHttp)+strlen(zHttpHost)+strlen(zRealScript)+3),
lineNum
);
priorSelf = self;
priorChild = children;
#endif
fclose(log);
nIn = nOut = 0;
}
}
if( closeConnection ){
exit(exitCode);
}
statusSent = 0;
}
/*
** Allocate memory safely
*/
static char *SafeMalloc( size_t size ){
char *p;
p = (char*)malloc(size);
if( p==0 ){
strcpy(zReplyStatus, "998");
MakeLogEntry(1,100); /* LOG: Malloc() failed */
exit(1);
}
return p;
}
/*
** Set the value of environment variable zVar to zValue.
*/
static void SetEnv(const char *zVar, const char *zValue){
char *z;
size_t len;
if( zValue==0 ) zValue="";
/* Disable an attempted bashdoor attack */
if( strncmp(zValue,"() {",4)==0 ) zValue = "";
len = strlen(zVar) + strlen(zValue) + 2;
z = SafeMalloc(len);
sprintf(z,"%s=%s",zVar,zValue);
putenv(z);
}
/*
** Remove the first space-delimited token from a string and return
** a pointer to it. Add a NULL to the string to terminate the token.
** Make *zLeftOver point to the start of the next token.
*/
static char *GetFirstElement(char *zInput, char **zLeftOver){
char *zResult = 0;
if( zInput==0 ){
if( zLeftOver ) *zLeftOver = 0;
return 0;
}
while( isspace(*(unsigned char*)zInput) ){ zInput++; }
zResult = zInput;
while( *zInput && !isspace(*(unsigned char*)zInput) ){ zInput++; }
if( *zInput ){
*zInput = 0;
zInput++;
while( isspace(*(unsigned char*)zInput) ){ zInput++; }
}
if( zLeftOver ){ *zLeftOver = zInput; }
return zResult;
}
/*
** Make a copy of a string into memory obtained from malloc.
*/
static char *StrDup(const char *zSrc){
char *zDest;
size_t size;
if( zSrc==0 ) return 0;
size = strlen(zSrc) + 1;
zDest = (char*)SafeMalloc( size );
strcpy(zDest,zSrc);
return zDest;
}
static char *StrAppend(char *zPrior, const char *zSep, const char *zSrc){
char *zDest;
size_t size;
size_t n0, n1, n2;
if( zSrc==0 ) return 0;
if( zPrior==0 ) return StrDup(zSrc);
n0 = strlen(zPrior);
n1 = strlen(zSep);
n2 = strlen(zSrc);
size = n0+n1+n2+1;
zDest = (char*)SafeMalloc( size );
memcpy(zDest, zPrior, n0);
free(zPrior);
memcpy(&zDest[n0],zSep,n1);
memcpy(&zDest[n0+n1],zSrc,n2+1);
return zDest;
}
/*
** Compare two ETag values. Return 0 if they match and non-zero if they differ.
**
** The one on the left might be a NULL pointer and it might be quoted.
*/
static int CompareEtags(const char *zA, const char *zB){
if( zA==0 ) return 1;
if( zA[0]=='"' ){
int lenB = (int)strlen(zB);
if( strncmp(zA+1, zB, lenB)==0 && zA[lenB+1]=='"' ) return 0;
}
return strcmp(zA, zB);
}
/*
** Break a line at the first \n or \r character seen.
*/
static void RemoveNewline(char *z){
if( z==0 ) return;
while( *z && *z!='\n' && *z!='\r' ){ z++; }
*z = 0;
}
/* Render seconds since 1970 as an RFC822 date string. Return
** a pointer to that string in a static buffer.
*/
static char *Rfc822Date(time_t t){
struct tm *tm;
static char zDate[100];
tm = gmtime(&t);
strftime(zDate, sizeof(zDate), "%a, %d %b %Y %H:%M:%S %z", tm);
return zDate;
}
/*
** Print a date tag in the header. The name of the tag is zTag.
** The date is determined from the unix timestamp given.
*/
static int DateTag(const char *zTag, time_t t){
return printf("%s: %s\r\n", zTag, Rfc822Date(t));
}
/*
** Parse an RFC822-formatted timestamp as we'd expect from HTTP and return
** a Unix epoch time. <= zero is returned on failure.
*/
time_t ParseRfc822Date(const char *zDate){
int mday, mon, year, yday, hour, min, sec;
char zIgnore[4];
char zMonth[4];
static const char *const azMonths[] =
{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 0};
if( 7==sscanf(zDate, "%3[A-Za-z], %d %3[A-Za-z] %d %d:%d:%d", zIgnore,
&mday, zMonth, &year, &hour, &min, &sec)){
if( year > 1900 ) year -= 1900;
for(mon=0; azMonths[mon]; mon++){
if( !strncmp( azMonths[mon], zMonth, 3 )){
int nDay;
int isLeapYr;
static int priorDays[] =
{ 0, 31, 59, 90,120,151,181,212,243,273,304,334 };
if( mon<0 ){
int nYear = (11 - mon)/12;
year -= nYear;
mon += nYear*12;
}else if( mon>11 ){
year += mon/12;
mon %= 12;
}
isLeapYr = year%4==0 && (year%100!=0 || (year+300)%400==0);
yday = priorDays[mon] + mday - 1;
if( isLeapYr && mon>1 ) yday++;
nDay = (year-70)*365 + (year-69)/4 - year/100 + (year+300)/400 + yday;
return ((time_t)(nDay*24 + hour)*60 + min)*60 + sec;
}
}
}
return 0;
}
/*
** Test procedure for ParseRfc822Date
*/
void TestParseRfc822Date(void){
time_t t1, t2;
for(t1=0; t1<0x7fffffff; t1 += 127){
t2 = ParseRfc822Date(Rfc822Date(t1));
assert( t1==t2 );
}
}
/*
** Print the first line of a response followed by the server type.
*/
static void StartResponse(const char *zResultCode){
time_t now;
time(&now);
if( statusSent ) return;
nOut += printf("%s %s\r\n", zProtocol, zResultCode);
strncpy(zReplyStatus, zResultCode, 3);
zReplyStatus[3] = 0;
if( zReplyStatus[0]>='4' ){
closeConnection = 1;
}
if( closeConnection ){
nOut += printf("Connection: close\r\n");
}else{
nOut += printf("Connection: keep-alive\r\n");
}
nOut += DateTag("Date", now);
statusSent = 1;
}
/*
** Tell the client that there is no such document
*/
static void NotFound(int lineno){
StartResponse("404 Not Found");
nOut += printf(
"Content-type: text/html\r\n"
"\r\n"
"<head><title lineno=\"%d\">Not Found</title></head>\n"
"<body><h1>Document Not Found</h1>\n"
"The document %s is not available on this server\n"
"</body>\n", lineno, zScript);
MakeLogEntry(0, lineno);
exit(0);
}
/*
** Tell the client that they are not welcomed here.
*/
static void Forbidden(int lineno){
StartResponse("403 Forbidden");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"Access denied\n"
);
closeConnection = 1;
MakeLogEntry(0, lineno);
exit(0);
}
/*
** Tell the client that authorization is required to access the
** document.
*/
static void NotAuthorized(const char *zRealm){
StartResponse("401 Authorization Required");
nOut += printf(
"WWW-Authenticate: Basic realm=\"%s\"\r\n"
"Content-type: text/html\r\n"
"\r\n"
"<head><title>Not Authorized</title></head>\n"
"<body><h1>401 Not Authorized</h1>\n"
"A login and password are required for this document\n"
"</body>\n", zRealm);
MakeLogEntry(0, 110); /* LOG: Not authorized */
}
/*
** Tell the client that there is an error in the script.
*/
static void CgiError(void){
StartResponse("500 Error");
nOut += printf(
"Content-type: text/html\r\n"
"\r\n"
"<head><title>CGI Program Error</title></head>\n"
"<body><h1>CGI Program Error</h1>\n"
"The CGI program %s generated an error\n"
"</body>\n", zScript);
MakeLogEntry(0, 120); /* LOG: CGI Error */
exit(0);
}
/*
** This is called if we timeout or catch some other kind of signal.
** Log an error code which is 900+iSig and then quit.
*/
static void Timeout(int iSig){
if( !debugFlag ){
if( zScript && zScript[0] ){
char zBuf[10];
zBuf[0] = '9';
zBuf[1] = '0' + (iSig/10)%10;
zBuf[2] = '0' + iSig%10;
zBuf[3] = 0;
strcpy(zReplyStatus, zBuf);
MakeLogEntry(0, 130); /* LOG: Timeout */
}
exit(0);
}
}
/*
** Tell the client that there is an error in the script.
*/
static void CgiScriptWritable(void){
StartResponse("500 CGI Configuration Error");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"The CGI program %s is writable by users other than its owner.\n",
zRealScript);
MakeLogEntry(0, 140); /* LOG: CGI script is writable */
exit(0);
}
/*
** Tell the client that the server malfunctioned.
*/
static void Malfunction(int linenum, const char *zFormat, ...){
va_list ap;
va_start(ap, zFormat);
StartResponse("500 Server Malfunction");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"Web server malfunctioned; error number %d\n\n", linenum);
if( zFormat ){
nOut += vprintf(zFormat, ap);
printf("\n");
nOut++;
}
MakeLogEntry(0, linenum);
exit(0);
}
/*
** Do a server redirect to the document specified. The document
** name not contain scheme or network location or the query string.
** It will be just the path.
*/
static void Redirect(const char *zPath, int iStatus, int finish, int lineno){
switch( iStatus ){
case 301:
StartResponse("301 Permanent Redirect");
break;
case 308:
StartResponse("308 Permanent Redirect");
break;
default:
StartResponse("302 Temporary Redirect");
break;
}
StartResponse("302 Temporary Redirect");
if( zServerPort==0 || zServerPort[0]==0 || strcmp(zServerPort,"80")==0 ){
nOut += printf("Location: %s://%s%s%s\r\n",
zHttp, zServerName, zPath, zQuerySuffix);
}else{
nOut += printf("Location: %s://%s:%s%s%s\r\n",
zHttp, zServerName, zServerPort, zPath, zQuerySuffix);
}
if( finish ){
nOut += printf("Content-length: 0\r\n");
nOut += printf("\r\n");
MakeLogEntry(0, lineno);
}
fflush(stdout);
}
/*
** This function treats its input as a base-64 string and returns the
** decoded value of that string. Characters of input that are not
** valid base-64 characters (such as spaces and newlines) are ignored.
*/
void Decode64(char *z64){
char *zData;
int n64;
int i, j;
int a, b, c, d;
static int isInit = 0;
static int trans[128];
static unsigned char zBase[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
if( !isInit ){
for(i=0; i<128; i++){ trans[i] = 0; }
for(i=0; zBase[i]; i++){ trans[zBase[i] & 0x7f] = i; }
isInit = 1;
}
n64 = strlen(z64);
while( n64>0 && z64[n64-1]=='=' ) n64--;
zData = z64;
for(i=j=0; i+3<n64; i+=4){
a = trans[z64[i] & 0x7f];
b = trans[z64[i+1] & 0x7f];
c = trans[z64[i+2] & 0x7f];
d = trans[z64[i+3] & 0x7f];
zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
zData[j++] = ((c<<6) & 0xc0) | (d & 0x3f);
}
if( i+2<n64 ){
a = trans[z64[i] & 0x7f];
b = trans[z64[i+1] & 0x7f];
c = trans[z64[i+2] & 0x7f];
zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
}else if( i+1<n64 ){
a = trans[z64[i] & 0x7f];
b = trans[z64[i+1] & 0x7f];
zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
}
zData[j] = 0;
}
/*
** Check to see if basic authorization credentials are provided for
** the user according to the information in zAuthFile. Return true
** if authorized. Return false if not authorized.
**
** File format:
**
** * Blank lines and lines that begin with '#' are ignored
** * "http-redirect" forces a redirect to HTTPS if not there already
** * "https-only" disallows operation in HTTP
** * "user NAME LOGIN:PASSWORD" checks to see if LOGIN:PASSWORD
** authorization credentials are provided, and if so sets the
** REMOTE_USER to NAME.
** * "realm TEXT" sets the realm to TEXT.
** * "anyone" bypasses authentication and allows anyone to see the
** files. Useful in combination with "http-redirect"
*/
static int CheckBasicAuthorization(const char *zAuthFile){
FILE *in;
char *zRealm = "unknown realm";
char *zLoginPswd;
char *zName;
char zLine[2000];
in = fopen(zAuthFile, "rb");
if( in==0 ){
NotFound(150); /* LOG: Cannot open -auth file */
return 0;
}
if( zAuthArg ) Decode64(zAuthArg);
while( fgets(zLine, sizeof(zLine), in) ){
char *zFieldName;
char *zVal;
zFieldName = GetFirstElement(zLine,&zVal);
if( zFieldName==0 || *zFieldName==0 ) continue;
if( zFieldName[0]=='#' ) continue;
RemoveNewline(zVal);
if( strcmp(zFieldName, "realm")==0 ){
zRealm = StrDup(zVal);
}else if( strcmp(zFieldName,"user")==0 ){
if( zAuthArg==0 ) continue;
zName = GetFirstElement(zVal, &zVal);
zLoginPswd = GetFirstElement(zVal, &zVal);
if( zLoginPswd==0 ) continue;
if( zAuthArg && strcmp(zAuthArg,zLoginPswd)==0 ){
zRemoteUser = StrDup(zName);
fclose(in);
return 1;
}
}else if( strcmp(zFieldName,"https-only")==0 ){
if( !useHttps ){
NotFound(160); /* LOG: http request on https-only page */
fclose(in);
return 0;
}
}else if( strcmp(zFieldName,"http-redirect")==0 ){
if( !useHttps ){
zHttp = "https";
Redirect(zScript, 301, 1, 170); /* LOG: -auth redirect */
fclose(in);
return 0;
}
}else if( strcmp(zFieldName,"anyone")==0 ){
fclose(in);
return 1;
}else{
NotFound(180); /* LOG: malformed entry in -auth file */
fclose(in);
return 0;
}
}
fclose(in);
NotAuthorized(zRealm);
return 0;
}
/*
** Guess the mime-type of a document based on its name.
*/
const char *GetMimeType(const char *zName, int nName){
const char *z;
int i;
int first, last;
int len;
char zSuffix[20];
/* A table of mimetypes based on file suffixes.
** Suffixes must be in sorted order so that we can do a binary
** search to find the mime-type
*/
static const struct {
const char *zSuffix; /* The file suffix */
int size; /* Length of the suffix */
const char *zMimetype; /* The corresponding mimetype */
} aMime[] = {
{ "ai", 2, "application/postscript" },
{ "aif", 3, "audio/x-aiff" },
{ "aifc", 4, "audio/x-aiff" },
{ "aiff", 4, "audio/x-aiff" },
{ "arj", 3, "application/x-arj-compressed" },
{ "asc", 3, "text/plain" },
{ "asf", 3, "video/x-ms-asf" },
{ "asx", 3, "video/x-ms-asx" },
{ "au", 2, "audio/ulaw" },
{ "avi", 3, "video/x-msvideo" },
{ "bat", 3, "application/x-msdos-program" },
{ "bcpio", 5, "application/x-bcpio" },
{ "bin", 3, "application/octet-stream" },
{ "c", 1, "text/plain" },
{ "cc", 2, "text/plain" },
{ "ccad", 4, "application/clariscad" },
{ "cdf", 3, "application/x-netcdf" },
{ "class", 5, "application/octet-stream" },
{ "cod", 3, "application/vnd.rim.cod" },
{ "com", 3, "application/x-msdos-program" },
{ "cpio", 4, "application/x-cpio" },
{ "cpt", 3, "application/mac-compactpro" },
{ "csh", 3, "application/x-csh" },
{ "css", 3, "text/css" },
{ "dcr", 3, "application/x-director" },
{ "deb", 3, "application/x-debian-package" },
{ "dir", 3, "application/x-director" },
{ "dl", 2, "video/dl" },
{ "dms", 3, "application/octet-stream" },
{ "doc", 3, "application/msword" },
{ "drw", 3, "application/drafting" },
{ "dvi", 3, "application/x-dvi" },
{ "dwg", 3, "application/acad" },
{ "dxf", 3, "application/dxf" },
{ "dxr", 3, "application/x-director" },
{ "eps", 3, "application/postscript" },
{ "etx", 3, "text/x-setext" },
{ "exe", 3, "application/octet-stream" },
{ "ez", 2, "application/andrew-inset" },
{ "f", 1, "text/plain" },
{ "f90", 3, "text/plain" },
{ "fli", 3, "video/fli" },
{ "flv", 3, "video/flv" },
{ "gif", 3, "image/gif" },
{ "gl", 2, "video/gl" },
{ "gtar", 4, "application/x-gtar" },
{ "gz", 2, "application/x-gzip" },
{ "hdf", 3, "application/x-hdf" },
{ "hh", 2, "text/plain" },
{ "hqx", 3, "application/mac-binhex40" },
{ "h", 1, "text/plain" },
{ "htm", 3, "text/html; charset=utf-8" },
{ "html", 4, "text/html; charset=utf-8" },
{ "ice", 3, "x-conference/x-cooltalk" },
{ "ief", 3, "image/ief" },
{ "iges", 4, "model/iges" },
{ "igs", 3, "model/iges" },
{ "ips", 3, "application/x-ipscript" },
{ "ipx", 3, "application/x-ipix" },
{ "jad", 3, "text/vnd.sun.j2me.app-descriptor" },
{ "jar", 3, "application/java-archive" },
{ "jpeg", 4, "image/jpeg" },
{ "jpe", 3, "image/jpeg" },
{ "jpg", 3, "image/jpeg" },
{ "js", 2, "application/x-javascript" },
{ "kar", 3, "audio/midi" },
{ "latex", 5, "application/x-latex" },
{ "lha", 3, "application/octet-stream" },
{ "lsp", 3, "application/x-lisp" },
{ "lzh", 3, "application/octet-stream" },
{ "m", 1, "text/plain" },
{ "m3u", 3, "audio/x-mpegurl" },
{ "man", 3, "application/x-troff-man" },
{ "me", 2, "application/x-troff-me" },
{ "mesh", 4, "model/mesh" },
{ "mid", 3, "audio/midi" },
{ "midi", 4, "audio/midi" },
{ "mif", 3, "application/x-mif" },
{ "mime", 4, "www/mime" },
{ "movie", 5, "video/x-sgi-movie" },
{ "mov", 3, "video/quicktime" },
{ "mp2", 3, "audio/mpeg" },
{ "mp2", 3, "video/mpeg" },
{ "mp3", 3, "audio/mpeg" },
{ "mpeg", 4, "video/mpeg" },
{ "mpe", 3, "video/mpeg" },
{ "mpga", 4, "audio/mpeg" },
{ "mpg", 3, "video/mpeg" },
{ "ms", 2, "application/x-troff-ms" },
{ "msh", 3, "model/mesh" },
{ "nc", 2, "application/x-netcdf" },
{ "oda", 3, "application/oda" },
{ "ogg", 3, "application/ogg" },
{ "ogm", 3, "application/ogg" },
{ "pbm", 3, "image/x-portable-bitmap" },
{ "pdb", 3, "chemical/x-pdb" },
{ "pdf", 3, "application/pdf" },
{ "pgm", 3, "image/x-portable-graymap" },
{ "pgn", 3, "application/x-chess-pgn" },
{ "pgp", 3, "application/pgp" },
{ "pl", 2, "application/x-perl" },
{ "pm", 2, "application/x-perl" },
{ "png", 3, "image/png" },
{ "pnm", 3, "image/x-portable-anymap" },
{ "pot", 3, "application/mspowerpoint" },
{ "ppm", 3, "image/x-portable-pixmap" },
{ "pps", 3, "application/mspowerpoint" },
{ "ppt", 3, "application/mspowerpoint" },
{ "ppz", 3, "application/mspowerpoint" },
{ "pre", 3, "application/x-freelance" },
{ "prt", 3, "application/pro_eng" },
{ "ps", 2, "application/postscript" },
{ "qt", 2, "video/quicktime" },
{ "ra", 2, "audio/x-realaudio" },
{ "ram", 3, "audio/x-pn-realaudio" },
{ "rar", 3, "application/x-rar-compressed" },
{ "ras", 3, "image/cmu-raster" },
{ "ras", 3, "image/x-cmu-raster" },
{ "rgb", 3, "image/x-rgb" },
{ "rm", 2, "audio/x-pn-realaudio" },
{ "roff", 4, "application/x-troff" },
{ "rpm", 3, "audio/x-pn-realaudio-plugin" },
{ "rtf", 3, "application/rtf" },
{ "rtf", 3, "text/rtf" },
{ "rtx", 3, "text/richtext" },
{ "scm", 3, "application/x-lotusscreencam" },
{ "set", 3, "application/set" },
{ "sgml", 4, "text/sgml" },
{ "sgm", 3, "text/sgml" },
{ "sh", 2, "application/x-sh" },
{ "shar", 4, "application/x-shar" },
{ "silo", 4, "model/mesh" },
{ "sit", 3, "application/x-stuffit" },
{ "skd", 3, "application/x-koan" },
{ "skm", 3, "application/x-koan" },
{ "skp", 3, "application/x-koan" },
{ "skt", 3, "application/x-koan" },
{ "smi", 3, "application/smil" },
{ "smil", 4, "application/smil" },
{ "snd", 3, "audio/basic" },
{ "sol", 3, "application/solids" },
{ "spl", 3, "application/x-futuresplash" },
{ "src", 3, "application/x-wais-source" },
{ "step", 4, "application/STEP" },
{ "stl", 3, "application/SLA" },
{ "stp", 3, "application/STEP" },
{ "sv4cpio", 7, "application/x-sv4cpio" },
{ "sv4crc", 6, "application/x-sv4crc" },
{ "svg", 3, "image/svg+xml" },
{ "swf", 3, "application/x-shockwave-flash" },
{ "t", 1, "application/x-troff" },
{ "tar", 3, "application/x-tar" },
{ "tcl", 3, "application/x-tcl" },
{ "tex", 3, "application/x-tex" },
{ "texi", 4, "application/x-texinfo" },
{ "texinfo", 7, "application/x-texinfo" },
{ "tgz", 3, "application/x-tar-gz" },
{ "tiff", 4, "image/tiff" },
{ "tif", 3, "image/tiff" },
{ "tr", 2, "application/x-troff" },
{ "tsi", 3, "audio/TSP-audio" },
{ "tsp", 3, "application/dsptype" },
{ "tsv", 3, "text/tab-separated-values" },
{ "txt", 3, "text/plain" },
{ "unv", 3, "application/i-deas" },
{ "ustar", 5, "application/x-ustar" },
{ "vcd", 3, "application/x-cdlink" },
{ "vda", 3, "application/vda" },
{ "viv", 3, "video/vnd.vivo" },
{ "vivo", 4, "video/vnd.vivo" },
{ "vrml", 4, "model/vrml" },
{ "vsix", 4, "application/vsix" },
{ "wav", 3, "audio/x-wav" },
{ "wax", 3, "audio/x-ms-wax" },
{ "wiki", 4, "application/x-fossil-wiki" },
{ "wma", 3, "audio/x-ms-wma" },
{ "wmv", 3, "video/x-ms-wmv" },
{ "wmx", 3, "video/x-ms-wmx" },
{ "wrl", 3, "model/vrml" },
{ "wvx", 3, "video/x-ms-wvx" },
{ "xbm", 3, "image/x-xbitmap" },
{ "xlc", 3, "application/vnd.ms-excel" },
{ "xll", 3, "application/vnd.ms-excel" },
{ "xlm", 3, "application/vnd.ms-excel" },
{ "xls", 3, "application/vnd.ms-excel" },
{ "xlw", 3, "application/vnd.ms-excel" },
{ "xml", 3, "text/xml" },
{ "xpm", 3, "image/x-xpixmap" },
{ "xwd", 3, "image/x-xwindowdump" },
{ "xyz", 3, "chemical/x-pdb" },
{ "zip", 3, "application/zip" },
};
for(i=nName-1; i>0 && zName[i]!='.'; i--){}
z = &zName[i+1];
len = nName - i;
if( len<(int)sizeof(zSuffix)-1 ){
strcpy(zSuffix, z);
for(i=0; zSuffix[i]; i++) zSuffix[i] = tolower(zSuffix[i]);
first = 0;
last = sizeof(aMime)/sizeof(aMime[0]);
while( first<=last ){
int c;
i = (first+last)/2;
c = strcmp(zSuffix, aMime[i].zSuffix);
if( c==0 ) return aMime[i].zMimetype;
if( c<0 ){
last = i-1;
}else{
first = i+1;
}
}
}
return "application/octet-stream";
}
/*
** The following table contains 1 for all characters that are permitted in
** the part of the URL before the query parameters and fragment.
**
** Allowed characters: 0-9a-zA-Z,-./:_~
**
** Disallowed characters include: !"#$%&'()*+;<=>?[\]^{|}
*/
static const char allowedInName[] = {
/* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xa xb xc xd xe xf */
/* 0x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 1x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 2x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
/* 3x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
/* 4x */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
/* 5x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
/* 6x */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
/* 7x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0,
/* 8x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* 9x */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Ax */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Bx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Cx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Dx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Ex */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* Fx */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
/*
** Remove all disallowed characters in the input string z[]. Convert any
** disallowed characters into "_".
**
** Not that the three character sequence "%XX" where X is any byte is
** converted into a single "_" character.
**
** Return the number of characters converted. An "%XX" -> "_" conversion
** counts as a single character.
*/
static int sanitizeString(char *z){
int nChange = 0;
while( *z ){
if( !allowedInName[*(unsigned char*)z] ){
if( *z=='%' && z[1]!=0 && z[2]!=0 ){
int i;
for(i=3; (z[i-2] = z[i])!=0; i++){}
}
*z = '_';
nChange++;
}
z++;
}
return nChange;
}
/*
** Count the number of "/" characters in a string.
*/
static int countSlashes(const char *z){
int n = 0;
while( *z ) if( *(z++)=='/' ) n++;
return n;
}
/*
** Transfer nXfer bytes from in to out, after first discarding
** nSkip bytes from in. Increment the nOut global variable
** according to the number of bytes transferred.
*/
static void xferBytes(FILE *in, FILE *out, int nXfer, int nSkip){
size_t n;
size_t got;
char zBuf[16384];
while( nSkip>0 ){
n = nSkip;
if( n>sizeof(zBuf) ) n = sizeof(zBuf);
got = fread(zBuf, 1, n, in);
if( got==0 ) break;
nSkip -= got;
}
while( nXfer>0 ){
n = nXfer;
if( n>sizeof(zBuf) ) n = sizeof(zBuf);
got = fread(zBuf, 1, n, in);
if( got==0 ) break;
fwrite(zBuf, got, 1, out);
nOut += got;
nXfer -= got;
}
}
/*
** Send the text of the file named by zFile as the reply. Use the
** suffix on the end of the zFile name to determine the mimetype.
**
** Return 1 to omit making a log entry for the reply.
*/
static int SendFile(
const char *zFile, /* Name of the file to send */
int lenFile, /* Length of the zFile name in bytes */
struct stat *pStat /* Result of a stat() against zFile */
){
const char *zContentType;
time_t t;
FILE *in;
char zETag[100];
zContentType = GetMimeType(zFile, lenFile);
if( zTmpNam ) unlink(zTmpNam);
sprintf(zETag, "m%xs%x", (int)pStat->st_mtime, (int)pStat->st_size);
if( CompareEtags(zIfNoneMatch,zETag)==0
|| (zIfModifiedSince!=0
&& (t = ParseRfc822Date(zIfModifiedSince))>0
&& t>=pStat->st_mtime)
){
StartResponse("304 Not Modified");
nOut += DateTag("Last-Modified", pStat->st_mtime);
nOut += printf("Cache-Control: max-age=%d\r\n", mxAge);
nOut += printf("ETag: \"%s\"\r\n", zETag);
nOut += printf("\r\n");
fflush(stdout);
MakeLogEntry(0, 470); /* LOG: ETag Cache Hit */
return 1;
}
in = fopen(zFile,"rb");
if( in==0 ) NotFound(480); /* LOG: fopen() failed for static content */
if( rangeEnd>0 && rangeStart<pStat->st_size ){
StartResponse("206 Partial Content");
if( rangeEnd>=pStat->st_size ){
rangeEnd = pStat->st_size-1;
}
nOut += printf("Content-Range: bytes %d-%d/%d\r\n",
rangeStart, rangeEnd, (int)pStat->st_size);
pStat->st_size = rangeEnd + 1 - rangeStart;
}else{
StartResponse("200 OK");
rangeStart = 0;
}
nOut += DateTag("Last-Modified", pStat->st_mtime);
nOut += printf("Cache-Control: max-age=%d\r\n", mxAge);
nOut += printf("ETag: \"%s\"\r\n", zETag);
nOut += printf("Content-type: %s\r\n",zContentType);
nOut += printf("Content-length: %d\r\n\r\n",(int)pStat->st_size);
fflush(stdout);
if( strcmp(zMethod,"HEAD")==0 ){
MakeLogEntry(0, 2); /* LOG: Normal HEAD reply */
fclose(in);
fflush(stdout);
return 1;
}
if( useTimeout ) alarm(30 + pStat->st_size/1000);
#ifdef linux
{
off_t offset = rangeStart;
nOut += sendfile(fileno(stdout), fileno(in), &offset, pStat->st_size);
}
#else
xferBytes(in, stdout, (int)pStat->st_size, rangeStart);
#endif
fclose(in);
return 0;
}
/*
** A CGI or SCGI script has run and is sending its reply back across
** the channel "in". Process this reply into an appropriate HTTP reply.
** Close the "in" channel when done.
*/
static void CgiHandleReply(FILE *in){
int seenContentLength = 0; /* True if Content-length: header seen */
int contentLength = 0; /* The content length */
size_t nRes = 0; /* Bytes of payload */
size_t nMalloc = 0; /* Bytes of space allocated to aRes */
char *aRes = 0; /* Payload */
int c; /* Next character from in */
char *z; /* Pointer to something inside of zLine */
int iStatus = 0; /* Reply status code */
char zLine[1000]; /* One line of reply from the CGI script */
if( useTimeout ) alarm(15);
while( fgets(zLine,sizeof(zLine),in) && !isspace((unsigned char)zLine[0]) ){
if( strncasecmp(zLine,"Location:",9)==0 ){
StartResponse("302 Redirect");
RemoveNewline(zLine);
z = &zLine[10];
while( isspace(*(unsigned char*)z) ){ z++; }
nOut += printf("Location: %s\r\n",z);
rangeEnd = 0;
}else if( strncasecmp(zLine,"Status:",7)==0 ){
int i;
for(i=7; isspace((unsigned char)zLine[i]); i++){}
nOut += printf("%s %s", zProtocol, &zLine[i]);
strncpy(zReplyStatus, &zLine[i], 3);
zReplyStatus[3] = 0;
iStatus = atoi(zReplyStatus);
if( iStatus!=200 ) rangeEnd = 0;
statusSent = 1;
}else if( strncasecmp(zLine, "Content-length:", 15)==0 ){
seenContentLength = 1;
contentLength = atoi(zLine+15);
}else{
size_t nLine = strlen(zLine);
if( nRes+nLine >= nMalloc ){
nMalloc += nMalloc + nLine*2;
aRes = realloc(aRes, nMalloc+1);
if( aRes==0 ){
Malfunction(600, "Out of memory: %d bytes", nMalloc);
}
}
memcpy(aRes+nRes, zLine, nLine);
nRes += nLine;
}
}
/* Copy everything else thru without change or analysis.
*/
if( rangeEnd>0 && seenContentLength && rangeStart<contentLength ){
StartResponse("206 Partial Content");
if( rangeEnd>=contentLength ){
rangeEnd = contentLength-1;
}
nOut += printf("Content-Range: bytes %d-%d/%d\r\n",
rangeStart, rangeEnd, contentLength);
contentLength = rangeEnd + 1 - rangeStart;
}else{
StartResponse("200 OK");
}
if( useTimeout ) alarm(60*5);
if( nRes>0 ){
aRes[nRes] = 0;
printf("%s", aRes);
nOut += nRes;
nRes = 0;
}
if( iStatus==304 ){
nOut += printf("\r\n\r\n");
}else if( seenContentLength ){
nOut += printf("Content-length: %d\r\n\r\n", contentLength);
xferBytes(in, stdout, contentLength, rangeStart);
}else{
while( (c = getc(in))!=EOF ){
if( nRes>=nMalloc ){
nMalloc = nMalloc*2 + 1000;
aRes = realloc(aRes, nMalloc+1);
if( aRes==0 ){
Malfunction(610, "Out of memory: %d bytes", nMalloc);
}
}
aRes[nRes++] = c;
}
if( nRes ){
aRes[nRes] = 0;
nOut += printf("Content-length: %d\r\n\r\n%s", (int)nRes, aRes);
}else{
nOut += printf("Content-length: 0\r\n\r\n");
}
}
free(aRes);
fclose(in);
}
/*
** Send an SCGI request to a host identified by zFile and process the
** reply.
*/
static void SendScgiRequest(const char *zFile, const char *zScript){
FILE *in;
FILE *s;
char *z;
char *zHost;
char *zPort = 0;
char *zRelight = 0;
char *zFallback = 0;
int rc;
int iSocket = -1;
struct addrinfo hints;
struct addrinfo *ai = 0;
struct addrinfo *p;
char *zHdr;
size_t nHdr = 0;
size_t nHdrAlloc;
int i;
char zLine[1000];
char zExtra[1000];
in = fopen(zFile, "rb");
if( in==0 ){
Malfunction(700, "cannot open \"%s\"\n", zFile);
}
if( fgets(zLine, sizeof(zLine)-1, in)==0 ){
Malfunction(701, "cannot read \"%s\"\n", zFile);
}
if( strncmp(zLine,"SCGI ",5)!=0 ){
Malfunction(702, "misformatted SCGI spec \"%s\"\n", zFile);
}
z = zLine+5;
zHost = GetFirstElement(z,&z);
zPort = GetFirstElement(z,0);
if( zHost==0 || zHost[0]==0 || zPort==0 || zPort[0]==0 ){
Malfunction(703, "misformatted SCGI spec \"%s\"\n", zFile);
}
while( fgets(zExtra, sizeof(zExtra)-1, in) ){
char *zCmd = GetFirstElement(zExtra,&z);
if( zCmd==0 ) continue;
if( zCmd[0]=='#' ) continue;
RemoveNewline(z);
if( strcmp(zCmd, "relight:")==0 ){
free(zRelight);
zRelight = StrDup(z);
continue;
}
if( strcmp(zCmd, "fallback:")==0 ){
free(zFallback);
zFallback = StrDup(z);
continue;
}
Malfunction(704, "unrecognized line in SCGI spec: \"%s %s\"\n",
zCmd, z ? z : "");
}
fclose(in);
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
rc = getaddrinfo(zHost,zPort,&hints,&ai);
if( rc ){
Malfunction(704, "cannot resolve SCGI server name %s:%s\n%s\n",
zHost, zPort, gai_strerror(rc));
}
while(1){ /* Exit via break */
for(p=ai; p; p=p->ai_next){
iSocket = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if( iSocket<0 ) continue;
if( connect(iSocket,p->ai_addr,p->ai_addrlen)>=0 ) break;
close(iSocket);
}
if( iSocket<0 || (s = fdopen(iSocket,"r+"))==0 ){
if( iSocket>=0 ) close(iSocket);
if( zRelight ){
rc = system(zRelight);
if( rc ){
Malfunction(721,"Relight failed with %d: \"%s\"\n",
rc, zRelight);
}
free(zRelight);
zRelight = 0;
sleep(1);
continue;
}
if( zFallback ){
struct stat statbuf;
int rc;
memset(&statbuf, 0, sizeof(statbuf));
if( chdir(zDir) ){
char zBuf[1000];
Malfunction(720, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zDir, getcwd(zBuf,999));
}
rc = stat(zFallback, &statbuf);
if( rc==0 && S_ISREG(statbuf.st_mode) && access(zFallback,R_OK)==0 ){
closeConnection = 1;
rc = SendFile(zFallback, (int)strlen(zFallback), &statbuf);
free(zFallback);
exit(0);
}else{
Malfunction(706, "bad fallback file: \"%s\"\n", zFallback);
}
}
Malfunction(707, "cannot open socket to SCGI server %s\n",
zScript);
}
break;
}
nHdrAlloc = 0;
zHdr = 0;
if( zContentLength==0 ) zContentLength = "0";
zScgi = "1";
for(i=0; i<(int)(sizeof(cgienv)/sizeof(cgienv[0])); i++){
int n1, n2;
if( cgienv[i].pzEnvValue[0]==0 ) continue;
n1 = (int)strlen(cgienv[i].zEnvName);
n2 = (int)strlen(*cgienv[i].pzEnvValue);
if( n1+n2+2+nHdr >= nHdrAlloc ){
nHdrAlloc = nHdr + n1 + n2 + 1000;
zHdr = realloc(zHdr, nHdrAlloc);
if( zHdr==0 ){
Malfunction(706, "out of memory");
}
}
memcpy(zHdr+nHdr, cgienv[i].zEnvName, n1);
nHdr += n1;
zHdr[nHdr++] = 0;
memcpy(zHdr+nHdr, *cgienv[i].pzEnvValue, n2);
nHdr += n2;
zHdr[nHdr++] = 0;
}
zScgi = 0;
fprintf(s,"%d:",(int)nHdr);
fwrite(zHdr, 1, nHdr, s);
fprintf(s,",");
free(zHdr);
if( zMethod[0]=='P'
&& atoi(zContentLength)>0
&& (in = fopen(zTmpNam,"r"))!=0 ){
size_t n;
while( (n = fread(zLine,1,sizeof(zLine),in))>0 ){
fwrite(zLine, 1, n, s);
}
fclose(in);
}
fflush(s);
CgiHandleReply(s);
}
/*
** This routine processes a single HTTP request on standard input and
** sends the reply to standard output. If the argument is 1 it means
** that we are should close the socket without processing additional
** HTTP requests after the current request finishes. 0 means we are
** allowed to keep the connection open and to process additional requests.
** This routine may choose to close the connection even if the argument
** is 0.
**
** If the connection should be closed, this routine calls exit() and
** thus never returns. If this routine does return it means that another
** HTTP request may appear on the wire.
*/
void ProcessOneRequest(int forceClose){
int i, j, j0;
char *z; /* Used to parse up a string */
struct stat statbuf; /* Information about the file to be retrieved */
FILE *in; /* For reading from CGI scripts */
#ifdef LOG_HEADER
FILE *hdrLog = 0; /* Log file for complete header content */
#endif
char zLine[1000]; /* A buffer for input lines or forming names */
/* Change directories to the root of the HTTP filesystem
*/
if( chdir(zRoot[0] ? zRoot : "/")!=0 ){
char zBuf[1000];
Malfunction(190, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zRoot, getcwd(zBuf,999));
}
nRequest++;
/*
** We must receive a complete header within 15 seconds
*/
signal(SIGALRM, Timeout);
signal(SIGSEGV, Timeout);
signal(SIGPIPE, Timeout);
if( useTimeout ) alarm(15);
/* Get the first line of the request and parse out the
** method, the script and the protocol.
*/
if( fgets(zLine,sizeof(zLine),stdin)==0 ){
exit(0);
}
gettimeofday(&beginTime, 0);
omitLog = 0;
nIn += strlen(zLine);
/* Parse the first line of the HTTP request */
zMethod = StrDup(GetFirstElement(zLine,&z));
zRealScript = zScript = StrDup(GetFirstElement(z,&z));
zProtocol = StrDup(GetFirstElement(z,&z));
if( zProtocol==0 || strncmp(zProtocol,"HTTP/",5)!=0 || strlen(zProtocol)!=8 ){
StartResponse("400 Bad Request");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"This server does not understand the requested protocol\n"
);
MakeLogEntry(0, 200); /* LOG: bad protocol in HTTP header */
exit(0);
}
if( zScript[0]==0 ) NotFound(210); /* LOG: Empty request URI */
if( forceClose ){
closeConnection = 1;
}else if( zProtocol[5]<'1' || zProtocol[7]<'1' ){
closeConnection = 1;
}
/* This very simple server only understands the GET, POST
** and HEAD methods
*/
if( strcmp(zMethod,"GET")!=0 && strcmp(zMethod,"POST")!=0
&& strcmp(zMethod,"HEAD")!=0 ){
StartResponse("501 Not Implemented");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"The %s method is not implemented on this server.\n",
zMethod);
MakeLogEntry(0, 220); /* LOG: Unknown request method */
exit(0);
}
/* If there is a log file (if zLogFile!=0) and if the pathname in
** the first line of the http request contains the magic string
** "FullHeaderLog" then write the complete header text into the
** file %s(zLogFile)-hdr. Overwrite the file. This is for protocol
** debugging only and is only enabled if althttpd is compiled with
** the -DLOG_HEADER=1 option.
*/
#ifdef LOG_HEADER
if( zLogFile
&& strstr(zScript,"FullHeaderLog")!=0
&& strlen(zLogFile)<sizeof(zLine)-50
){
sprintf(zLine, "%s-hdr", zLogFile);
hdrLog = fopen(zLine, "wb");
}
#endif
/* Get all the optional fields that follow the first line.
*/
zCookie = 0;
zAuthType = 0;
zRemoteUser = 0;
zReferer = 0;
zIfNoneMatch = 0;
zIfModifiedSince = 0;
rangeEnd = 0;
while( fgets(zLine,sizeof(zLine),stdin) ){
char *zFieldName;
char *zVal;
#ifdef LOG_HEADER
if( hdrLog ) fprintf(hdrLog, "%s", zLine);
#endif
nIn += strlen(zLine);
zFieldName = GetFirstElement(zLine,&zVal);
if( zFieldName==0 || *zFieldName==0 ) break;
RemoveNewline(zVal);
if( strcasecmp(zFieldName,"User-Agent:")==0 ){
zAgent = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Accept:")==0 ){
zAccept = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Accept-Encoding:")==0 ){
zAcceptEncoding = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Content-length:")==0 ){
zContentLength = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Content-type:")==0 ){
zContentType = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Referer:")==0 ){
zReferer = StrDup(zVal);
if( strstr(zVal, "devids.net/")!=0 ){ zReferer = "devids.net.smut";
Forbidden(230); /* LOG: Referrer is devids.net */
}
}else if( strcasecmp(zFieldName,"Cookie:")==0 ){
zCookie = StrAppend(zCookie,"; ",zVal);
}else if( strcasecmp(zFieldName,"Connection:")==0 ){
if( strcasecmp(zVal,"close")==0 ){
closeConnection = 1;
}else if( !forceClose && strcasecmp(zVal, "keep-alive")==0 ){
closeConnection = 0;
}
}else if( strcasecmp(zFieldName,"Host:")==0 ){
int inSquare = 0;
char c;
if( sanitizeString(zVal) ){
Forbidden(240); /* LOG: Illegal content in HOST: parameter */
}
zHttpHost = StrDup(zVal);
zServerPort = zServerName = StrDup(zHttpHost);
while( zServerPort && (c = *zServerPort)!=0
&& (c!=':' || inSquare) ){
if( c=='[' ) inSquare = 1;
if( c==']' ) inSquare = 0;
zServerPort++;
}
if( zServerPort && *zServerPort ){
*zServerPort = 0;
zServerPort++;
}
if( zRealPort ){
zServerPort = StrDup(zRealPort);
}
}else if( strcasecmp(zFieldName,"Authorization:")==0 ){
zAuthType = GetFirstElement(StrDup(zVal), &zAuthArg);
}else if( strcasecmp(zFieldName,"If-None-Match:")==0 ){
zIfNoneMatch = StrDup(zVal);
}else if( strcasecmp(zFieldName,"If-Modified-Since:")==0 ){
zIfModifiedSince = StrDup(zVal);
}else if( strcasecmp(zFieldName,"Range:")==0
&& strcmp(zMethod,"GET")==0 ){
int x1 = 0, x2 = 0;
int n = sscanf(zVal, "bytes=%d-%d", &x1, &x2);
if( n==2 && x1>=0 && x2>=x1 ){
rangeStart = x1;
rangeEnd = x2;
}else if( n==1 && x1>0 ){
rangeStart = x1;
rangeEnd = 0x7fffffff;
}
}
}
#ifdef LOG_HEADER
if( hdrLog ) fclose(hdrLog);
#endif
/* Disallow requests from certain clients */
if( zAgent ){
const char *azDisallow[] = {
"Windows 9",
"Download Master",
"Ezooms/",
"HTTrace",
"AhrefsBot",
"MicroMessenger",
"OPPO A33 Build",
"SemrushBot",
};
size_t ii;
for(ii=0; ii<sizeof(azDisallow)/sizeof(azDisallow[0]); ii++){
if( strstr(zAgent,azDisallow[ii])!=0 ){
Forbidden(250); /* LOG: Disallowed user agent */
}
}
#if 0
/* Spider attack from 2019-04-24 */
if( strcmp(zAgent,
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36")==0 ){
Forbidden(251); /* LOG: Disallowed user agent (20190424) */
}
#endif
}
#if 0
if( zReferer ){
static const char *azDisallow[] = {
"skidrowcrack.com",
"hoshiyuugi.tistory.com",
"skidrowgames.net",
};
int i;
for(i=0; i<sizeof(azDisallow)/sizeof(azDisallow[0]); i++){
if( strstr(zReferer, azDisallow[i])!=0 ){
NotFound(260); /* LOG: Disallowed referrer */
}
}
}
#endif
/* Make an extra effort to get a valid server name and port number.
** Only Netscape provides this information. If the browser is
** Internet Explorer, then we have to find out the information for
** ourselves.
*/
if( zServerName==0 ){
zServerName = SafeMalloc( 100 );
gethostname(zServerName,100);
}
if( zServerPort==0 || *zServerPort==0 ){
zServerPort = DEFAULT_PORT;
}
/* Remove the query string from the end of the requested file.
*/
for(z=zScript; *z && *z!='?'; z++){}
if( *z=='?' ){
zQuerySuffix = StrDup(z);
*z = 0;
}else{
zQuerySuffix = "";
}
zQueryString = *zQuerySuffix ? &zQuerySuffix[1] : zQuerySuffix;
/* Create a file to hold the POST query data, if any. We have to
** do it this way. We can't just pass the file descriptor down to
** the child process because the fgets() function may have already
** read part of the POST data into its internal buffer.
*/
if( zMethod[0]=='P' && zContentLength!=0 ){
size_t len = atoi(zContentLength);
FILE *out;
char *zBuf;
int n;
if( len>MAX_CONTENT_LENGTH ){
StartResponse("500 Request too large");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"Too much POST data\n"
);
MakeLogEntry(0, 270); /* LOG: Request too large */
exit(0);
}
rangeEnd = 0;
sprintf(zTmpNamBuf, "/tmp/-post-data-XXXXXX");
zTmpNam = zTmpNamBuf;
if( mkstemp(zTmpNam)<0 ){
Malfunction(280, /* LOG: mkstemp() failed */
"Cannot create a temp file in which to store POST data");
}
out = fopen(zTmpNam,"wb");
if( out==0 ){
StartResponse("500 Cannot create /tmp file");
nOut += printf(
"Content-type: text/plain\r\n"
"\r\n"
"Could not open \"%s\" for writing\n", zTmpNam
);
MakeLogEntry(0, 290); /* LOG: cannot create temp file for POST */
exit(0);
}
zBuf = SafeMalloc( len+1 );
if( useTimeout ) alarm(15 + len/2000);
n = fread(zBuf,1,len,stdin);
nIn += n;
fwrite(zBuf,1,n,out);
free(zBuf);
fclose(out);
}
/* Make sure the running time is not too great */
if( useTimeout ) alarm(10);
/* Convert all unusual characters in the script name into "_".
**
** This is a defense against various attacks, XSS attacks in particular.
*/
sanitizeString(zScript);
/* Do not allow "/." or "/-" to to occur anywhere in the entity name.
** This prevents attacks involving ".." and also allows us to create
** files and directories whose names begin with "-" or "." which are
** invisible to the webserver.
**
** Exception: Allow the "/.well-known/" prefix in accordance with
** RFC-5785.
*/
for(z=zScript; *z; z++){
if( *z=='/' && (z[1]=='.' || z[1]=='-') ){
if( strncmp(zScript,"/.well-known/",13)==0 && (z[1]!='.' || z[2]!='.') ){
/* Exception: Allow "/." and "/-" for URLs that being with
** "/.well-known/". But do not allow "/..". */
continue;
}
NotFound(300); /* LOG: Path element begins with "." or "-" */
}
}
/* Figure out what the root of the filesystem should be. If the
** HTTP_HOST parameter exists (stored in zHttpHost) then remove the
** port number from the end (if any), convert all characters to lower
** case, and convert non-alphanumber characters (including ".") to "_".
** Then try to find a directory with that name and the extension .website.
** If not found, look for "default.website".
*/
if( zScript[0]!='/' ){
NotFound(310); /* LOG: URI does not start with "/" */
}
if( strlen(zRoot)+40 >= sizeof(zLine) ){
NotFound(320); /* LOG: URI too long */
}
if( zHttpHost==0 || zHttpHost[0]==0 ){
NotFound(330); /* LOG: Missing HOST: parameter */
}else if( strlen(zHttpHost)+strlen(zRoot)+10 >= sizeof(zLine) ){
NotFound(340); /* LOG: HOST parameter too long */
}else{
sprintf(zLine, "%s/%s", zRoot, zHttpHost);
for(i=strlen(zRoot)+1; zLine[i] && zLine[i]!=':'; i++){
unsigned char c = (unsigned char)zLine[i];
if( !isalnum(c) ){
if( c=='.' && (zLine[i+1]==0 || zLine[i+1]==':') ){
/* If the client sent a FQDN with a "." at the end
** (example: "sqlite.org." instead of just "sqlite.org") then
** omit the final "." from the document root directory name */
break;
}
zLine[i] = '_';
}else if( isupper(c) ){
zLine[i] = tolower(c);
}
}
strcpy(&zLine[i], ".website");
}
if( stat(zLine,&statbuf) || !S_ISDIR(statbuf.st_mode) ){
sprintf(zLine, "%s/default.website", zRoot);
if( stat(zLine,&statbuf) || !S_ISDIR(statbuf.st_mode) ){
if( standalone ){
sprintf(zLine, "%s", zRoot);
}else{
NotFound(350); /* LOG: *.website permissions */
}
}
}
zHome = StrDup(zLine);
/* Change directories to the root of the HTTP filesystem
*/
if( chdir(zHome)!=0 ){
char zBuf[1000];
Malfunction(360, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zHome, getcwd(zBuf,999));
}
/* Locate the file in the filesystem. We might have to append
** a name like "/home" or "/index.html" or "/index.cgi" in order
** to find it. Any excess path information is put into the
** zPathInfo variable.
*/
j = j0 = (int)strlen(zLine);
i = 0;
while( zScript[i] ){
while( zScript[i] && (i==0 || zScript[i]!='/') ){
zLine[j] = zScript[i];
i++; j++;
}
zLine[j] = 0;
if( stat(zLine,&statbuf)!=0 ){
int stillSearching = 1;
while( stillSearching && i>0 && j>j0 ){
while( j>j0 && zLine[j-1]!='/' ){ j--; }
strcpy(&zLine[j-1], "/not-found.html");
if( stat(zLine,&statbuf)==0 && S_ISREG(statbuf.st_mode)
&& access(zLine,R_OK)==0 ){
zRealScript = StrDup(&zLine[j0]);
Redirect(zRealScript, 302, 1, 370); /* LOG: redirect to not-found */
return;
}else{
j--;
}
}
if( stillSearching ) NotFound(380); /* LOG: URI not found */
break;
}
if( S_ISREG(statbuf.st_mode) ){
if( access(zLine,R_OK) ){
NotFound(390); /* LOG: File not readable */
}
zRealScript = StrDup(&zLine[j0]);
break;
}
if( zScript[i]==0 || zScript[i+1]==0 ){
static const char *azIndex[] = { "/home", "/index.html", "/index.cgi" };
int k = j>0 && zLine[j-1]=='/' ? j-1 : j;
unsigned int jj;
for(jj=0; jj<sizeof(azIndex)/sizeof(azIndex[0]); jj++){
strcpy(&zLine[k],azIndex[jj]);
if( stat(zLine,&statbuf)!=0 ) continue;
if( !S_ISREG(statbuf.st_mode) ) continue;
if( access(zLine,R_OK) ) continue;
break;
}
if( jj>=sizeof(azIndex)/sizeof(azIndex[0]) ){
NotFound(400); /* LOG: URI is a directory w/o index.html */
}
zRealScript = StrDup(&zLine[j0]);
if( zScript[i]==0 ){
/* If the requested URL does not end with "/" but we had to
** append "index.html", then a redirect is necessary. Otherwise
** none of the relative URLs in the delivered document will be
** correct. */
Redirect(zRealScript,301,1,410); /* LOG: redirect to add trailing / */
return;
}
break;
}
zLine[j] = zScript[i];
i++; j++;
}
zFile = StrDup(zLine);
zPathInfo = StrDup(&zScript[i]);
lenFile = strlen(zFile);
zDir = StrDup(zFile);
for(i=strlen(zDir)-1; i>0 && zDir[i]!='/'; i--){};
if( i==0 ){
strcpy(zDir,"/");
}else{
zDir[i] = 0;
}
/* Check to see if there is an authorization file. If there is,
** process it.
*/
sprintf(zLine, "%s/-auth", zDir);
if( access(zLine,R_OK)==0 && !CheckBasicAuthorization(zLine) ) return;
/* Take appropriate action
*/
if( (statbuf.st_mode & 0100)==0100 && access(zFile,X_OK)==0 ){
char *zBaseFilename; /* Filename without directory prefix */
/*
** Abort with an error if the CGI script is writable by anyone other
** than its owner.
*/
if( statbuf.st_mode & 0022 ){
CgiScriptWritable();
}
/* If its executable, it must be a CGI program. Start by
** changing directories to the directory holding the program.
*/
if( chdir(zDir) ){
char zBuf[1000];
Malfunction(420, /* LOG: chdir() failed */
"cannot chdir to [%s] from [%s]",
zDir, getcwd(zBuf,999));
}
/* Compute the base filename of the CGI script */
for(i=strlen(zFile)-1; i>=0 && zFile[i]!='/'; i--){}
zBaseFilename = &zFile[i+1];
/* Setup the environment appropriately.
*/
putenv("GATEWAY_INTERFACE=CGI/1.0");
for(i=0; i<(int)(sizeof(cgienv)/sizeof(cgienv[0])); i++){
if( *cgienv[i].pzEnvValue ){
SetEnv(cgienv[i].zEnvName,*cgienv[i].pzEnvValue);
}
}
if( useHttps ){
putenv("HTTPS=on");
}
/* For the POST method all input has been written to a temporary file,
** so we have to redirect input to the CGI script from that file.
*/
if( zMethod[0]=='P' ){
if( dup(0)<0 ){
Malfunction(430, /* LOG: dup(0) failed */
"Unable to duplication file descriptor 0");
}
close(0);
open(zTmpNam, O_RDONLY);
}
if( strncmp(zBaseFilename,"nph-",4)==0 ){
/* If the name of the CGI script begins with "nph-" then we are
** dealing with a "non-parsed headers" CGI script. Just exec()
** it directly and let it handle all its own header generation.
*/
execl(zBaseFilename,zBaseFilename,(char*)0);
/* NOTE: No log entry written for nph- scripts */
exit(0);
}
/* Fall thru to here only if this process (the server) is going
** to read and augment the header sent back by the CGI process.
** Open a pipe to receive the output from the CGI process. Then
** fork the CGI process. Once everything is done, we should be
** able to read the output of CGI on the "in" stream.
*/
{
int px[2];
if( pipe(px) ){
Malfunction(440, /* LOG: pipe() failed */
"Unable to create a pipe for the CGI program");
}
if( fork()==0 ){
close(px[0]);
close(1);
if( dup(px[1])!=1 ){
Malfunction(450, /* LOG: dup(1) failed */
"Unable to duplicate file descriptor %d to 1",
px[1]);
}
close(px[1]);
for(i=3; close(i)==0; i++){}
execl(zBaseFilename, zBaseFilename, (char*)0);
exit(0);
}
close(px[1]);
in = fdopen(px[0], "rb");
}
if( in==0 ){
CgiError();
}else{
CgiHandleReply(in);
}
}else if( lenFile>5 && strcmp(&zFile[lenFile-5],".scgi")==0 ){
/* Any file that ends with ".scgi" is assumed to be text of the
** form:
** SCGI hostname port
** Open a TCP/IP connection to that host and send it an SCGI request
*/
SendScgiRequest(zFile, zScript);
}else if( countSlashes(zRealScript)!=countSlashes(zScript) ){
/* If the request URI for static content contains material past the
** actual content file name, report that as a 404 error. */
NotFound(460); /* LOG: Excess URI content past static file name */
}else{
/* If it isn't executable then it
** must a simple file that needs to be copied to output.
*/
if( SendFile(zFile, lenFile, &statbuf) ) return;
}
fflush(stdout);
MakeLogEntry(0, 0); /* LOG: Normal reply */
/* The next request must arrive within 30 seconds or we close the connection
*/
omitLog = 1;
if( useTimeout ) alarm(30);
}
#define MAX_PARALLEL 50 /* Number of simultaneous children */
/*
** All possible forms of an IP address. Needed to work around GCC strict
** aliasing rules.
*/
typedef union {
struct sockaddr sa; /* Abstract superclass */
struct sockaddr_in sa4; /* IPv4 */
struct sockaddr_in6 sa6; /* IPv6 */
struct sockaddr_storage sas; /* Should be the maximum of the above 3 */
} address;
/*
** Implement an HTTP server daemon listening on port iPort.
**
** As new connections arrive, fork a child and let child return
** out of this procedure call. The child will handle the request.
** The parent never returns from this procedure.
**
** Return 0 to each child as it runs. If unable to establish a
** listening socket, return non-zero.
*/
int http_server(const char *zPort, int localOnly){
int listener[20]; /* The server sockets */
int connection; /* A socket for each individual connection */
fd_set readfds; /* Set of file descriptors for select() */
address inaddr; /* Remote address */
socklen_t lenaddr; /* Length of the inaddr structure */
int child; /* PID of the child process */
int nchildren = 0; /* Number of child processes */
struct timeval delay; /* How long to wait inside select() */
int opt = 1; /* setsockopt flag */
struct addrinfo sHints; /* Address hints */
struct addrinfo *pAddrs, *p; /* */
int rc; /* Result code */
int i, n;
int maxFd = -1;
memset(&sHints, 0, sizeof(sHints));
if( ipv4Only ){
sHints.ai_family = PF_INET;
/*printf("ipv4 only\n");*/
}else if( ipv6Only ){
sHints.ai_family = PF_INET6;
/*printf("ipv6 only\n");*/
}else{
sHints.ai_family = PF_UNSPEC;
}
sHints.ai_socktype = SOCK_STREAM;
sHints.ai_flags = AI_PASSIVE;
sHints.ai_protocol = 0;
rc = getaddrinfo(localOnly ? "localhost": 0, zPort, &sHints, &pAddrs);
if( rc ){
fprintf(stderr, "could not get addr info: %s",
rc!=EAI_SYSTEM ? gai_strerror(rc) : strerror(errno));
return 1;
}
for(n=0, p=pAddrs; n<(int)(sizeof(listener)/sizeof(listener[0])) && p!=0;
p=p->ai_next){
listener[n] = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if( listener[n]>=0 ){
/* if we can't terminate nicely, at least allow the socket to be reused */
setsockopt(listener[n], SOL_SOCKET, SO_REUSEADDR,&opt, sizeof(opt));
#if defined(IPV6_V6ONLY)
if( p->ai_family==AF_INET6 ){
int v6only = 1;
setsockopt(listener[n], IPPROTO_IPV6, IPV6_V6ONLY,
&v6only, sizeof(v6only));
}
#endif
if( bind(listener[n], p->ai_addr, p->ai_addrlen)<0 ){
printf("bind failed: %s\n", strerror(errno));
close(listener[n]);
continue;
}
if( listen(listener[n], 20)<0 ){
printf("listen() failed: %s\n", strerror(errno));
close(listener[n]);
continue;
}
n++;
}
}
if( n==0 ){
fprintf(stderr, "cannot open any sockets\n");
return 1;
}
while( 1 ){
if( nchildren>MAX_PARALLEL ){
/* Slow down if connections are arriving too fast */
sleep( nchildren-MAX_PARALLEL );
}
delay.tv_sec = 60;
delay.tv_usec = 0;
FD_ZERO(&readfds);
for(i=0; i<n; i++){
assert( listener[i]>=0 );
FD_SET( listener[i], &readfds);
if( listener[i]>maxFd ) maxFd = listener[i];
}
select( maxFd+1, &readfds, 0, 0, &delay);
for(i=0; i<n; i++){
if( FD_ISSET(listener[i], &readfds) ){
lenaddr = sizeof(inaddr);
connection = accept(listener[i], &inaddr.sa, &lenaddr);
if( connection>=0 ){
child = fork();
if( child!=0 ){
if( child>0 ) nchildren++;
close(connection);
/* printf("subprocess %d started...\n", child); fflush(stdout); */
}else{
int nErr = 0, fd;
close(0);
fd = dup(connection);
if( fd!=0 ) nErr++;
close(1);
fd = dup(connection);
if( fd!=1 ) nErr++;
close(connection);
return nErr;
}
}
}
/* Bury dead children */
while( (child = waitpid(0, 0, WNOHANG))>0 ){
/* printf("process %d ends\n", child); fflush(stdout); */
nchildren--;
}
}
}
/* NOT REACHED */
exit(1);
}
int main(int argc, char **argv){
int i; /* Loop counter */
char *zPermUser = 0; /* Run daemon with this user's permissions */
const char *zPort = 0; /* Implement an HTTP server process */
int useChrootJail = 1; /* True to use a change-root jail */
struct passwd *pwd = 0; /* Information about the user */
/* Record the time when processing begins.
*/
gettimeofday(&beginTime, 0);
/* Parse command-line arguments
*/
while( argc>1 && argv[1][0]=='-' ){
char *z = argv[1];
char *zArg = argc>=3 ? argv[2] : "0";
if( z[0]=='-' && z[1]=='-' ) z++;
if( strcmp(z,"-user")==0 ){
zPermUser = zArg;
}else if( strcmp(z,"-root")==0 ){
zRoot = zArg;
}else if( strcmp(z,"-logfile")==0 ){
zLogFile = zArg;
}else if( strcmp(z,"-max-age")==0 ){
mxAge = atoi(zArg);
}else if( strcmp(z,"-https")==0 ){
useHttps = atoi(zArg);
zHttp = useHttps ? "https" : "http";
if( useHttps ) zRemoteAddr = getenv("REMOTE_HOST");
}else if( strcmp(z, "-port")==0 ){
zPort = zArg;
standalone = 1;
}else if( strcmp(z, "-family")==0 ){
if( strcmp(zArg, "ipv4")==0 ){
ipv4Only = 1;
}else if( strcmp(zArg, "ipv6")==0 ){
ipv6Only = 1;
}else{
Malfunction(500, /* LOG: unknown IP protocol */
"unknown IP protocol: [%s]\n", zArg);
}
}else if( strcmp(z, "-jail")==0 ){
if( atoi(zArg)==0 ){
useChrootJail = 0;
}
}else if( strcmp(z, "-debug")==0 ){
if( atoi(zArg) ){
useTimeout = 0;
}
}else if( strcmp(z, "-input")==0 ){
if( freopen(zArg, "rb", stdin)==0 || stdin==0 ){
Malfunction(501, /* LOG: cannot open --input file */
"cannot open --input file \"%s\"\n", zArg);
}
}else if( strcmp(z, "-datetest")==0 ){
TestParseRfc822Date();
printf("Ok\n");
exit(0);
}else{
Malfunction(510, /* LOG: unknown command-line argument on launch */
"unknown argument: [%s]\n", z);
}
argv += 2;
argc -= 2;
}
if( zRoot==0 ){
if( standalone ){
zRoot = ".";
}else{
Malfunction(520, /* LOG: --root argument missing */
"no --root specified");
}
}
/* Change directories to the root of the HTTP filesystem. Then
** create a chroot jail there.
*/
if( chdir(zRoot)!=0 ){
Malfunction(530, /* LOG: chdir() failed */
"cannot change to directory [%s]", zRoot);
}
/* Get information about the user if available */
if( zPermUser ) pwd = getpwnam(zPermUser);
/* Enter the chroot jail if requested */
if( zPermUser && useChrootJail && getuid()==0 ){
if( chroot(".")<0 ){
Malfunction(540, /* LOG: chroot() failed */
"unable to create chroot jail");
}else{
zRoot = "";
}
}
/* Activate the server, if requested */
if( zPort && http_server(zPort, 0) ){
Malfunction(550, /* LOG: server startup failed */
"failed to start server");
}
/* Drop root privileges.
*/
if( zPermUser ){
if( pwd ){
if( setgid(pwd->pw_gid) ){
Malfunction(560, /* LOG: setgid() failed */
"cannot set group-id to %d", pwd->pw_gid);
}
if( setuid(pwd->pw_uid) ){
Malfunction(570, /* LOG: setuid() failed */
"cannot set user-id to %d", pwd->pw_uid);
}
}else{
Malfunction(580, /* LOG: unknown user */
"no such user [%s]", zPermUser);
}
}
if( getuid()==0 ){
Malfunction(590, /* LOG: cannot run as root */
"cannot run as root");
}
/* Get the IP address from whence the request originates
*/
if( zRemoteAddr==0 ){
address remoteAddr;
unsigned int size = sizeof(remoteAddr);
char zHost[NI_MAXHOST];
if( getpeername(0, &remoteAddr.sa, &size)>=0 ){
getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0,
NI_NUMERICHOST);
zRemoteAddr = StrDup(zHost);
}
}
if( zRemoteAddr!=0
&& strncmp(zRemoteAddr, "::ffff:", 7)==0
&& strchr(zRemoteAddr+7, ':')==0
&& strchr(zRemoteAddr+7, '.')!=0
){
zRemoteAddr += 7;
}
/* Process the input stream */
for(i=0; i<100; i++){
ProcessOneRequest(0);
}
ProcessOneRequest(1);
exit(0);
}
#if 0
/* Copy/paste the following text into SQLite to generate the xref
** table that describes all error codes.
*/
BEGIN;
CREATE TABLE IF NOT EXISTS xref(lineno INTEGER PRIMARY KEY, desc TEXT);
DELETE FROM Xref;
INSERT INTO xref VALUES(100,'Malloc() failed');
INSERT INTO xref VALUES(110,'Not authorized');
INSERT INTO xref VALUES(120,'CGI Error');
INSERT INTO xref VALUES(130,'Timeout');
INSERT INTO xref VALUES(140,'CGI script is writable');
INSERT INTO xref VALUES(150,'Cannot open -auth file');
INSERT INTO xref VALUES(160,'http request on https-only page');
INSERT INTO xref VALUES(170,'-auth redirect');
INSERT INTO xref VALUES(180,'malformed entry in -auth file');
INSERT INTO xref VALUES(190,'chdir() failed');
INSERT INTO xref VALUES(200,'bad protocol in HTTP header');
INSERT INTO xref VALUES(210,'Empty request URI');
INSERT INTO xref VALUES(220,'Unknown request method');
INSERT INTO xref VALUES(230,'Referrer is devids.net');
INSERT INTO xref VALUES(240,'Illegal content in HOST: parameter');
INSERT INTO xref VALUES(250,'Disallowed user agent');
INSERT INTO xref VALUES(260,'Disallowed referrer');
INSERT INTO xref VALUES(270,'Request too large');
INSERT INTO xref VALUES(280,'mkstemp() failed');
INSERT INTO xref VALUES(290,'cannot create temp file for POST content');
INSERT INTO xref VALUES(300,'Path element begins with . or -');
INSERT INTO xref VALUES(310,'URI does not start with /');
INSERT INTO xref VALUES(320,'URI too long');
INSERT INTO xref VALUES(330,'Missing HOST: parameter');
INSERT INTO xref VALUES(340,'HOST parameter too long');
INSERT INTO xref VALUES(350,'*.website permissions');
INSERT INTO xref VALUES(360,'chdir() failed');
INSERT INTO xref VALUES(370,'redirect to not-found page');
INSERT INTO xref VALUES(380,'URI not found');
INSERT INTO xref VALUES(390,'File not readable');
INSERT INTO xref VALUES(400,'URI is a directory w/o index.html');
INSERT INTO xref VALUES(410,'redirect to add trailing /');
INSERT INTO xref VALUES(420,'chdir() failed');
INSERT INTO xref VALUES(430,'dup(0) failed');
INSERT INTO xref VALUES(440,'pipe() failed');
INSERT INTO xref VALUES(450,'dup(1) failed');
INSERT INTO xref VALUES(460,'Excess URI content past static file name');
INSERT INTO xref VALUES(470,'ETag Cache Hit');
INSERT INTO xref VALUES(480,'fopen() failed for static content');
INSERT INTO xref VALUES(2,'Normal HEAD reply');
INSERT INTO xref VALUES(0,'Normal reply');
INSERT INTO xref VALUES(500,'unknown IP protocol');
INSERT INTO xref VALUES(501,'cannot open --input file');
INSERT INTO xref VALUES(510,'unknown command-line argument on launch');
INSERT INTO xref VALUES(520,'--root argument missing');
INSERT INTO xref VALUES(530,'chdir() failed');
INSERT INTO xref VALUES(540,'chroot() failed');
INSERT INTO xref VALUES(550,'server startup failed');
INSERT INTO xref VALUES(560,'setgid() failed');
INSERT INTO xref VALUES(570,'setuid() failed');
INSERT INTO xref VALUES(580,'unknown user');
INSERT INTO xref VALUES(590,'cannot run as root');
INSERT INTO xref VALUES(600,'malloc() failed');
INSERT INTO xref VALUES(610,'malloc() failed');
COMMIT;
#endif /* SQL */