ADDED .editorconfig Index: .editorconfig ================================================================== --- /dev/null +++ .editorconfig @@ -0,0 +1,12 @@ +# EditorConfig (https://editorconfig.com) configuration suitable for +# "drh projects": fossil, althttpd, and their kin. + +# Defaults for all files +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 + +[{Makefile,Makefile.*,*.make,*.make.in}] +indent_style = tab Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -1,5 +1,10 @@ +default: althttpd althttpsd + althttpd: althttpd.c cc -Os -Wall -Wextra -o althttpd althttpd.c +althttpsd: althttpd.c + cc -Os -Wall -Wextra -fPIC -o althttpsd -DENABLE_TLS althttpd.c -lssl -lcrypto + clean: - rm -f althttpd + rm -f althttpd althttpsd Index: althttpd.c ================================================================== --- althttpd.c +++ althttpd.c @@ -73,11 +73,11 @@ ** 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 +** to hostname:port, then relay back the reply. Error behavior is ** determined by subsequent lines of the .scgi file. See SCGI below ** for details. ** ** Command-line Options: ** @@ -102,13 +102,14 @@ ** 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.) +** --https BOOLEAN Indicates that input is coming over SSL and is being +** decoded upstream, perhaps by stunnel. This option +** does *not* activate built-in TLS support. Use --tls +** for that. ** ** --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. ** @@ -119,14 +120,31 @@ ** ** --max-age SEC The value for "Cache-Control: max-age=%d". Defaults to ** 120 seconds. ** ** --max-cpu SEC Maximum number of seconds of CPU time allowed per -** HTTP connection. Default 30. 0 means no limit. +** HTTP connection. Default 30 (build option: +** -DMAX_CPU=integer). 0 means no limit. ** ** --debug Disables input timeouts. This is useful for debugging -** when inputs is being typed in manually. +** when inputs are being typed in manually. +** +** Additional command-line options available when compiling with ENABLE_TLS: +** +** --cert FILE The TLS certificate, the "fullchain.pem" file +** +** --pkey FILE The TLS private key, the "privkey.pem" file. May be +** omitted if the --cert file is the concatenation of +** the fullchain.pem and the privkey.pem. +** +** --tls BOOLEAN Activate TLS support if BOOLEAN is true. This is +** implied if the --cert option is specified. If this +** option is true and --cert is omitted, then an +** insecure self-signed certificate is used. Use this +** self-signed cert for testing purposes only, as it is +** wildly insecure. +** ** ** Command-line options can take either one or two initial "-" characters. ** So "--debug" and "-debug" mean the same thing, for example. ** ** @@ -144,11 +162,11 @@ ** 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. +** with an error. (Build option: -DMAX_CONTENT_LENGTH=integer) ** ** (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.) @@ -176,11 +194,11 @@ ** ** 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: +** To locate 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 @@ -203,24 +221,26 @@ ** ** 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. +** 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 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 @@ -284,11 +304,11 @@ /* ** 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 const 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 */ @@ -320,17 +340,18 @@ 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 const 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 int useHttps = 0; /* 0=HTTP, 1=external HTTPS (stunnel), + ** 2=builtin TLS support */ 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 */ @@ -341,58 +362,197 @@ 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 */ static int maxCpu = MAX_CPU; /* Maximum CPU time per process */ +static void Malfunction(int errNo, const char *zFormat, ...); + +#ifdef ENABLE_TLS +#include +#include +#include +#include +typedef struct TlsServerConn { + SSL *ssl; /* The SSL codec */ + int atEof; /* True when EOF reached. */ + int iSocket; /* The socket */ +} TlsServerConn; + +/* +** There can only be a single OpenSSL IO connection open at a time. +** State information about that IO is stored in the following +** local variables: +*/ +static struct TlsState { + int isInit; /* 0: uninit 1: init as client 2: init as server */ + SSL_CTX *ctx; + const char *zCertFile; /* --cert CLI arg */ + const char *zKeyFile; /* --pkey CLI arg */ + TlsServerConn * sslCon; +} tlsState = { + 0, /* isInit */ + NULL, /* SSL_CTX *ctx */ + NULL, /* zCertFile */ + NULL, /* zKeyFile */ + NULL /* sslCon */ +}; +/* +** Read a single line of text from the client and stores it in zBuf +** (which must be at least nBuf bytes long). At EOF it sets +** tlsState.sslCon->atEof to non-0 and returns 0. On error is simply +** returns 0. Once tlsState.sslCon->atEof is non-0, subsequent calls +** to this function return 0 without reading anything. +** +** If it reads anything, it returns zBuf. +*/ +static char *tls_gets(void *pServerArg, char *zBuf, int nBuf){ + int n = 0; + int i; + TlsServerConn * const pServer = (TlsServerConn*)pServerArg; + if( pServer->atEof ) return 0; + for(i=0; issl, &zBuf[i], 1); + if( n<=0 ){ + pServer->atEof = 1; + return 0; + } + if( zBuf[i]=='\n' ) break; + } + zBuf[i+1] = 0; + return zBuf; +} +/* +** Reads up tp nBuf bytes of TLS-decoded bytes from the client and +** stores them in zBuf, which must be least nBuf bytes long. Returns +** the number of bytes read. Fails fatally if nBuf is "too big". Once +** pServerArg reaches EOF, this function simply returns 0. +*/ +static size_t tls_read_server(void *pServerArg, void *zBuf, size_t nBuf){ + int n; + TlsServerConn *pServer = (TlsServerConn*)pServerArg; + if( pServer->atEof ) return 0; + if( nBuf>0x7fffffff ){ Malfunction(500,"SSL read too big"); } + n = SSL_read(pServer->ssl, zBuf, (int)nBuf); + if( n==0 ) pServer->atEof = 1; + return n<=0 ? 0 : n; +} + +/* +** Write cleartext bytes into the SSL server codec so that they can +** be encrypted and sent back to the client. On success, returns +** the number of bytes written, else returns a negative value. +*/ +static int tls_write_server(void *pServerArg, void const *zBuf, size_t nBuf){ + int n; + TlsServerConn * const pServer = (TlsServerConn*)pServerArg; + if( nBuf<=0 ) return 0; + if( nBuf>0x7fffffff ){ Malfunction(500,"SSL write too big"); } + n = SSL_write(pServer->ssl, zBuf, (int)nBuf); + if( n<=0 ){ + /* Do NOT call Malfunction() from here, as Malfunction() + ** may output via this function. The current error handling + ** is somewhat unsatisfactory, as it can lead to negative + ** response length sizes in the althttpd log. */ + return -SSL_get_error(pServer->ssl, n); + }else{ + return n; + } +} +#endif /* ENABLE_TLS */ + +/* +** A printf() proxy which outputs either to stdout or the outbound TLS +** connection, depending on connection state. It uses a +** statically-sized buffer for TLS output and will fail (via +** Malfunction()) if it's passed too much data. In non-TLS mode it has +** no such limitation. The buffer is generously sized, in any case, to +** be able to handle all of the headers output by althttpd as of the +** time of this writing. +*/ +#ifdef ENABLE_TLS +static int althttpd_vprintf(char const * fmt, va_list va){ + if( useHttps!=2 || NULL==tlsState.sslCon ){ + return vprintf(fmt, va); + }else{ + enum { PF_BUFFER_SIZE = 1024 * 2 }; + static char pfBuffer[PF_BUFFER_SIZE] = {0}; + const int sz = vsnprintf(pfBuffer, PF_BUFFER_SIZE, fmt, va); + if( sz='4' ){ closeConnection = 1; } if( closeConnection ){ - nOut += printf("Connection: close\r\n"); + nOut += althttpd_printf("Connection: close\r\n"); }else{ - nOut += printf("Connection: keep-alive\r\n"); + nOut += althttpd_printf("Connection: keep-alive\r\n"); } nOut += DateTag("Date", now); statusSent = 1; } @@ -696,11 +856,11 @@ /* ** Tell the client that there is no such document */ static void NotFound(int lineno){ StartResponse("404 Not Found"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/html; charset=utf-8\r\n" "\r\n" "Not Found\n" "

