Index: misc/althttpd.c ================================================================== --- misc/althttpd.c +++ misc/althttpd.c @@ -3,11 +3,11 @@ ** ** Features: ** ** * Launched from inetd/xinetd/stunnel4, or as a stand-alone server ** * One process per request -** * Deliver static content or run CGI +** * 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 @@ -40,12 +40,12 @@ ** ** (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. All other files are delivered -** as is. +** (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 @@ -56,10 +56,14 @@ ** 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. ** ** Command-line Options: ** ** --root DIR Defines the directory that contains the various ** $HOST.website subdirectories, each containing web content @@ -262,26 +266,24 @@ 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 *gateway_interface = "CGI/1.0"; /* CGI version number */ /* ** 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_LENGTH", &zContentLength }, { "CONTENT_TYPE", &zContentType }, { "DOCUMENT_ROOT", &zHome }, - { "GATEWAY_INTERFACE", &gateway_interface }, { "HTTP_ACCEPT", &zAccept }, { "HTTP_ACCEPT_ENCODING", &zAcceptEncoding }, { "HTTP_COOKIE", &zCookie }, { "HTTP_HOST", &zHttpHost }, { "HTTP_IF_MODIFIED_SINCE", &zIfModifiedSince }, @@ -306,12 +308,12 @@ /* ** Double any double-quote characters in a string. */ static char *Escape(char *z){ - int i, j; - int n; + 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; @@ -427,11 +429,11 @@ } /* ** Allocate memory safely */ -static char *SafeMalloc( int size ){ +static char *SafeMalloc( size_t size ){ char *p; p = (char*)malloc(size); if( p==0 ){ strcpy(zReplyStatus, "998"); @@ -444,11 +446,11 @@ /* ** Set the value of environment variable zVar to zValue. */ static void SetEnv(const char *zVar, const char *zValue){ char *z; - int len; + 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); @@ -482,21 +484,21 @@ /* ** Make a copy of a string into memory obtained from malloc. */ static char *StrDup(const char *zSrc){ char *zDest; - int size; + 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; - int size; + size_t size; int n1, n2; if( zSrc==0 ) return 0; if( zPrior==0 ) return StrDup(zSrc); size = (n1=strlen(zSrc)) + (n2=strlen(zSep)) + strlen(zPrior) + 1; @@ -1186,12 +1188,12 @@ ** 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 */ - int nRes = 0; /* Bytes of payload */ - int nMalloc = 0; /* Bytes of space allocated to aRes */ + 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 */ char zLine[1000]; /* One line of reply from the CGI script */ @@ -1245,15 +1247,115 @@ } } aRes[nRes++] = c; } aRes[nRes] = 0; - nOut += printf("Content-length: %d\r\n\r\n%s", nRes, aRes); + nOut += printf("Content-length: %d\r\n\r\n%s", (int)nRes, aRes); 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; + 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]; + 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); + } + fclose(in); + 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 || zPort==0 ){ + Malfunction(703, "misformatted SCGI spec \"%s\"\n", zFile); + } + 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)); + } + 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 ){ + Malfunction(705, "cannot open socket to SCGI server %s\n", + zScript); + } + s = fdopen(iSocket, "r+"); + if( s==0 ){ + Malfunction(706, "could not turn the socket into a FILE\n"); + } + + nHdrAlloc = 0; + zHdr = 0; + if( zContentLength==0 ) zContentLength = "0"; + 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; + } + 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 @@ -1487,11 +1589,11 @@ ** 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 ){ - int len = atoi(zContentLength); + size_t len = atoi(zContentLength); FILE *out; char *zBuf; int n; if( len>MAX_CONTENT_LENGTH ){ @@ -1684,15 +1786,19 @@ 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 */ + /* - ** The followings static variables are used to setup the environment - ** for the CGI script + ** Abort with an error if the CGI script is writable by anyone other + ** than its owner. */ - char *zBaseFilename; /* Filename without directory prefix */ + 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) ){ @@ -1699,30 +1805,27 @@ 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"); } - /* - ** Abort with an error if the CGI script is writable by anyone other - ** than its owner. - */ - if( statbuf.st_mode & 0022 ){ - CgiScriptWritable(); - } - /* 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 ){ @@ -1731,13 +1834,11 @@ } close(0); open(zTmpNam, O_RDONLY); } - for(i=strlen(zFile)-1; i>=0 && zFile[i]!='/'; i--){} - zBaseFilename = &zFile[i+1]; - if( i>=0 && strncmp(zBaseFilename,"nph-",4)==0 ){ + 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); @@ -1776,10 +1877,17 @@ 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{ Index: misc/althttpd.md ================================================================== --- misc/althttpd.md +++ misc/althttpd.md @@ -150,11 +150,14 @@ On a minimal installation that only hosts a single website, it suffices to have a single subdirectory named "default.website". Within the *.website directory, the file to be served is selected by the HTTP request URI. Files that are marked as executable are run -as CGI. Non-executable files are delivered as-is. +as CGI. Non-executable files with a name that ends with ".scgi" +and that have content of the form "SCGI hostname port" relay an SCGI +request to hostname:port. All other non-execcutable files are delivered +as-is. If the request URI specifies the name of a directory within *.website, then althttpd appends "/index.html" and "/index.cgi", in that order, looking for a match.