Document Not Found

\n" "The document %s is not available on this server\n" @@ -712,11 +872,11 @@ /* ** Tell the client that they are not welcomed here. */ static void Forbidden(int lineno){ StartResponse("403 Forbidden"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\r\n" "\r\n" "Access denied\n" ); closeConnection = 1; @@ -728,11 +888,11 @@ ** Tell the client that authorization is required to access the ** document. */ static void NotAuthorized(const char *zRealm){ StartResponse("401 Authorization Required"); - nOut += printf( + nOut += althttpd_printf( "WWW-Authenticate: Basic realm=\"%s\"\r\n" "Content-type: text/html; charset=utf-8\r\n" "\r\n" "Not Authorized\n" "

401 Not Authorized

\n" @@ -744,11 +904,11 @@ /* ** Tell the client that there is an error in the script. */ static void CgiError(void){ StartResponse("500 Error"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/html; charset=utf-8\r\n" "\r\n" "CGI Program Error\n" "

CGI Program Error

\n" "The CGI program %s generated an error\n" @@ -779,11 +939,11 @@ /* ** Tell the client that there is an error in the script. */ static void CgiScriptWritable(void){ StartResponse("500 CGI Configuration Error"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\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 */ @@ -791,21 +951,21 @@ } /* ** Tell the client that the server malfunctioned. */ -static void Malfunction(int linenum, const char *zFormat, ...){ +void Malfunction(int linenum, const char *zFormat, ...){ va_list ap; va_start(ap, zFormat); StartResponse("500 Server Malfunction"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\r\n" "\r\n" "Web server malfunctioned; error number %d\n\n", linenum); if( zFormat ){ - nOut += vprintf(zFormat, ap); - printf("\n"); + nOut += althttpd_vprintf(zFormat, ap); + althttpd_printf("\n"); nOut++; } va_end(ap); MakeLogEntry(0, linenum); exit(0); @@ -827,19 +987,19 @@ default: StartResponse("302 Temporary Redirect"); break; } if( zServerPort==0 || zServerPort[0]==0 || strcmp(zServerPort,"80")==0 ){ - nOut += printf("Location: %s://%s%s%s\r\n", + nOut += althttpd_printf("Location: %s://%s%s%s\r\n", zHttp, zServerName, zPath, zQuerySuffix); }else{ - nOut += printf("Location: %s://%s:%s%s%s\r\n", + nOut += althttpd_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"); + nOut += althttpd_printf("Content-length: 0\r\n"); + nOut += althttpd_printf("\r\n"); MakeLogEntry(0, lineno); } fflush(stdout); } @@ -846,11 +1006,11 @@ /* ** 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){ +static void Decode64(char *z64){ char *zData; int n64; int i, j; int a, b, c, d; static int isInit = 0; @@ -887,10 +1047,181 @@ zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03); } zData[j] = 0; } +#ifdef ENABLE_TLS +/* This is a self-signed cert in the PEM format that can be used when +** no other certs are available. +** +** NB: Use of this self-signed cert is wildly insecure. Use for testing +** purposes only. +*/ +static const char sslSelfCert[] = +"-----BEGIN CERTIFICATE-----\n" +"MIIDMTCCAhkCFGrDmuJkkzWERP/ITBvzwwI2lv0TMA0GCSqGSIb3DQEBCwUAMFQx\n" +"CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzESMBAGA1UEBwwJQ2hhcmxvdHRlMRMw\n" +"EQYDVQQKDApGb3NzaWwtU0NNMQ8wDQYDVQQDDAZGb3NzaWwwIBcNMjExMjI3MTEz\n" +"MTU2WhgPMjEyMTEyMjcxMTMxNTZaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJO\n" +"QzESMBAGA1UEBwwJQ2hhcmxvdHRlMRMwEQYDVQQKDApGb3NzaWwtU0NNMQ8wDQYD\n" +"VQQDDAZGb3NzaWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCCbTU2\n" +"6GRQHQqLq7vyZ0OxpAxmgfAKCxt6eIz+jBi2ZM/CB5vVXWVh2+SkSiWEA3UZiUqX\n" +"xZlzmS/CglZdiwLLDJML8B4OiV72oivFH/vJ7+cbvh1dTxnYiHuww7GfQngPrLfe\n" +"fiIYPDk1GTUJHBQ7Ue477F7F8vKuHdVgwktF/JDM6M60aSqlo2D/oysirrb+dlur\n" +"Tlv0rjsYOfq6bLAajoL3qi/vek6DNssoywbge4PfbTgS9g7Gcgncbcet5pvaS12J\n" +"avhFcd4JU4Ity49Hl9S/C2MfZ1tE53xVggRwKz4FPj65M5uymTdcxtjKXtCxIE1k\n" +"KxJxXQh7rIYjm+RTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFkdtpqcybAzJN8G\n" +"+ONuUm5sXNbWta7JGvm8l0BTSBcCUtJA3hn16iJqXA9KmLnaF2denC4EYk+KlVU1\n" +"QXxskPJ4jB8A5B05jMijYv0nzCxKhviI8CR7GLEEGKzeg9pbW0+O3vaVehoZtdFX\n" +"z3SsCssr9QjCLiApQxMzW1Iv3od2JXeHBwfVMFrWA1VCEUCRs8OSW/VOqDPJLVEi\n" +"G6wxc4kN9dLK+5S29q3nzl24/qzXoF8P9Re5KBCbrwaHgy+OEEceq5jkmfGFxXjw\n" +"pvVCNry5uAhH5NqbXZampUWqiWtM4eTaIPo7Y2mDA1uWhuWtO6F9PsnFJlQHCnwy\n" +"s/TsrXk=\n" +"-----END CERTIFICATE-----\n"; + +/* This is the private-key corresponding to the cert above +*/ +static const char sslSelfPKey[] = +"-----BEGIN PRIVATE KEY-----\n" +"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCCbTU26GRQHQqL\n" +"q7vyZ0OxpAxmgfAKCxt6eIz+jBi2ZM/CB5vVXWVh2+SkSiWEA3UZiUqXxZlzmS/C\n" +"glZdiwLLDJML8B4OiV72oivFH/vJ7+cbvh1dTxnYiHuww7GfQngPrLfefiIYPDk1\n" +"GTUJHBQ7Ue477F7F8vKuHdVgwktF/JDM6M60aSqlo2D/oysirrb+dlurTlv0rjsY\n" +"Ofq6bLAajoL3qi/vek6DNssoywbge4PfbTgS9g7Gcgncbcet5pvaS12JavhFcd4J\n" +"U4Ity49Hl9S/C2MfZ1tE53xVggRwKz4FPj65M5uymTdcxtjKXtCxIE1kKxJxXQh7\n" +"rIYjm+RTAgMBAAECggEANfTH1vc8yIe7HRzmm9lsf8jF+II4s2705y2H5qY+cvYx\n" +"nKtZJGOG1X0KkYy7CGoFv5K0cSUl3lS5FVamM/yWIzoIex/Sz2C1EIL2aI5as6ez\n" +"jB6SN0/J+XI8+Vt7186/rHxfdIPpxuzjHbxX3HTpScETNWcLrghbrPxakbTPPxwt\n" +"+x7QlPmmkFNuMfvkzToFf9NdwL++44TeBPOpvD/Lrw+eyqdth9RJPq9cM96plh9V\n" +"HuRqeD8+QNafaXBdSQs3FJK/cDK/vWGKZWIfFVSDbDhwYljkXGijreFjtXQfkkpF\n" +"rl1J87/H9Ee7z8fTD2YXQHl+0/rghAVtac3u54dpQQKBgQC2XG3OEeMrOp9dNkUd\n" +"F8VffUg0ecwG+9L3LCe7U71K0kPmXjV6xNnuYcNQu84kptc5vI8wD23p29LaxdNc\n" +"9m0lcw06/YYBOPkNphcHkINYZTvVJF10mL3isymzMaTtwDkZUkOjL1B+MTiFT/qp\n" +"ARKrTYGJ4HxY7+tUkI5pUmg4PQKBgQC3GA4d1Rz3Pb/RRpcsZgWknKsKhoN36mSn\n" +"xFJ3wPBvVv2B1ltTMzh/+the0ty6clzMrvoLERzRcheDsNrc/j/TUVG8sVdBYJwX\n" +"tMZyFW4NVMOErT/1ukh6jBqIMBo6NJL3EV/AKj0yniksgKOr0/AAduAccnGST8Jd\n" +"SHOdjwvHzwKBgGZBq/zqgNTDuYseHGE07CMgcDWkumiMGv8ozlq3mSR0hUiPOTPP\n" +"YFjQjyIdPXnF6FfiyPPtIvgIoNK2LVAqiod+XUPf152l4dnqcW13dn9BvOxGyPTR\n" +"lWCikFaAFviOWjY9r9m4dU1dslDmySqthFd0TZgPvgps9ivkJ0cdw30NAoGAMC/E\n" +"h1VvKiK2OP27C5ROJ+STn1GHiCfIFd81VQ8SODtMvL8NifgRBp2eFFaqgOdYRQZI\n" +"CGGYlAbS6XXCJCdF5Peh62dA75PdgN+y2pOJQzjrvB9cle9Q4++7i9wdCvSLOTr5\n" +"WDnFoWy+qVexu6crovOmR9ZWzYrwPFy1EOJ010ECgYBl7Q+jmjOSqsVwhFZ0U7LG\n" +"diN+vXhWfn1wfOWd8u79oaqU/Oy7xyKW2p3H5z2KFrBM/vib53Lh4EwFZjcX+jVG\n" +"krAmbL+M/hP7z3TD2UbESAzR/c6l7FU45xN84Lsz5npkR8H/uAHuqLgb9e430Mjx\n" +"YNMwdb8rChHHChNZu6zuxw==\n" +"-----END PRIVATE KEY-----\n"; + +/* +** Read a PEM certificate from memory and push it into an SSL_CTX. +** Return the number of errors. +*/ +static int sslctx_use_cert_from_mem( + SSL_CTX *ctx, + const char *pData, + int nData +){ + BIO *in; + int rc = 1; + X509 *x = 0; + X509 *cert = 0; + + in = BIO_new_mem_buf(pData, nData); + if( in==0 ) goto end_of_ucfm; + x = X509_new(); + if( x==0 ) goto end_of_ucfm; + cert = PEM_read_bio_X509(in, &x, 0, 0); + if( cert==0 ) goto end_of_ucfm; + rc = SSL_CTX_use_certificate(ctx, x)<=0; +end_of_ucfm: + X509_free(x); + BIO_free(in); + return rc; +} + +/* +** Read a PEM private key from memory and add it to an SSL_CTX. +** Return the number of errors. +*/ +static int sslctx_use_pkey_from_mem( + SSL_CTX *ctx, + const char *pData, + int nData +){ + int rc = 1; + BIO *in; + EVP_PKEY *pkey = 0; + + in = BIO_new_mem_buf(pData, nData); + if( in==0 ) goto end_of_upkfm; + pkey = PEM_read_bio_PrivateKey(in, 0, 0, 0); + if( pkey==0 ) goto end_of_upkfm; + rc = SSL_CTX_use_PrivateKey(ctx, pkey)<=0; + EVP_PKEY_free(pkey); +end_of_upkfm: + BIO_free(in); + return rc; +} + +/* +** Initialize the SSL library so that it is able to handle +** server-side connections. Invokes Malfunction() if there are +** any problems (so does not return on error). +** +** If zKeyFile and zCertFile are not NULL, then they are the names +** of disk files that hold the certificate and private-key for the +** server. If zCertFile is not NULL but zKeyFile is NULL, then +** zCertFile is assumed to be a concatenation of the certificate and +** the private-key in the PEM format. +** +** If zCertFile is NULL or empty then a built-in self-signed cert is +** used. +** +** Error messages may contain the paths to the given files, but this +** function is called before the server starts listening for requests, +** so those will never be sent to clients. +*/ +static void ssl_init_server(const char *zCertFile, + const char *zKeyFile){ + if( tlsState.isInit==0 ){ + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + tlsState.ctx = SSL_CTX_new(SSLv23_server_method()); + if( tlsState.ctx==0 ){ + ERR_print_errors_fp(stderr); + Malfunction(500,"Error initializing the SSL server"); + } + if( zCertFile && zCertFile[0] ){ + if( SSL_CTX_use_certificate_chain_file(tlsState.ctx, + zCertFile)!=1 ){ + ERR_print_errors_fp(stderr); + Malfunction(500,"Error loading CERT file \"%s\"", + zCertFile); + } + if( zKeyFile==0 ) zKeyFile = zCertFile; + if( SSL_CTX_use_PrivateKey_file(tlsState.ctx, zKeyFile, + SSL_FILETYPE_PEM)<=0 ){ + ERR_print_errors_fp(stderr); + Malfunction(500,"Error loading PRIVATE KEY from file \"%s\"", + zKeyFile); + } + }else if( sslctx_use_cert_from_mem(tlsState.ctx, sslSelfCert, -1) + || sslctx_use_pkey_from_mem(tlsState.ctx, sslSelfPKey, -1) ){ + Malfunction(500,"Error loading self-signed CERT"); + } + if( !SSL_CTX_check_private_key(tlsState.ctx) ){ + Malfunction(500,"PRIVATE KEY \"%s\" does not match CERT \"%s\"", + zKeyFile, zCertFile); + } + SSL_CTX_set_mode(tlsState.ctx, SSL_MODE_AUTO_RETRY); + tlsState.isInit = 2; + }else{ + assert( tlsState.isInit==2 ); + } +} +#endif /*ENABLE_TLS*/ + /* ** 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. ** @@ -1262,14 +1593,132 @@ int n = 0; while( *z ) if( *(z++)=='/' ) n++; return n; } + +#ifdef ENABLE_TLS +/* +** Create a new server-side codec. The argument is the socket's file +** descriptor from which the codec reads and writes. The returned +** memory must eventually be passed to tls_close_server(). +*/ +static void *tls_new_server(int iSocket){ + TlsServerConn *pServer = malloc(sizeof(*pServer)); + BIO *b = pServer ? BIO_new_socket(iSocket, 0) : NULL; + if( NULL==b ){ + Malfunction(500,"Cannot allocate TlsServerConn."); + } + assert(NULL!=tlsState.ctx); + pServer->ssl = SSL_new(tlsState.ctx); + pServer->atEof = 0; + pServer->iSocket = iSocket; + SSL_set_bio(pServer->ssl, b, b); + SSL_accept(pServer->ssl); + return (void*)pServer; +} + +/* +** Close a server-side code previously returned from tls_new_server(). +*/ +static void tls_close_server(void *pServerArg){ + TlsServerConn *pServer = (TlsServerConn*)pServerArg; + SSL_free(pServer->ssl); + memset(pServer, 0, sizeof(TlsServerConn)); + free(pServer); +} + +static void tls_atexit(void){ + if( tlsState.sslCon ){ + tls_close_server(tlsState.sslCon); + tlsState.sslCon = NULL; + } +} +#endif /* ENABLE_TLS */ + +/* +** Works like fgets(): +** +** Read a single line of input into s[]. Ensure that s[] is zero-terminated. +** The s[] buffer is size bytes and so at most size-1 bytes will be read. +** +** Return a pointer to s[] on success, or NULL at end-of-input. +** +** If in TLS mode, the final argument is ignored and the TLS +** connection is read instead. +*/ +static char *althttpd_fgets(char *s, int size, FILE *in){ + if( useHttps!=2 ){ + return fgets(s, size, in); + } +#ifdef ENABLE_TLS + assert(NULL!=tlsState.sslCon); + return tls_gets(tlsState.sslCon, s, size); +#else + Malfunction(500,"SSL not available"); + return NULL; +#endif +} +/* +** Works like fread() but may, depending on connection state, use +** libssl to read the data (in which case the final argument is +** ignored). The target buffer must be at least (sz*nmemb) bytes. +*/ +static size_t althttpd_fread(void *tgt, size_t sz, size_t nmemb, FILE *in){ + if( useHttps!=2 ){ + return fread(tgt, sz, nmemb, in); + } +#ifdef ENABLE_TLS + assert(NULL!=tlsState.sslCon); + return tls_read_server(tlsState.sslCon, tgt, sz*nmemb); +#else + Malfunction(500,"SSL not available"); + return 0; +#endif +} + +/* +** Works like fwrite() but may, depending on connection state, write to +** the active TLS connection (in which case the final argument is +** ignored). +** +*/ +static size_t althttpd_fwrite( + void const *src, /* Buffer containing content to write */ + size_t sz, /* Size of each element in the buffer */ + size_t nmemb, /* Number of elements to write */ + FILE *out /* Write on this stream */ +){ + if( useHttps!=2 ){ + return fwrite(src, sz, nmemb, out); + } +#ifdef ENABLE_TLS + assert(NULL!=tlsState.sslCon); + return tls_write_server(tlsState.sslCon, src, sz*nmemb); +#else + Malfunction(500,"SSL not available"); + return 0; +#endif +} + +/* +** In non-builtin-TLS mode, fflush()es the given FILE handle, else +** this is a no-op. +*/ +static void althttpd_fflush(FILE *f){ + if( useHttps!=2 ){ + fflush(f); + } +} + /* ** 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. +** +** When running in built-in TLS mode the 2nd argument is ignored and +** output is instead sent via the TLS connection. */ static void xferBytes(FILE *in, FILE *out, int nXfer, int nSkip){ size_t n; size_t got; char zBuf[16384]; @@ -1283,11 +1732,11 @@ 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); + althttpd_fwrite(zBuf, got, 1, out); nOut += got; nXfer -= got; } } @@ -1315,13 +1764,13 @@ && (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"); + nOut += althttpd_printf("Cache-Control: max-age=%d\r\n", mxAge); + nOut += althttpd_printf("ETag: \"%s\"\r\n", zETag); + nOut += althttpd_printf("\r\n"); fflush(stdout); MakeLogEntry(0, 470); /* LOG: ETag Cache Hit */ return 1; } in = fopen(zFile,"rb"); @@ -1329,48 +1778,66 @@ if( rangeEnd>0 && rangeStartst_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", + nOut += althttpd_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; charset=utf-8\r\n",zContentType); - nOut += printf("Content-length: %d\r\n\r\n",(int)pStat->st_size); + nOut += althttpd_printf("Cache-Control: max-age=%d\r\n", mxAge); + nOut += althttpd_printf("ETag: \"%s\"\r\n", zETag); + nOut += althttpd_printf("Content-type: %s; charset=utf-8\r\n",zContentType); + nOut += althttpd_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 - { + if( 2!=useHttps ){ off_t offset = rangeStart; nOut += sendfile(fileno(stdout), fileno(in), &offset, pStat->st_size); - } -#else - xferBytes(in, stdout, (int)pStat->st_size, rangeStart); + }else #endif + { + xferBytes(in, stdout, (int)pStat->st_size, rangeStart); + } fclose(in); return 0; } + +/* +** Streams all contents from in to out. If in TLS mode, the +** output stream is ignored and the output instead goes +** to the TLS channel. +*/ +static void stream_file(FILE * const in, FILE * const out){ + enum { STREAMBUF_SIZE = 1024 * 4 }; + char streamBuf[STREAMBUF_SIZE]; + size_t n; + while( (n = fread(streamBuf, 1,sizeof(STREAMBUF_SIZE),in)) ){ + althttpd_fwrite(streamBuf, 1, n, out); + } +} /* ** 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. +** +** If isNPH is true, the input is assumed to be from a +** non-parsed-header CGI and is passed on as-is to stdout or the TLS +** layer, depending on the connection state. */ -static void CgiHandleReply(FILE *in){ +static void CgiHandleReply(FILE *in, int isNPH){ 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 */ @@ -1383,22 +1850,34 @@ /* Disable the timeout, so that we can implement Hanging-GET or ** long-poll style CGIs. The RLIMIT_CPU will serve as a safety ** to help prevent a run-away CGI */ alarm(0); } + + if( isNPH ){ + /* + ** Non-parsed-header output: simply pipe it out as-is. We + ** need to go through this routine, instead of simply exec()'ing, + ** in order to go through the TLS output channel. + */ + stream_file(in, stdout); + fclose(in); + return; + } + 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); + nOut += althttpd_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]); + nOut += althttpd_printf("%s %s", zProtocol, &zLine[i]); strncpy(zReplyStatus, &zLine[i], 3); zReplyStatus[3] = 0; iStatus = atoi(zReplyStatus); if( iStatus!=200 ) rangeEnd = 0; statusSent = 1; @@ -1416,34 +1895,33 @@ } memcpy(aRes+nRes, zLine, nLine); nRes += nLine; } } - /* Copy everything else thru without change or analysis. */ if( rangeEnd>0 && seenContentLength && rangeStart=contentLength ){ rangeEnd = contentLength-1; } - nOut += printf("Content-Range: bytes %d-%d/%d\r\n", - rangeStart, rangeEnd, contentLength); + nOut += althttpd_printf("Content-Range: bytes %d-%d/%d\r\n", + rangeStart, rangeEnd, contentLength); contentLength = rangeEnd + 1 - rangeStart; }else{ StartResponse("200 OK"); } if( nRes>0 ){ aRes[nRes] = 0; - printf("%s", aRes); + althttpd_fwrite(aRes, nRes, 1, stdout); nOut += nRes; nRes = 0; } if( iStatus==304 ){ - nOut += printf("\r\n\r\n"); + nOut += althttpd_printf("\r\n\r\n"); }else if( seenContentLength ){ - nOut += printf("Content-length: %d\r\n\r\n", contentLength); + nOut += althttpd_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; @@ -1454,13 +1932,14 @@ } aRes[nRes++] = c; } if( nRes ){ aRes[nRes] = 0; - nOut += printf("Content-length: %d\r\n\r\n%s", (int)nRes, aRes); + nOut += althttpd_printf("Content-length: %d\r\n\r\n", (int)nRes); + nOut += althttpd_fwrite(aRes, nRes, 1, stdout); }else{ - nOut += printf("Content-length: 0\r\n\r\n"); + nOut += althttpd_printf("Content-length: 0\r\n\r\n"); } } free(aRes); fclose(in); } @@ -1607,18 +2086,46 @@ 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); - } + stream_file(in, s); fclose(in); } fflush(s); - CgiHandleReply(s); + CgiHandleReply(s, 0); +} + +/* +** If running in builtin TLS mode, initializes the SSL I/O +** state and returns 1, else does nothing and returns 0. +*/ +static int tls_init_conn(int iSocket){ +#ifdef ENABLE_TLS + if( 2==useHttps ){ + /*assert(NULL==tlsState.sslCon);*/ + if( NULL==tlsState.sslCon ){ + tlsState.sslCon = (TlsServerConn *)tls_new_server(iSocket); + if( NULL==tlsState.sslCon ){ + Malfunction(500,"Could not instantiate TLS context."); + } + atexit(tls_atexit); + } + return 1; + } +#else + if( 0==iSocket ){/*unused arg*/} +#endif + return 0; +} +static void tls_close_conn(void){ +#ifdef ENABLE_TLS + if( tlsState.sslCon ){ + tls_close_server(tlsState.sslCon); + tlsState.sslCon = NULL; + } +#endif } /* ** This routine processes a single HTTP request on standard input and ** sends the reply to standard output. If the argument is 1 it means @@ -1629,12 +2136,16 @@ ** 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. +** +** socketId must be 0 (if running via xinetd/etc) or the socket ID +** accept()ed by http_server(). It is only used for built-in TLS +** mode. */ -void ProcessOneRequest(int forceClose){ +void ProcessOneRequest(int forceClose, int socketId){ 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 @@ -1646,14 +2157,17 @@ */ if( chdir(zRoot[0] ? zRoot : "/")!=0 ){ char zBuf[1000]; Malfunction(190, /* LOG: chdir() failed */ "cannot chdir to [%s] from [%s]", - zRoot, getcwd(zBuf,999)); + zRoot, getcwd(zBuf,sizeof(zBuf)-1)); } nRequest++; - + if( tls_init_conn(socketId) ){ + /* Never(?) reuse TLS connections? */ + forceClose = 1; + } /* ** We must receive a complete header within 15 seconds */ signal(SIGALRM, Timeout); signal(SIGSEGV, Timeout); @@ -1662,11 +2176,11 @@ 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 ){ + if( althttpd_fgets(zLine,sizeof(zLine),stdin)==0 ){ exit(0); } gettimeofday(&beginTime, 0); omitLog = 0; nIn += strlen(zLine); @@ -1675,11 +2189,11 @@ 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( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\r\n" "\r\n" "This server does not understand the requested protocol\n" ); MakeLogEntry(0, 200); /* LOG: bad protocol in HTTP header */ @@ -1700,11 +2214,11 @@ ** and HEAD methods */ if( strcmp(zMethod,"GET")!=0 && strcmp(zMethod,"POST")!=0 && strcmp(zMethod,"HEAD")!=0 ){ StartResponse("501 Not Implemented"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\r\n" "\r\n" "The %s method is not implemented on this server.\n", zMethod); MakeLogEntry(0, 220); /* LOG: Unknown request method */ @@ -1736,11 +2250,11 @@ zRemoteUser = 0; zReferer = 0; zIfNoneMatch = 0; zIfModifiedSince = 0; rangeEnd = 0; - while( fgets(zLine,sizeof(zLine),stdin) ){ + while( althttpd_fgets(zLine,sizeof(zLine),stdin) ){ char *zFieldName; char *zVal; #ifdef LOG_HEADER if( hdrLog ) fprintf(hdrLog, "%s", zLine); @@ -1899,11 +2413,11 @@ char *zBuf; int n; if( len>MAX_CONTENT_LENGTH ){ StartResponse("500 Request too large"); - nOut += printf( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\r\n" "\r\n" "Too much POST data\n" ); MakeLogEntry(0, 270); /* LOG: Request too large */ @@ -1917,21 +2431,21 @@ "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( + nOut += althttpd_printf( "Content-type: text/plain; charset=utf-8\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); + n = althttpd_fread(zBuf,1,len,stdin); nIn += n; fwrite(zBuf,1,n,out); free(zBuf); fclose(out); } @@ -2008,11 +2522,10 @@ 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 */ @@ -2040,10 +2553,11 @@ 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 */ + tls_close_conn(); return; }else{ j--; } } @@ -2076,10 +2590,11 @@ /* 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 / */ + tls_close_conn(); return; } break; } zLine[j] = zScript[i]; @@ -2098,11 +2613,14 @@ /* 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; + if( access(zLine,R_OK)==0 && !CheckBasicAuthorization(zLine) ){ + tls_close_conn(); + return; + } /* Take appropriate action */ if( (statbuf.st_mode & 0100)==0100 && access(zFile,X_OK)==0 ){ char *zBaseFilename; /* Filename without directory prefix */ @@ -2148,31 +2666,25 @@ ** 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"); + "Unable to duplicate file descriptor 0"); } close(0); + } + if( zTmpNam ){ + /* Becomes the stdin of our upcoming CGI process. */ 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. + /* Fall thru to here for the NPH (non-parsed-headers) case and 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 */ @@ -2195,11 +2707,11 @@ in = fdopen(px[0], "rb"); } if( in==0 ){ CgiError(); }else{ - CgiHandleReply(in); + CgiHandleReply(in, strncmp(zBaseFilename,"nph-",4)==0); } }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 @@ -2209,16 +2721,20 @@ }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 it isn't executable then it must be a simple file that needs + ** to be copied to output. */ - if( SendFile(zFile, lenFile, &statbuf) ) return; + if( SendFile(zFile, lenFile, &statbuf) ){ + tls_close_conn(); + return; + } } - fflush(stdout); + tls_close_conn(); + althttpd_fflush(stdout); MakeLogEntry(0, 0); /* LOG: Normal reply */ /* The next request must arrive within 30 seconds or we close the connection */ omitLog = 1; @@ -2245,12 +2761,15 @@ ** 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. +** +** When it accept()s a connection, the socket ID is written to the +** final argument. */ -int http_server(const char *zPort, int localOnly){ +int http_server(const char *zPort, int localOnly, int * httpConnection){ 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 */ @@ -2265,14 +2784,14 @@ int maxFd = -1; memset(&sHints, 0, sizeof(sHints)); if( ipv4Only ){ sHints.ai_family = PF_INET; - /*printf("ipv4 only\n");*/ + /*althttpd_printf("ipv4 only\n");*/ }else if( ipv6Only ){ sHints.ai_family = PF_INET6; - /*printf("ipv6 only\n");*/ + /*althttpd_printf("ipv6 only\n");*/ }else{ sHints.ai_family = PF_UNSPEC; } sHints.ai_socktype = SOCK_STREAM; sHints.ai_flags = AI_PASSIVE; @@ -2347,10 +2866,11 @@ if( fd!=0 ) nErr++; close(1); fd = dup(connection); if( fd!=1 ) nErr++; close(connection); + *httpConnection = fd; return nErr; } } } /* Bury dead children */ @@ -2362,27 +2882,27 @@ } /* NOT REACHED */ exit(1); } - -int main(int argc, char **argv){ +int main(int argc, const char **argv){ int i; /* Loop counter */ - char *zPermUser = 0; /* Run daemon with this user's permissions */ + const 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 */ + int httpConnection = 0; /* Socket ID of inbound http connection */ /* 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"; + const char *z = argv[1]; + const 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; @@ -2391,17 +2911,22 @@ }else if( strcmp(z,"-max-age")==0 ){ mxAge = atoi(zArg); }else if( strcmp(z,"-max-cpu")==0 ){ maxCpu = atoi(zArg); }else if( strcmp(z,"-https")==0 ){ - useHttps = atoi(zArg); - zHttp = useHttps ? "https" : "http"; - if( useHttps ) zRemoteAddr = getenv("REMOTE_HOST"); + int const x = atoi(zArg); + if( x<=0 ){ + useHttps = 0; + zHttp = "http"; + }else{ + zHttp = "https"; + zRemoteAddr = getenv("REMOTE_HOST"); + useHttps = 1; + } }else if( strcmp(z, "-port")==0 ){ zPort = zArg; - standalone = 1; - + standalone = 1 + (useHttps==2); }else if( strcmp(z, "-family")==0 ){ if( strcmp(zArg, "ipv4")==0 ){ ipv4Only = 1; }else if( strcmp(zArg, "ipv6")==0 ){ ipv6Only = 1; @@ -2420,11 +2945,33 @@ }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 ){ + } +#ifdef ENABLE_TLS + else if( strcmp(z, "-cert")==0 ){ + useHttps = 2; + zHttp = "https"; + tlsState.zCertFile = zArg; + if( tlsState.zKeyFile==0 ) tlsState.zKeyFile = zArg; + if( standalone ){ + standalone = 2; + if( 0==zPort ) zPort = "443"; + } + }else if( strcmp(z, "-pkey")==0 ){ + tlsState.zKeyFile = zArg; + }else if( strcmp(z, "-tls")==0 && atoi(zArg) ){ + useHttps = 2; + zHttp = "https"; + if( standalone ){ + standalone = 2; + if( 0==zPort ) zPort = "443"; + } + } +#endif + else if( strcmp(z, "-datetest")==0 ){ TestParseRfc822Date(); printf("Ok\n"); exit(0); }else{ Malfunction(510, /* LOG: unknown command-line argument on launch */ @@ -2439,11 +2986,21 @@ }else{ Malfunction(520, /* LOG: --root argument missing */ "no --root specified"); } } - + +#if ENABLE_TLS + /* We "need" to read the cert before chroot'ing to allow that the + ** cert is stored in space outside of the --root and not readable by + ** the --user. + */ + if( useHttps>1 ){ + ssl_init_server(tlsState.zCertFile, tlsState.zKeyFile); + } +#endif + /* Change directories to the root of the HTTP filesystem. Then ** create a chroot jail there. */ if( chdir(zRoot)!=0 ){ Malfunction(530, /* LOG: chdir() failed */ @@ -2462,11 +3019,11 @@ zRoot = ""; } } /* Activate the server, if requested */ - if( zPort && http_server(zPort, 0) ){ + if( zPort && http_server(zPort, 0, &httpConnection) ){ Malfunction(550, /* LOG: server startup failed */ "failed to start server"); } #ifdef RLIMIT_CPU @@ -2519,14 +3076,16 @@ ){ zRemoteAddr += 7; } /* Process the input stream */ - for(i=0; i<100; i++){ - ProcessOneRequest(0); + if( useHttps!=2 ){ + for(i=0; i<100; i++){ + ProcessOneRequest(0, httpConnection); + } } - ProcessOneRequest(1); + ProcessOneRequest(1, httpConnection); exit(0); } #if 0 /* Copy/paste the following text into SQLite to generate the xref Index: althttpd.md ================================================================== --- althttpd.md +++ althttpd.md @@ -23,11 +23,12 @@ wholly focused on serving that one connection. A single althttpd process will handle one or more HTTP requests over the same connection. When the connection closes, the althttpd process exits. Althttpd can also operate stand-alone. Althttpd -itself listens on port 80 for incoming HTTP requests, then forks +itself listens on port 80 for incoming HTTP requests (or 443 for +incoming HTTPS requests), then forks a copy of itself to handle each inbound connection. Each connection is still handled using a separate process. The only difference is that the connection-handler process is now started by a master althttpd instance rather than by xinetd or stunnel4. @@ -34,21 +35,27 @@ Althttpd has no configuration file. All configuration is handled using a few command-line arguments. This helps to keep the configuration simple and mitigates worries about about introducing a security vulnerability through a misconfigured web server. -Althttpd does not itself handle TLS connections. For HTTPS, althttpd -relies on stunnel4 to handle TLS protocol negotiation, decryption, and -encryption. - Because each althttpd process only needs to service a single connection, althttpd is single threaded. Furthermore, each process only lives for the duration of a single connection, which means that althttpd does not need to worry too much about memory leaks. These design factors help keep the althttpd source code simple, which facilitates security auditing and analysis. +For serving TLS connections there are two options: + +1. althttpd can be built with the `ENABLE_TLS` macro defined and linked to +`-lssl -lcrypto`, then started with the `--cert fullchain.pem` and +`--pkey privkey.pem` flags. + +2. althttpd can be started via an external connection service such as +stunnel4, passing the `-https 1` flag to althttpd to tell it that it is +"indirectly" operating in HTTPS mode via that service. + Source Code ----------- The complete source code for althttpd is contained within a single @@ -59,10 +66,17 @@ > gcc -Os -o /usr/bin/althttpd althttpd.c The althttpd source code is heavily commented and accessible. It should be relatively easy to customize for specialized needs. + +To build althttpd with built-in TLS support using libssl: + +> + gcc -Os -o /usr/bin/althttpd -fPIC -DENABLE_SSL -lssl -lcrypto \ + althttpd.c + Setup Using Xinetd ------------------ Shown below is the complete text of the /etc/xinetd.d/http file on @@ -216,10 +230,38 @@ mode, listening on port 8080. The author of althttpd has only ever used stand-alone mode for testing. Since althttpd does not itself support TLS encryption, the stunnel4 setup is preferred for production websites. + +Stand-alone with HTTPS +---------------------- + +If althttpd is built with TLS support then it can be told to operate +in HTTPS mode with one of the following options: + +> + althttpd -root ~/www --port 8043 -tls 1 + +this option uses a compiled-in self-signed SSL certificate +**which is wildly insecure** and is intended for testing purposes. +only. Use the --cert option to specify your own PEM-format SSL +certificate. The argument to --cert can be the concatenation of +the SSL private key (often named "privkey.pem") and the certificate +chain (often named "fullchain.pem"). Alternatively, the --cert +can point to just the fullchain.pem file and the separate --pkey +option can point to the privkey.pem file. + +Start althttpd with: + +> + althttpd -root ~/www --port 8043 --cert fullchain.pem --pkey privkey.pem + +Note that the certificate is read before althttpd drops root +privileges, so the certificate may live somewhere inaccessible to +the non-root user under which the althttpd process will run. + Security Features ----------------- To defend against mischief, there are restrictions on names of files that