diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/cgi.c | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/cgi.c')
| -rw-r--r-- | src/cgi.c | 3105 |
1 files changed, 3105 insertions, 0 deletions
diff --git a/src/cgi.c b/src/cgi.c new file mode 100644 index 0000000..443b4df --- /dev/null +++ b/src/cgi.c @@ -0,0 +1,3105 @@ +/* +** Copyright (c) 2006 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file began as a set of C functions and procedures used to interpret +** CGI environment variables for Fossil web pages that were invoked by +** CGI. That's where the file name comes from. But over the years it +** has grown to incorporate lots of related functionality, including: +** +** * Interpreting CGI environment variables when Fossil is run as +** CGI (the original purpose). +** +** * Interpreting HTTP requests received directly or via an SSH tunnel. +** +** * Interpreting SCGI requests +** +** * Generating appropriate replies to CGI, SCGI, and HTTP requests. +** +** * Listening for incoming HTTP requests and dispatching them. +** (Used by "fossil ui" and "fossil server", for example). +** +** So, even though the name of this file implies that it only deals with +** CGI, in fact, the code in this file is used to interpret webpage requests +** received by a variety of means, and to generate well-formatted replies +** to those requests. +** +** The code in this file abstracts the web-request so that downstream +** modules that generate the body of the reply (based on the requested page) +** do not need to know if the request is coming from CGI, direct HTTP, +** SCGI, or some other means. +** +** This module gathers information about web page request into a key/value +** store. Keys and values come from: +** +** * Query parameters +** * POST parameter +** * Cookies +** * Environment variables +** +** The parameters are accessed using cgi_parameter() and similar functions +** or their convenience macros P() and similar. +** +** Environment variable parameters are set as if the request were coming +** in over CGI even if the request arrived via SCGI or direct HTTP. Thus +** the downstream modules that are trying to interpret the request do not +** need to know the request protocol - they can just request the values +** of environment variables and everything will always work. +** +** This file contains routines used by Fossil when it is acting as a +** CGI client. For the code used by Fossil when it is acting as a +** CGI server (for the /ext webpage) see the "extcgi.c" source file. +*/ +#include "config.h" +#ifdef _WIN32 +# if !defined(_WIN32_WINNT) +# define _WIN32_WINNT 0x0501 +# endif +# include <winsock2.h> +# include <ws2tcpip.h> +#else +# include <sys/socket.h> +# include <sys/un.h> +# include <netinet/in.h> +# include <netdb.h> +# include <arpa/inet.h> +# include <sys/times.h> +# include <sys/time.h> +# include <sys/wait.h> +# include <sys/select.h> +# include <errno.h> +#endif +#ifdef __EMX__ + typedef int socklen_t; +#endif +#include <time.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <assert.h> +#include "cgi.h" +#include "cygsup.h" + +#if INTERFACE +/* +** Shortcuts for cgi_parameter. P("x") returns the value of query parameter +** or cookie "x", or NULL if there is no such parameter or cookie. PD("x","y") +** does the same except "y" is returned in place of NULL if there is not match. +*/ +#define P(x) cgi_parameter((x),0) +#define PD(x,y) cgi_parameter((x),(y)) +#define PT(x) cgi_parameter_trimmed((x),0) +#define PDT(x,y) cgi_parameter_trimmed((x),(y)) +#define PB(x) cgi_parameter_boolean(x) +#define PCK(x) cgi_parameter_checked(x,1) +#define PIF(x,y) cgi_parameter_checked(x,y) +#define P_NoBot(x) cgi_parameter_no_attack((x),0) +#define PD_NoBot(x,y) cgi_parameter_no_attack((x),(y)) + +/* +** Shortcut for the cgi_printf() routine. Instead of using the +** +** @ ... +** +** notation provided by the translate.c utility, you can also +** optionally use: +** +** CX(...) +*/ +#define CX cgi_printf + +/* +** Destinations for output text. +*/ +#define CGI_HEADER 0 +#define CGI_BODY 1 + +/* +** Flags for SSH HTTP clients +*/ +#define CGI_SSH_CLIENT 0x0001 /* Client is SSH */ +#define CGI_SSH_COMPAT 0x0002 /* Compat for old SSH transport */ +#define CGI_SSH_FOSSIL 0x0004 /* Use new Fossil SSH transport */ + +#endif /* INTERFACE */ + +/* +** The reply content is generated in two pieces: the header and the body. +** These pieces are generated separately because they are not necessarily +** produced in order. Parts of the header might be built after all or +** part of the body. The header and body are accumulated in separate +** Blob structures then output sequentially once everything has been +** built. +** +** Do not confuse the content header with the HTTP header. The content header +** is generated by downstream code. The HTTP header is generated by the +** cgi_reply() routine below. +** +** The content header and content body are *approximately* the <head> +** element and the <body> elements for HTML replies. However this is only +** approximate. The content header also includes parts of <body> that +** show the banner and menu bar at the top of each page. Also note that +** not all replies are HTML, but there can still be separate header and +** body sections of the content. +** +** The cgi_destination() interface switches between the buffers. +*/ +static Blob cgiContent[2] = { BLOB_INITIALIZER, BLOB_INITIALIZER }; +static Blob *pContent = &cgiContent[0]; + +/* +** Set the destination buffer into which to accumulate CGI content. +*/ +void cgi_destination(int dest){ + switch( dest ){ + case CGI_HEADER: { + pContent = &cgiContent[0]; + break; + } + case CGI_BODY: { + pContent = &cgiContent[1]; + break; + } + default: { + cgi_panic("bad destination"); + } + } +} + +/* +** Check to see if the content header or body contains the zNeedle string. +** Return true if it does and false if it does not. +*/ +int cgi_header_contains(const char *zNeedle){ + return strstr(blob_str(&cgiContent[0]), zNeedle)!=0; +} +int cgi_body_contains(const char *zNeedle){ + return strstr(blob_str(&cgiContent[1]), zNeedle)!=0; +} + +/* +** Append new reply content to what already exists. +*/ +void cgi_append_content(const char *zData, int nAmt){ + blob_append(pContent, zData, nAmt); +} + +/* +** Reset both reply content buffers to be empty. +*/ +void cgi_reset_content(void){ + blob_reset(&cgiContent[0]); + blob_reset(&cgiContent[1]); +} + +/* +** Return a pointer to Blob that is currently accumulating reply content. +*/ +Blob *cgi_output_blob(void){ + return pContent; +} + +/* +** Return the content header as a text string +*/ +const char *cgi_header(void){ + return blob_str(&cgiContent[0]); +} + +/* +** Combine the header and body content all into the header buffer. +** In other words, append the body content to the end of the header +** content. +*/ +static void cgi_combine_header_and_body(void){ + int size = blob_size(&cgiContent[1]); + if( size>0 ){ + blob_append(&cgiContent[0], blob_buffer(&cgiContent[1]), size); + blob_reset(&cgiContent[1]); + } +} + +/* +** Return a pointer to the combined header+body content. +*/ +char *cgi_extract_content(void){ + cgi_combine_header_and_body(); + return blob_buffer(&cgiContent[0]); +} + +/* +** Additional information used to form the HTTP reply +*/ +static const char *zReplyMimeType = "text/html"; /* Content type of the reply */ +static const char *zReplyStatus = "OK"; /* Reply status description */ +static int iReplyStatus = 200; /* Reply status code */ +static Blob extraHeader = BLOB_INITIALIZER; /* Extra header text */ +static int rangeStart = 0; /* Start of Range: */ +static int rangeEnd = 0; /* End of Range: plus 1 */ + +/* +** Set the reply content type. +** +** The reply content type defaults to "text/html". It only needs to be +** changed (by calling this routine) in the exceptional case where some +** other content type is being returned. +*/ +void cgi_set_content_type(const char *zType){ + int i; + for(i=0; zType[i]>='+' && zType[i]<='z'; i++){} + zReplyMimeType = fossil_strndup(zType, i); +} + +/* +** Erase any existing reply content. Replace is with a pNewContent. +** +** This routine erases pNewContent. In other words, it move pNewContent +** into the content buffer. +*/ +void cgi_set_content(Blob *pNewContent){ + cgi_reset_content(); + cgi_destination(CGI_HEADER); + cgiContent[0] = *pNewContent; + blob_zero(pNewContent); +} + +/* +** Set the reply status code +*/ +void cgi_set_status(int iStat, const char *zStat){ + zReplyStatus = fossil_strdup(zStat); + iReplyStatus = iStat; +} + +/* +** Append text to the content header buffer. +*/ +void cgi_append_header(const char *zLine){ + blob_append(&extraHeader, zLine, -1); +} +void cgi_printf_header(const char *zLine, ...){ + va_list ap; + va_start(ap, zLine); + blob_vappendf(&extraHeader, zLine, ap); + va_end(ap); +} + +/* +** Set a cookie by queuing up the appropriate HTTP header output. If +** !g.isHTTP, this is a no-op. +** +** Zero lifetime implies a session cookie. A negative one expires +** the cookie immediately. +*/ +void cgi_set_cookie( + const char *zName, /* Name of the cookie */ + const char *zValue, /* Value of the cookie. Automatically escaped */ + const char *zPath, /* Path cookie applies to. NULL means "/" */ + int lifetime /* Expiration of the cookie in seconds from now */ +){ + char const *zSecure = ""; + if(!g.isHTTP) return /* e.g. JSON CLI mode, where g.zTop is not set */; + else if( zPath==0 ){ + zPath = g.zTop; + if( zPath[0]==0 ) zPath = "/"; + } + if( g.zBaseURL!=0 && fossil_strncmp(g.zBaseURL, "https:", 6)==0 ){ + zSecure = " secure;"; + } + if( lifetime!=0 ){ + blob_appendf(&extraHeader, + "Set-Cookie: %s=%t; Path=%s; max-age=%d; HttpOnly; %s\r\n", + zName, lifetime>0 ? zValue : "null", zPath, lifetime, zSecure); + }else{ + blob_appendf(&extraHeader, + "Set-Cookie: %s=%t; Path=%s; HttpOnly; %s\r\n", + zName, zValue, zPath, zSecure); + } +} + + +/* +** Return true if the response should be sent with Content-Encoding: gzip. +*/ +static int is_gzippable(void){ + if( g.fNoHttpCompress ) return 0; + if( strstr(PD("HTTP_ACCEPT_ENCODING", ""), "gzip")==0 ) return 0; + /* Maintenance note: this oddball structure is intended to make + ** adding new mimetypes to this list less of a performance hit than + ** doing a strcmp/glob over a growing set of compressible types. */ + switch(zReplyMimeType ? *zReplyMimeType : 0){ + case (int)'a': + if(0==fossil_strncmp("application/",zReplyMimeType,12)){ + const char * z = &zReplyMimeType[12]; + switch(*z){ + case (int)'j': + return fossil_strcmp("javascript", z)==0 + || fossil_strcmp("json", z)==0; + case (int)'w': return fossil_strcmp("wasm", z)==0; + case (int)'x': + return fossil_strcmp("x-tcl", z)==0 + || fossil_strcmp("x-tar", z)==0; + default: + return sqlite3_strglob("*xml", z)==0; + } + } + break; + case (int)'i': + return fossil_strcmp(zReplyMimeType, "image/svg+xml")==0 + || fossil_strcmp(zReplyMimeType, "image/vnd.microsoft.icon")==0; + case (int)'t': + return fossil_strncmp(zReplyMimeType, "text/", 5)==0; + } + return 0; +} + + +/* +** The following routines read or write content from/to the wire for +** an HTTP request. Depending on settings the content might be coming +** from or going to a socket, or a file, or it might come from or go +** to an SSL decoder/encoder. +*/ +/* +** 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. +*/ +static char *cgi_fgets(char *s, int size){ + if( !g.httpUseSSL ){ + return fgets(s, size, g.httpIn); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_gets(g.httpSSLConn, s, size); +#else + fossil_fatal("SSL not available"); +#endif +} + +/* Works like fread(): +** +** Read as many as bytes of content as we can, up to a maximum of nmemb +** bytes. Return the number of bytes read. Return 0 if there is no +** further input or if an I/O error occurs. +*/ +size_t cgi_fread(void *ptr, size_t nmemb){ + if( !g.httpUseSSL ){ + return fread(ptr, 1, nmemb, g.httpIn); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_read_server(g.httpSSLConn, ptr, nmemb, 1); +#else + fossil_fatal("SSL not available"); + /* NOT REACHED */ + return 0; +#endif +} + +/* Works like feof(): +** +** Return true if end-of-input has been reached. +*/ +int cgi_feof(void){ + if( !g.httpUseSSL ){ + return feof(g.httpIn); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_eof(g.httpSSLConn); +#else + return 1; +#endif +} + +/* Works like fwrite(): +** +** Try to output nmemb bytes of content. Return the number of +** bytes actually written. +*/ +static size_t cgi_fwrite(void *ptr, size_t nmemb){ + if( !g.httpUseSSL ){ + return fwrite(ptr, 1, nmemb, g.httpOut); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_write_server(g.httpSSLConn, ptr, nmemb); +#else + fossil_fatal("SSL not available"); +#endif +} + +/* Works like fflush(): +** +** Make sure I/O has completed. +*/ +static void cgi_fflush(void){ + if( !g.httpUseSSL ){ + fflush(g.httpOut); + } +} + +/* +** Given a Content-Type value, returns a string suitable for appending +** to the Content-Type header for adding (or not) the "; charset=..." +** part. It returns an empty string for most types or if zReplyMimeType +** is NULL. +** +** See forum post f60dece061c364d1 for the discussions which lead to +** this. Previously we always appended the charset, but WASM loaders +** are pedantic and refuse to load any responses which have a +** charset. Also, adding a charset is not strictly appropriate for +** most types (and not required for many others which may ostensibly +** benefit from one, as detailed in that forum post). +*/ +static const char * content_type_charset(const char *zReplyMimeType){ + if(0==fossil_strncmp(zReplyMimeType,"text/",5)){ + return "; charset=utf-8"; + } + return ""; +} + +/* +** Generate the reply to a web request. The output might be an +** full HTTP response, or a CGI response, depending on how things have +** be set up. +** +** The reply consists of a response header (an HTTP or CGI response header) +** followed by the concatenation of the content header and content body. +*/ +void cgi_reply(void){ + Blob hdr = BLOB_INITIALIZER; + int total_size; + if( iReplyStatus<=0 ){ + iReplyStatus = 200; + zReplyStatus = "OK"; + } + + if( g.fullHttpReply ){ + if( rangeEnd>0 + && iReplyStatus==200 + && fossil_strcmp(P("REQUEST_METHOD"),"GET")==0 + ){ + iReplyStatus = 206; + zReplyStatus = "Partial Content"; + } + blob_appendf(&hdr, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); + blob_appendf(&hdr, "Date: %s\r\n", cgi_rfc822_datestamp(time(0))); + blob_appendf(&hdr, "Connection: close\r\n"); + blob_appendf(&hdr, "X-UA-Compatible: IE=edge\r\n"); + }else{ + assert( rangeEnd==0 ); + blob_appendf(&hdr, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); + } + if( etag_tag()[0]!=0 + && iReplyStatus==200 + && strcmp(zReplyMimeType,"text/html")!=0 + ){ + /* Do not cache HTML replies as those will have been generated and + ** will likely, therefore, contains a nonce and we want that nonce to + ** be different every time. */ + blob_appendf(&hdr, "ETag: \"%s\"\r\n", etag_tag()); + blob_appendf(&hdr, "Cache-Control: max-age=%d\r\n", etag_maxage()); + if( etag_mtime()>0 ){ + blob_appendf(&hdr, "Last-Modified: %s\r\n", + cgi_rfc822_datestamp(etag_mtime())); + } + }else if( g.isConst ){ + /* isConst means that the reply is guaranteed to be invariant, even + ** after configuration changes and/or Fossil binary recompiles. */ + blob_appendf(&hdr, "Cache-Control: max-age=315360000, immutable\r\n"); + }else{ + blob_appendf(&hdr, "Cache-control: no-cache\r\n"); + } + + if( blob_size(&extraHeader)>0 ){ + blob_appendf(&hdr, "%s", blob_buffer(&extraHeader)); + } + + /* Add headers to turn on useful security options in browsers. */ + blob_appendf(&hdr, "X-Frame-Options: SAMEORIGIN\r\n"); + /* The previous stops fossil pages appearing in frames or iframes, preventing + ** click-jacking attacks on supporting browsers. + ** + ** Other good headers would be + ** Strict-Transport-Security: max-age=62208000 + ** if we're using https. However, this would break sites which serve different + ** content on http and https protocols. Also, + ** X-Content-Security-Policy: allow 'self' + ** would help mitigate some XSS and data injection attacks, but will break + ** deliberate inclusion of external resources, such as JavaScript syntax + ** highlighter scripts. + ** + ** These headers are probably best added by the web server hosting fossil as + ** a CGI script. + */ + + if( iReplyStatus!=304 ) { + blob_appendf(&hdr, "Content-Type: %s%s\r\n", zReplyMimeType, + content_type_charset(zReplyMimeType)); + if( fossil_strcmp(zReplyMimeType,"application/x-fossil")==0 ){ + cgi_combine_header_and_body(); + blob_compress(&cgiContent[0], &cgiContent[0]); + } + + if( is_gzippable() && iReplyStatus!=206 ){ + int i; + gzip_begin(0); + for( i=0; i<2; i++ ){ + int size = blob_size(&cgiContent[i]); + if( size>0 ) gzip_step(blob_buffer(&cgiContent[i]), size); + blob_reset(&cgiContent[i]); + } + gzip_finish(&cgiContent[0]); + blob_appendf(&hdr, "Content-Encoding: gzip\r\n"); + blob_appendf(&hdr, "Vary: Accept-Encoding\r\n"); + } + total_size = blob_size(&cgiContent[0]) + blob_size(&cgiContent[1]); + if( iReplyStatus==206 ){ + blob_appendf(&hdr, "Content-Range: bytes %d-%d/%d\r\n", + rangeStart, rangeEnd-1, total_size); + total_size = rangeEnd - rangeStart; + } + blob_appendf(&hdr, "Content-Length: %d\r\n", total_size); + }else{ + total_size = 0; + } + blob_appendf(&hdr, "\r\n"); + cgi_fwrite(blob_buffer(&hdr), blob_size(&hdr)); + blob_reset(&hdr); + if( total_size>0 + && iReplyStatus!=304 + && fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0 + ){ + int i, size; + for(i=0; i<2; i++){ + size = blob_size(&cgiContent[i]); + if( size<=rangeStart ){ + rangeStart -= size; + }else{ + int n = size - rangeStart; + if( n>total_size ){ + n = total_size; + } + cgi_fwrite(blob_buffer(&cgiContent[i])+rangeStart, n); + rangeStart = 0; + total_size -= n; + } + } + } + cgi_fflush(); + CGIDEBUG(("-------- END cgi ---------\n")); + + /* After the webpage has been sent, do any useful background + ** processing. + */ + g.cgiOutput = 2; + if( g.db!=0 && iReplyStatus==200 ){ + backoffice_check_if_needed(); + } +} + +/* +** Generate an HTTP or CGI redirect response that causes a redirect +** to the URL given in the argument. +** +** The URL must be relative to the base of the fossil server. +*/ +NORETURN void cgi_redirect_with_status( + const char *zURL, + int iStat, + const char *zStat +){ + char *zLocation; + CGIDEBUG(("redirect to %s\n", zURL)); + if( fossil_strncmp(zURL,"http:",5)==0 + || fossil_strncmp(zURL,"https:",6)==0 ){ + zLocation = mprintf("Location: %s\r\n", zURL); + }else if( *zURL=='/' ){ + int n1 = (int)strlen(g.zBaseURL); + int n2 = (int)strlen(g.zTop); + if( g.zBaseURL[n1-1]=='/' ) zURL++; + zLocation = mprintf("Location: %.*s%s\r\n", n1-n2, g.zBaseURL, zURL); + }else{ + zLocation = mprintf("Location: %s/%s\r\n", g.zBaseURL, zURL); + } + cgi_append_header(zLocation); + cgi_reset_content(); + cgi_printf("<html>\n<p>Redirect to %h</p>\n</html>\n", zLocation); + cgi_set_status(iStat, zStat); + free(zLocation); + cgi_reply(); + fossil_exit(0); +} +NORETURN void cgi_redirect_perm(const char *zURL){ + cgi_redirect_with_status(zURL, 301, "Moved Permanently"); +} +NORETURN void cgi_redirect(const char *zURL){ + cgi_redirect_with_status(zURL, 302, "Moved Temporarily"); +} +NORETURN void cgi_redirect_with_method(const char *zURL){ + cgi_redirect_with_status(zURL, 307, "Temporary Redirect"); +} +NORETURN void cgi_redirectf(const char *zFormat, ...){ + va_list ap; + va_start(ap, zFormat); + cgi_redirect(vmprintf(zFormat, ap)); + va_end(ap); +} + +/* +** Add a "Content-disposition: attachment; filename=%s" header to the reply. +*/ +void cgi_content_disposition_filename(const char *zFilename){ + char *z; + int i, n; + + /* 0123456789 123456789 123456789 123456789 123456*/ + z = mprintf("Content-Disposition: attachment; filename=\"%s\";\r\n", + file_tail(zFilename)); + n = (int)strlen(z); + for(i=43; i<n-4; i++){ + char c = z[i]; + if( fossil_isalnum(c) ) continue; + if( c=='.' || c=='-' || c=='/' ) continue; + z[i] = '_'; + } + cgi_append_header(z); + fossil_free(z); +} + +/* +** Return the URL for the caller. This is obtained from either the +** "referer" CGI parameter, if it exists, or the HTTP_REFERER HTTP parameter. +** If neither exist, return zDefault. +*/ +const char *cgi_referer(const char *zDefault){ + const char *zRef = P("referer"); + if( zRef==0 ){ + zRef = P("HTTP_REFERER"); + if( zRef==0 ) zRef = zDefault; + } + return zRef; +} + + +/* +** Return true if the current request is coming from the same origin. +** +** If the request comes from a different origin and bErrorLog is true, then +** put a warning message on the error log as this was a possible hack +** attempt. +*/ +int cgi_same_origin(int bErrorLog){ + const char *zRef; + char *zToFree = 0; + int nBase; + int rc; + if( g.zBaseURL==0 ) return 0; + zRef = P("HTTP_REFERER"); + if( zRef==0 ) return 0; + if( strchr(zRef,'%')!=0 ){ + zToFree = strdup(zRef); + dehttpize(zToFree); + zRef = zToFree; + } + nBase = (int)strlen(g.zBaseURL); + if( fossil_strncmp(g.zBaseURL,zRef,nBase)!=0 ){ + rc = 0; + }else if( zRef[nBase]!=0 && zRef[nBase]!='/' ){ + rc = 0; + }else{ + rc = 1; + } + if( rc==0 && bErrorLog && fossil_strcmp(P("REQUST_METHOD"),"POST")==0 ){ + fossil_errorlog("warning: POST from different origin"); + } + fossil_free(zToFree); + return rc; +} + +/* +** Return true if the current CGI request is a POST request +*/ +static int cgi_is_post_request(void){ + const char *zMethod = P("REQUEST_METHOD"); + if( zMethod==0 ) return 0; + if( strcmp(zMethod,"POST")!=0 ) return 0; + return 1; +} + +/* +** Return true if the current request appears to be safe from a +** Cross-Site Request Forgery (CSRF) attack. The level of checking +** is determined by the parameter. The higher the number, the more +** secure we are: +** +** 0: Request must come from the same origin +** 1: Same origin and must be a POST request +** 2: All of the above plus must have a valid CSRF token +** +** Results are cached in the g.okCsrf variable. The g.okCsrf value +** has meaning as follows: +** +** -1: Not a secure request +** 0: Status unknown +** 1: Request comes from the same origin +** 2: (1) plus it is a POST request +** 3: (2) plus there is a valid "csrf" token in the request +*/ +int cgi_csrf_safe(int securityLevel){ + if( g.okCsrf<0 ) return 0; + if( g.okCsrf==0 ){ + if( !cgi_same_origin(1) ){ + g.okCsrf = -1; + }else{ + g.okCsrf = 1; + if( cgi_is_post_request() ){ + g.okCsrf = 2; + if( fossil_strcmp(P("csrf"), g.zCsrfToken)==0 ){ + g.okCsrf = 3; + } + } + } + } + return g.okCsrf >= (securityLevel+1); +} + +/* +** Verify that CSRF defenses are maximal - that the request comes from +** the same origin, that it is a POST request, and that there is a valid +** "csrf" token. If this is not the case, fail immediately. +*/ +void cgi_csrf_verify(void){ + if( !cgi_csrf_safe(2) ){ + fossil_fatal("Cross-site Request Forgery detected"); + } +} + +/* +** Information about all query parameters, post parameter, cookies and +** CGI environment variables are stored in a hash table as follows: +*/ +static int nAllocQP = 0; /* Space allocated for aParamQP[] */ +static int nUsedQP = 0; /* Space actually used in aParamQP[] */ +static int sortQP = 0; /* True if aParamQP[] needs sorting */ +static int seqQP = 0; /* Sequence numbers */ +static struct QParam { /* One entry for each query parameter or cookie */ + const char *zName; /* Parameter or cookie name */ + const char *zValue; /* Value of the query parameter or cookie */ + int seq; /* Order of insertion */ + char isQP; /* True for query parameters */ + char cTag; /* Tag on query parameters */ + char isFetched; /* 1 if the var is requested via P/PD() */ +} *aParamQP; /* An array of all parameters and cookies */ + +/* +** Add another query parameter or cookie to the parameter set. +** zName is the name of the query parameter or cookie and zValue +** is its fully decoded value. +** +** zName and zValue are not copied and must not change or be +** deallocated after this routine returns. +*/ +void cgi_set_parameter_nocopy(const char *zName, const char *zValue, int isQP){ + if( nAllocQP<=nUsedQP ){ + nAllocQP = nAllocQP*2 + 10; + if( nAllocQP>1000 ){ + /* Prevent a DOS service attack against the framework */ + fossil_fatal("Too many query parameters"); + } + aParamQP = fossil_realloc( aParamQP, nAllocQP*sizeof(aParamQP[0]) ); + } + aParamQP[nUsedQP].zName = zName; + aParamQP[nUsedQP].zValue = zValue; + if( g.fHttpTrace ){ + fprintf(stderr, "# cgi: %s = [%s]\n", zName, zValue); + } + aParamQP[nUsedQP].seq = seqQP++; + aParamQP[nUsedQP].isQP = isQP; + aParamQP[nUsedQP].cTag = 0; + aParamQP[nUsedQP].isFetched = 0; + nUsedQP++; + sortQP = 1; +} + +/* +** Add another query parameter or cookie to the parameter set. +** zName is the name of the query parameter or cookie and zValue +** is its fully decoded value. zName will be modified to be an +** all lowercase string. +** +** zName and zValue are not copied and must not change or be +** deallocated after this routine returns. This routine changes +** all ASCII alphabetic characters in zName to lower case. The +** caller must not change them back. +*/ +void cgi_set_parameter_nocopy_tolower( + char *zName, + const char *zValue, + int isQP +){ + int i; + for(i=0; zName[i]; i++){ zName[i] = fossil_tolower(zName[i]); } + cgi_set_parameter_nocopy(zName, zValue, isQP); +} + +/* +** Add another query parameter or cookie to the parameter set. +** zName is the name of the query parameter or cookie and zValue +** is its fully decoded value. +** +** Copies are made of both the zName and zValue parameters. +*/ +void cgi_set_parameter(const char *zName, const char *zValue){ + cgi_set_parameter_nocopy(fossil_strdup(zName),fossil_strdup(zValue), 0); +} +void cgi_set_query_parameter(const char *zName, const char *zValue){ + cgi_set_parameter_nocopy(fossil_strdup(zName),fossil_strdup(zValue), 1); +} + +/* +** Replace a parameter with a new value. +*/ +void cgi_replace_parameter(const char *zName, const char *zValue){ + int i; + for(i=0; i<nUsedQP; i++){ + if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){ + aParamQP[i].zValue = zValue; + return; + } + } + cgi_set_parameter_nocopy(zName, zValue, 0); +} +void cgi_replace_query_parameter(const char *zName, const char *zValue){ + int i; + for(i=0; i<nUsedQP; i++){ + if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){ + aParamQP[i].zValue = zValue; + assert( aParamQP[i].isQP ); + return; + } + } + cgi_set_parameter_nocopy(zName, zValue, 1); +} +void cgi_replace_query_parameter_tolower(char *zName, const char *zValue){ + int i; + for(i=0; zName[i]; i++){ zName[i] = fossil_tolower(zName[i]); } + cgi_replace_query_parameter(zName, zValue); +} + +/* +** Delete a parameter. +*/ +void cgi_delete_parameter(const char *zName){ + int i; + for(i=0; i<nUsedQP; i++){ + if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){ + --nUsedQP; + if( i<nUsedQP ){ + memmove(aParamQP+i, aParamQP+i+1, sizeof(*aParamQP)*(nUsedQP-i)); + } + return; + } + } +} +void cgi_delete_query_parameter(const char *zName){ + int i; + for(i=0; i<nUsedQP; i++){ + if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){ + assert( aParamQP[i].isQP ); + --nUsedQP; + if( i<nUsedQP ){ + memmove(aParamQP+i, aParamQP+i+1, sizeof(*aParamQP)*(nUsedQP-i)); + } + return; + } + } +} + +/* +** Return the number of query parameters. Cookies and environment variables +** do not count. Also, do not count the special QP "name". +*/ +int cgi_qp_count(void){ + int cnt = 0; + int i; + for(i=0; i<nUsedQP; i++){ + if( aParamQP[i].isQP && fossil_strcmp(aParamQP[i].zName,"name")!=0 ) cnt++; + } + return cnt; +} + +/* +** Add an environment variable value to the parameter set. The zName +** portion is fixed but a copy is made of zValue. +*/ +void cgi_setenv(const char *zName, const char *zValue){ + cgi_set_parameter_nocopy(zName, fossil_strdup(zValue), 0); +} + +/* +** Returns true if NUL-terminated z contains any non-NUL +** control characters (<0x20, 32d). +*/ +static int contains_ctrl(const char *zIn){ + const unsigned char *z = (const unsigned char*)zIn; + assert(z); + for( ; *z>=0x20; ++z ){} + return 0!=*z; +} + +/* +** Add a list of query parameters or cookies to the parameter set. +** +** Each parameter is of the form NAME=VALUE. Both the NAME and the +** VALUE may be url-encoded ("+" for space, "%HH" for other special +** characters). But this routine assumes that NAME contains no +** special character and therefore does not decode it. +** +** If NAME begins with something other than a lower-case letter then +** the entire NAME=VALUE term is ignored. Hence: +** +** * cookies and query parameters that have uppercase names +** are ignored. +** +** * it is impossible for a cookie or query parameter to +** override the value of an environment variable since +** environment variables always have uppercase names. +** +** 2018-03-29: Also ignore the entry if NAME that contains any characters +** other than [-a-zA-Z0-9_]. There are no known exploits involving unusual +** names that contain characters outside that set, but it never hurts to +** be extra cautious when sanitizing inputs. +** +** Parameters are separated by the "terminator" character. Whitespace +** before the NAME is ignored. +** +** The input string "z" is modified but no copies is made. "z" +** should not be deallocated or changed again after this routine +** returns or it will corrupt the parameter table. +** +** If bPermitCtrl is false and the decoded value of any entry in z +** contains control characters (<0x20, 32d) then that key/value pair +** are skipped. +*/ +static void add_param_list(char *z, int terminator, int bPermitCtrl){ + int isQP = terminator=='&'; + while( *z ){ + char *zName; + char *zValue; + while( fossil_isspace(*z) ){ z++; } + zName = z; + while( *z && *z!='=' && *z!=terminator ){ z++; } + if( *z=='=' ){ + *z = 0; + z++; + zValue = z; + while( *z && *z!=terminator ){ z++; } + if( *z ){ + *z = 0; + z++; + } + dehttpize(zValue); + }else{ + if( *z ){ *z++ = 0; } + zValue = ""; + } + if( zName[0] && fossil_no_strange_characters(zName+1) ){ + if( 0==bPermitCtrl && contains_ctrl(zValue) ){ + continue /* Reject it. An argument could be made + ** for break instead of continue. */; + }else if( fossil_islower(zName[0]) ){ + cgi_set_parameter_nocopy(zName, zValue, isQP); + }else if( fossil_isupper(zName[0]) ){ + cgi_set_parameter_nocopy_tolower(zName, zValue, isQP); + } + } +#ifdef FOSSIL_ENABLE_JSON + json_setenv( zName, cson_value_new_string(zValue,strlen(zValue)) ); +#endif /* FOSSIL_ENABLE_JSON */ + } +} + +/* +** *pz is a string that consists of multiple lines of text. This +** routine finds the end of the current line of text and converts +** the "\n" or "\r\n" that ends that line into a "\000". It then +** advances *pz to the beginning of the next line and returns the +** previous value of *pz (which is the start of the current line.) +*/ +static char *get_line_from_string(char **pz, int *pLen){ + char *z = *pz; + int i; + if( z[0]==0 ) return 0; + for(i=0; z[i]; i++){ + if( z[i]=='\n' ){ + if( i>0 && z[i-1]=='\r' ){ + z[i-1] = 0; + }else{ + z[i] = 0; + } + i++; + break; + } + } + *pz = &z[i]; + *pLen -= i; + return z; +} + +/* +** The input *pz points to content that is terminated by a "\r\n" +** followed by the boundary marker zBoundary. An extra "--" may or +** may not be appended to the boundary marker. There are *pLen characters +** in *pz. +** +** This routine adds a "\000" to the end of the content (overwriting +** the "\r\n") and returns a pointer to the content. The *pz input +** is adjusted to point to the first line following the boundary. +** The length of the content is stored in *pnContent. +*/ +static char *get_bounded_content( + char **pz, /* Content taken from here */ + int *pLen, /* Number of bytes of data in (*pz)[] */ + char *zBoundary, /* Boundary text marking the end of content */ + int *pnContent /* Write the size of the content here */ +){ + char *z = *pz; + int len = *pLen; + int i; + int nBoundary = strlen(zBoundary); + *pnContent = len; + for(i=0; i<len; i++){ + if( z[i]=='\n' && fossil_strncmp(zBoundary, &z[i+1], + nBoundary)==0 ){ + if( i>0 && z[i-1]=='\r' ) i--; + z[i] = 0; + *pnContent = i; + i += nBoundary; + break; + } + } + *pz = &z[i]; + get_line_from_string(pz, pLen); + return z; +} + +/* +** Tokenize a line of text into as many as nArg tokens. Make +** azArg[] point to the start of each token. +** +** Tokens consist of space or semi-colon delimited words or +** strings inside double-quotes. Example: +** +** content-disposition: form-data; name="fn"; filename="index.html" +** +** The line above is tokenized as follows: +** +** azArg[0] = "content-disposition:" +** azArg[1] = "form-data" +** azArg[2] = "name=" +** azArg[3] = "fn" +** azArg[4] = "filename=" +** azArg[5] = "index.html" +** azArg[6] = 0; +** +** '\000' characters are inserted in z[] at the end of each token. +** This routine returns the total number of tokens on the line, 6 +** in the example above. +*/ +static int tokenize_line(char *z, int mxArg, char **azArg){ + int i = 0; + while( *z ){ + while( fossil_isspace(*z) || *z==';' ){ z++; } + if( *z=='"' && z[1] ){ + *z = 0; + z++; + if( i<mxArg-1 ){ azArg[i++] = z; } + while( *z && *z!='"' ){ z++; } + if( *z==0 ) break; + *z = 0; + z++; + }else{ + if( i<mxArg-1 ){ azArg[i++] = z; } + while( *z && !fossil_isspace(*z) && *z!=';' && *z!='"' ){ z++; } + if( *z && *z!='"' ){ + *z = 0; + z++; + } + } + } + azArg[i] = 0; + return i; +} + +/* +** Scan the multipart-form content and make appropriate entries +** into the parameter table. +** +** The content string "z" is modified by this routine but it is +** not copied. The calling function must not deallocate or modify +** "z" after this routine finishes or it could corrupt the parameter +** table. +*/ +static void process_multipart_form_data(char *z, int len){ + char *zLine; + int nArg, i; + char *zBoundary; + char *zValue; + char *zName = 0; + int showBytes = 0; + char *azArg[50]; + + zBoundary = get_line_from_string(&z, &len); + if( zBoundary==0 ) return; + while( (zLine = get_line_from_string(&z, &len))!=0 ){ + if( zLine[0]==0 ){ + int nContent = 0; + zValue = get_bounded_content(&z, &len, zBoundary, &nContent); + if( zName && zValue ){ + if( fossil_islower(zName[0]) ){ + cgi_set_parameter_nocopy(zName, zValue, 1); + if( showBytes ){ + cgi_set_parameter_nocopy(mprintf("%s:bytes", zName), + mprintf("%d",nContent), 1); + } + }else if( fossil_isupper(zName[0]) ){ + cgi_set_parameter_nocopy_tolower(zName, zValue, 1); + if( showBytes ){ + cgi_set_parameter_nocopy_tolower(mprintf("%s:bytes", zName), + mprintf("%d",nContent), 1); + } + } + } + zName = 0; + showBytes = 0; + }else{ + nArg = tokenize_line(zLine, count(azArg), azArg); + for(i=0; i<nArg; i++){ + int c = fossil_tolower(azArg[i][0]); + int n = strlen(azArg[i]); + if( c=='c' && sqlite3_strnicmp(azArg[i],"content-disposition:",n)==0 ){ + i++; + }else if( c=='n' && sqlite3_strnicmp(azArg[i],"name=",n)==0 ){ + zName = azArg[++i]; + }else if( c=='f' && sqlite3_strnicmp(azArg[i],"filename=",n)==0 ){ + char *z = azArg[++i]; + if( zName && z ){ + if( fossil_islower(zName[0]) ){ + cgi_set_parameter_nocopy(mprintf("%s:filename",zName), z, 1); + }else if( fossil_isupper(zName[0]) ){ + cgi_set_parameter_nocopy_tolower(mprintf("%s:filename",zName), + z, 1); + } + } + showBytes = 1; + }else if( c=='c' && sqlite3_strnicmp(azArg[i],"content-type:",n)==0 ){ + char *z = azArg[++i]; + if( zName && z ){ + if( fossil_islower(zName[0]) ){ + cgi_set_parameter_nocopy(mprintf("%s:mimetype",zName), z, 1); + }else if( fossil_isupper(zName[0]) ){ + cgi_set_parameter_nocopy_tolower(mprintf("%s:mimetype",zName), + z, 1); + } + } + } + } + } + } +} + + +#ifdef FOSSIL_ENABLE_JSON +/* +** Reads a JSON object from the given blob, which is assumed to have +** been populated by the caller from stdin, the SSL API, or a file, as +** appropriate for the particular use case. On success g.json.post is +** updated to hold the content. On error a FSL_JSON_E_INVALID_REQUEST +** response is output and fossil_exit() is called (in HTTP mode exit +** code 0 is used). +*/ +void cgi_parse_POST_JSON( Blob * pIn ){ + cson_value * jv = NULL; + cson_parse_info pinfo = cson_parse_info_empty; + assert(g.json.gc.a && "json_bootstrap_early() was not called!"); + jv = cson_parse_Blob(pIn, &pinfo); + if( jv==NULL ){ + goto invalidRequest; + }else{ + json_gc_add( "POST.JSON", jv ); + g.json.post.v = jv; + g.json.post.o = cson_value_get_object( jv ); + if( !g.json.post.o ){ /* we don't support non-Object (Array) requests */ + goto invalidRequest; + } + } + return; + invalidRequest: + cgi_set_content_type(json_guess_content_type()); + if(0 != pinfo.errorCode){ /* fancy error message */ + char * msg = mprintf("JSON parse error at line %u, column %u, " + "byte offset %u: %s", + pinfo.line, pinfo.col, pinfo.length, + cson_rc_string(pinfo.errorCode)); + json_err( FSL_JSON_E_INVALID_REQUEST, msg, 1 ); + fossil_free(msg); + }else if(jv && !g.json.post.o){ + json_err( FSL_JSON_E_INVALID_REQUEST, + "Request envelope must be a JSON Object (not array).", 1 ); + }else{ /* generic error message */ + json_err( FSL_JSON_E_INVALID_REQUEST, NULL, 1 ); + } + fossil_exit( g.isHTTP ? 0 : 1); +} +#endif /* FOSSIL_ENABLE_JSON */ + +/* +** Log HTTP traffic to a file. Begin the log on first use. Close the log +** when the argument is NULL. +*/ +void cgi_trace(const char *z){ + static FILE *pLog = 0; + if( g.fHttpTrace==0 ) return; + if( z==0 ){ + if( pLog ) fclose(pLog); + pLog = 0; + return; + } + if( pLog==0 ){ + char zFile[50]; +#if defined(_WIN32) + unsigned r; + sqlite3_randomness(sizeof(r), &r); + sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%08x.txt", r); +#else + sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%05d.txt", getpid()); +#endif + pLog = fossil_fopen(zFile, "wb"); + if( pLog ){ + fprintf(stderr, "# open log on %s\n", zFile); + }else{ + fprintf(stderr, "# failed to open %s\n", zFile); + return; + } + } + fputs(z, pLog); +} + +/* Forward declaration */ +static NORETURN void malformed_request(const char *zMsg, ...); + +/* +** Checks the QUERY_STRING environment variable, sets it up via +** add_param_list() and, if found, applies its "skin" setting. Returns +** 0 if no QUERY_STRING is set, else it returns a bitmask of: +** +** 0x01 = QUERY_STRING was set up +** 0x02 = "skin" URL param arg was processed +** 0x04 = "x-f-l-c" cookie arg was processed. +** +* In the case of the skin, the cookie may still need flushing +** by the page, via cookie_render(). +*/ +int cgi_setup_query_string(void){ + int rc = 0; + char * z = (char*)P("QUERY_STRING"); + if( z ){ + rc = 0x01; + z = fossil_strdup(z); + add_param_list(z, '&', 0); + z = (char*)P("skin"); + if( z ){ + char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM); + rc |= 0x02; + if( !zErr && P("once")==0 ){ + cookie_write_parameter("skin","skin",z); + /* Per /chat discussion, passing ?skin=... without "once" + ** implies the "udc" argument, so we force that into the + ** environment here. */ + cgi_set_parameter_nocopy("udc", "1", 1); + } + fossil_free(zErr); + } + } + if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){ + /* x-f-l-c (X-Fossil-Login-Card) transmitted via cookie instead of + ** in the sync payload. The format of this value is the same as a + ** "login" card, as parsed by xfer.c:page_xfer(). */ + rc |= 0x04; + g.syncInfo.zLoginCard = fossil_strdup(z); + g.syncInfo.fLoginCardMode |= 0x02; + cgi_delete_parameter("x-f-l-c"); + } + return rc; +} + +/* +** Initialize the query parameter database. Information is pulled from +** the QUERY_STRING environment variable (if it exists), from standard +** input if there is POST data, and from HTTP_COOKIE. +** +** REQUEST_URI, PATH_INFO, and SCRIPT_NAME are related as follows: +** +** REQUEST_URI == SCRIPT_NAME + PATH_INFO +** +** Or if QUERY_STRING is not empty: +** +** REQUEST_URI == SCRIPT_NAME + PATH_INFO + '?' + QUERY_STRING +** +** Where "+" means concatenate. Fossil requires SCRIPT_NAME. If +** REQUEST_URI is provided but PATH_INFO is not, then PATH_INFO is +** computed from REQUEST_URI and SCRIPT_NAME. If PATH_INFO is provided +** but REQUEST_URI is not, then compute REQUEST_URI from PATH_INFO and +** SCRIPT_NAME. If neither REQUEST_URI nor PATH_INFO are provided, then +** assume that PATH_INFO is an empty string and set REQUEST_URI equal +** to PATH_INFO. +** +** Sometimes PATH_INFO is missing and SCRIPT_NAME is not a prefix of +** REQUEST_URI. (See https://fossil-scm.org/forum/forumpost/049e8650ed) +** In that case, truncate SCRIPT_NAME so that it is a proper prefix +** of REQUEST_URI. +** +** SCGI typically omits PATH_INFO. CGI sometimes omits REQUEST_URI and +** PATH_INFO when it is empty. +** +** CGI Parameter quick reference: +** +** REQUEST_URI +** _____________|________________ +** / \ +** https://fossil-scm.org/forum/info/12736b30c072551a?t=c +** \___/ \____________/\____/\____________________/ \_/ +** | | | | | +** | HTTP_HOST | PATH_INFO QUERY_STRING +** | | +** REQUEST_SCHEMA SCRIPT_NAME +** +*/ +void cgi_init(void){ + char *z; + const char *zType; + const char *zSemi; + int len; + const char *zRequestUri = cgi_parameter("REQUEST_URI",0); + const char *zScriptName = cgi_parameter("SCRIPT_NAME",0); + const char *zPathInfo = cgi_parameter("PATH_INFO",0); + const char *zContentLength = 0; +#ifdef _WIN32 + const char *zServerSoftware = cgi_parameter("SERVER_SOFTWARE",0); +#endif + +#ifdef FOSSIL_ENABLE_JSON + const int noJson = P("no_json")!=0; +#endif + g.isHTTP = 1; + cgi_destination(CGI_BODY); + + /* We must have SCRIPT_NAME. If the web server did not supply it, try + ** to compute it from REQUEST_URI and PATH_INFO. */ + if( zScriptName==0 ){ + if( zRequestUri==0 || zPathInfo==0 ){ + malformed_request("missing SCRIPT_NAME"); /* Does not return */ + } + z = (char*)strstr(zRequestUri,zPathInfo); + if( z==0 ){ + malformed_request("PATH_INFO not found in REQUEST_URI"); + } + zScriptName = fossil_strndup(zRequestUri,(int)(z-zRequestUri)); + cgi_set_parameter("SCRIPT_NAME", zScriptName); + } + +#ifdef _WIN32 + /* The Microsoft IIS web server does not define REQUEST_URI, instead it uses + ** PATH_INFO for virtually the same purpose. Define REQUEST_URI the same as + ** PATH_INFO and redefine PATH_INFO with SCRIPT_NAME removed from the + ** beginning. */ + if( zServerSoftware && strstr(zServerSoftware, "Microsoft-IIS") ){ + int i, j; + cgi_set_parameter("REQUEST_URI", zPathInfo); + for(i=0; zPathInfo[i]==zScriptName[i] && zPathInfo[i]; i++){} + for(j=i; zPathInfo[j] && zPathInfo[j]!='?'; j++){} + zPathInfo = fossil_strndup(zPathInfo+i, j-i); + cgi_replace_parameter("PATH_INFO", zPathInfo); + } +#endif + if( zRequestUri==0 ){ + const char *z = zPathInfo; + const char *zQS = cgi_parameter("QUERY_STRING",0); + if( zPathInfo==0 ){ + malformed_request("missing PATH_INFO and/or REQUEST_URI"); + } + if( z[0]=='/' ) z++; + if( zQS && zQS[0] ){ + zRequestUri = mprintf("%s/%s?%s", zScriptName, z, zQS); + }else{ + zRequestUri = mprintf("%s/%s", zScriptName, z); + } + cgi_set_parameter("REQUEST_URI", zRequestUri); + } + if( zPathInfo==0 ){ + int i, j; + for(i=0; zRequestUri[i]==zScriptName[i] && zRequestUri[i]; i++){} + for(j=i; zRequestUri[j] && zRequestUri[j]!='?'; j++){} + zPathInfo = fossil_strndup(zRequestUri+i, j-i); + cgi_set_parameter_nocopy("PATH_INFO", zPathInfo, 0); + if( j>i && zScriptName[i]!=0 ){ + /* If SCRIPT_NAME is not a prefix of REQUEST_URI, truncate it so + ** that it is. See https://fossil-scm.org/forum/forumpost/049e8650ed + */ + char *zNew = fossil_strndup(zScriptName, i); + cgi_replace_parameter("SCRIPT_NAME", zNew); + } + } +#ifdef FOSSIL_ENABLE_JSON + if(noJson==0 && json_request_is_json_api(zPathInfo)){ + /* We need to change some following behaviour depending on whether + ** we are operating in JSON mode or not. We cannot, however, be + ** certain whether we should/need to be in JSON mode until the + ** PATH_INFO is set up. + */ + g.json.isJsonMode = 1; + json_bootstrap_early(); + }else{ + assert(!g.json.isJsonMode && + "Internal misconfiguration of g.json.isJsonMode"); + } +#endif + z = (char*)P("HTTP_COOKIE"); + if( z ){ + z = fossil_strdup(z); + add_param_list(z, ';', 0); + z = (char*)cookie_value("skin",0); + if(z){ + skin_use_alternative(z, 2, SKIN_FROM_COOKIE); + } + } + + cgi_setup_query_string(); + + z = (char*)P("REMOTE_ADDR"); + if( z ){ + g.zIpAddr = fossil_strdup(z); + } + + zContentLength = P("CONTENT_LENGTH"); + if( zContentLength==0 ){ + len = 0; + if( sqlite3_stricmp(PD("REQUEST_METHOD",""),"POST")==0 ){ + malformed_request("missing CONTENT_LENGTH on a POST method"); + } + }else{ + len = atoi(zContentLength); + } + zType = P("CONTENT_TYPE"); + zSemi = zType ? strchr(zType, ';') : 0; + if( zSemi ){ + g.zContentType = fossil_strndup(zType, (int)(zSemi-zType)); + zType = g.zContentType; + }else{ + g.zContentType = zType; + } + blob_zero(&g.cgiIn); + if( len>0 && zType ){ + if( blob_read_from_cgi(&g.cgiIn, len)<len ){ + char *zMsg = mprintf("CGI content-length mismatch: Wanted %d bytes" + " but got only %d\n", len, blob_size(&g.cgiIn)); + malformed_request(zMsg); + } + if( fossil_strcmp(zType, "application/x-fossil")==0 ){ + blob_uncompress(&g.cgiIn, &g.cgiIn); + } +#ifdef FOSSIL_ENABLE_JSON + if( noJson==0 && g.json.isJsonMode!=0 + && json_can_consume_content_type(zType)!=0 ){ + cgi_parse_POST_JSON(&g.cgiIn); + cgi_set_content_type(json_guess_content_type()); + } +#endif /* FOSSIL_ENABLE_JSON */ + } +} + +/* +** Decode POST parameter information in the cgiIn content, if any. +*/ +void cgi_decode_post_parameters(void){ + int len = blob_size(&g.cgiIn); + if( len==0 ) return; + if( fossil_strcmp(g.zContentType,"application/x-www-form-urlencoded")==0 + || fossil_strncmp(g.zContentType,"multipart/form-data",19)==0 + ){ + char *z = blob_str(&g.cgiIn); + cgi_trace(z); + if( g.zContentType[0]=='a' ){ + add_param_list(z, '&', 1); + }else{ + process_multipart_form_data(z, len); + } + blob_init(&g.cgiIn, 0, 0); + } +} + +/* +** This is the comparison function used to sort the aParamQP[] array of +** query parameters and cookies. +*/ +static int qparam_compare(const void *a, const void *b){ + struct QParam *pA = (struct QParam*)a; + struct QParam *pB = (struct QParam*)b; + int c; + c = fossil_strcmp(pA->zName, pB->zName); + if( c==0 ){ + c = pA->seq - pB->seq; + } + return c; +} + +/* +** Return the value of a query parameter or cookie whose name is zName. +** If there is no query parameter or cookie named zName and the first +** character of zName is uppercase, then check to see if there is an +** environment variable by that name and return it if there is. As +** a last resort when nothing else matches, return zDefault. +*/ +const char *cgi_parameter(const char *zName, const char *zDefault){ + int lo, hi, mid, c; + + /* The sortQP flag is set whenever a new query parameter is inserted. + ** It indicates that we need to resort the query parameters. + */ + if( sortQP ){ + int i, j; + qsort(aParamQP, nUsedQP, sizeof(aParamQP[0]), qparam_compare); + sortQP = 0; + /* After sorting, remove duplicate parameters. The secondary sort + ** key is aParamQP[].seq and we keep the first entry. That means + ** with duplicate calls to cgi_set_parameter() the second and + ** subsequent calls are effectively no-ops. */ + for(i=j=1; i<nUsedQP; i++){ + if( fossil_strcmp(aParamQP[i].zName,aParamQP[i-1].zName)==0 ){ + continue; + } + if( j<i ){ + memcpy(&aParamQP[j], &aParamQP[i], sizeof(aParamQP[j])); + } + j++; + } + nUsedQP = j; + } + + /* Invoking with a NULL zName is just a way to cause the parameters + ** to be sorted. So go ahead and bail out in that case */ + if( zName==0 || zName[0]==0 ) return 0; + + /* Do a binary search for a matching query parameter */ + lo = 0; + hi = nUsedQP-1; + while( lo<=hi ){ + mid = (lo+hi)/2; + c = fossil_strcmp(aParamQP[mid].zName, zName); + if( c==0 ){ + CGIDEBUG(("mem-match [%s] = [%s]\n", zName, aParamQP[mid].zValue)); + aParamQP[mid].isFetched = 1; + return aParamQP[mid].zValue; + }else if( c>0 ){ + hi = mid-1; + }else{ + lo = mid+1; + } + } + + /* If no match is found and the name begins with an upper-case + ** letter, then check to see if there is an environment variable + ** with the given name. + */ + if( fossil_isupper(zName[0]) ){ + const char *zValue = fossil_getenv(zName); + if( zValue ){ + cgi_set_parameter_nocopy(zName, zValue, 0); + CGIDEBUG(("env-match [%s] = [%s]\n", zName, zValue)); + return zValue; + } + } + CGIDEBUG(("no-match [%s]\n", zName)); + return zDefault; +} + +/* +** Return TRUE if the specific parameter exists and is a query parameter. +** Return FALSE if the parameter is a cookie or environment variable. +*/ +int cgi_is_qp(const char *zName){ + int i; + if( zName==0 || fossil_isupper(zName[0]) ) return 0; + for(i=0; i<nUsedQP; i++){ + if( fossil_strcmp(aParamQP[i].zName,zName)==0 ){ + return aParamQP[i].isQP; + } + } + return 0; +} + +/* +** Renders the "begone, spider" page and exits. +*/ +static void cgi_begone_spider(const char *zName){ + Blob content = empty_blob; + cgi_set_content(&content); + style_set_current_feature("test"); + style_submenu_enable(0); + style_header("Malicious Query Detected"); + @ <h2>Begone, Knave!</h2> + @ <p>This page was generated because Fossil detected an (unsuccessful) + @ SQL injection attack or other nefarious content in your HTTP request. + @ + @ <p>If you believe you are innocent and have reached this page in error, + @ contact the Fossil developers on the Fossil-SCM Forum. Type + @ "fossil-scm forum" into any search engine to locate the Fossil-SCM Forum. + style_finish_page(); + cgi_set_status(418,"I'm a teapot"); + cgi_reply(); + fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName); + exit(0); +} + +/* +** If looks_like_attack() returns true for the given string, call +** cgi_begone_spider() and does not return, else this function has no +** side effects. The range of checks performed by this function may +** be extended in the future. +** +** Checks are omitted for any logged-in user. +** +** This is the primary defense against attack. Fossil should easily be +** proof against SQL injection and XSS attacks even without this +** routine. Rather, this is an attempt to avoid denial-of-service caused +** by persistent spiders that hammer the server with dozens or hundreds of +** probes per seconds as they look for vulnerabilities. In other +** words, this is an effort to reduce the CPU load imposed by malicious +** spiders. Though those routine might help make attacks harder, it is +** not itself an impenetrably barrier against attack and should not be +** relied upon as the only defense. +*/ +void cgi_value_spider_check(const char *zTxt, const char *zName){ + if( g.zLogin==0 && looks_like_attack(zTxt) ){ + cgi_begone_spider(zName); + } +} + +/* +** A variant of cgi_parameter() with the same semantics except that if +** cgi_parameter(zName,zDefault) returns a value other than zDefault +** then it passes that value to cgi_value_spider_check(). +*/ +const char *cgi_parameter_no_attack(const char *zName, const char *zDefault){ + const char *zTxt = cgi_parameter(zName, zDefault); + + if( zTxt!=zDefault ){ + cgi_value_spider_check(zTxt, zName); + } + return zTxt; +} + +/* +** Return the value of the first defined query parameter or cookie whose +** name appears in the list of arguments. Or if no parameter is found, +** return NULL. +*/ +const char *cgi_coalesce(const char *zName, ...){ + va_list ap; + const char *z; + const char *zX; + if( zName==0 ) return 0; + z = cgi_parameter(zName, 0); + va_start(ap, zName); + while( z==0 && (zX = va_arg(ap,const char*))!=0 ){ + z = cgi_parameter(zX, 0); + } + va_end(ap); + return z; +} + +/* +** Return the value of a CGI parameter with leading and trailing +** spaces removed and with internal \r\n changed to just \n +*/ +char *cgi_parameter_trimmed(const char *zName, const char *zDefault){ + const char *zIn; + char *zOut, c; + int i, j; + zIn = cgi_parameter(zName, 0); + if( zIn==0 ) zIn = zDefault; + if( zIn==0 ) return 0; + while( fossil_isspace(zIn[0]) ) zIn++; + zOut = fossil_strdup(zIn); + for(i=j=0; (c = zOut[i])!=0; i++){ + if( c=='\r' && zOut[i+1]=='\n' ) continue; + zOut[j++] = c; + } + zOut[j] = 0; + while( j>0 && fossil_isspace(zOut[j-1]) ) zOut[--j] = 0; + return zOut; +} + +/* +** Return true if the CGI parameter zName exists and is not equal to 0, +** or "no" or "off". +*/ +int cgi_parameter_boolean(const char *zName){ + const char *zIn = cgi_parameter(zName, 0); + if( zIn==0 ) return 0; + return zIn[0]==0 || is_truth(zIn); +} + +/* +** Return either an empty string "" or the string "checked" depending +** on whether or not parameter zName has value iValue. If parameter +** zName does not exist, that is assumed to be the same as value 0. +** +** This routine implements the PCK(x) and PIF(x,y) macros. The PIF(x,y) +** macro generateds " checked" if the value of parameter x equals integer y. +** PCK(x) is the same as PIF(x,1). These macros are used to generate +** the "checked" attribute on checkbox and radio controls of forms. +*/ +const char *cgi_parameter_checked(const char *zName, int iValue){ + const char *zIn = cgi_parameter(zName,0); + int x; + if( zIn==0 ){ + x = 0; + }else if( !fossil_isdigit(zIn[0]) ){ + x = is_truth(zIn); + }else{ + x = atoi(zIn); + } + return x==iValue ? "checked" : ""; +} + +/* +** Return the name of the i-th CGI parameter. Return NULL if there +** are fewer than i registered CGI parameters. +*/ +const char *cgi_parameter_name(int i){ + if( i>=0 && i<nUsedQP ){ + return aParamQP[i].zName; + }else{ + return 0; + } +} + +/* +** Print CGI debugging messages. +*/ +void cgi_debug(const char *zFormat, ...){ + va_list ap; + if( g.fDebug ){ + va_start(ap, zFormat); + vfprintf(g.fDebug, zFormat, ap); + va_end(ap); + fflush(g.fDebug); + } +} + +/* +** Return true if any of the query parameters in the argument +** list are defined. +*/ +int cgi_any(const char *z, ...){ + va_list ap; + char *z2; + if( cgi_parameter(z,0)!=0 ) return 1; + va_start(ap, z); + while( (z2 = va_arg(ap, char*))!=0 ){ + if( cgi_parameter(z2,0)!=0 ) return 1; + } + va_end(ap); + return 0; +} + +/* +** Return true if all of the query parameters in the argument list +** are defined. +*/ +int cgi_all(const char *z, ...){ + va_list ap; + char *z2; + if( cgi_parameter(z,0)==0 ) return 0; + va_start(ap, z); + while( (z2 = va_arg(ap, char*))==0 ){ + if( cgi_parameter(z2,0)==0 ) return 0; + } + va_end(ap); + return 1; +} + +/* +** Load all relevant environment variables into the parameter buffer. +** Invoke this routine prior to calling cgi_print_all() in order to see +** the full CGI environment. This routine intended for debugging purposes +** only. +*/ +void cgi_load_environment(void){ + /* The following is a list of environment variables that Fossil considers + ** to be "relevant". */ + static const char *const azCgiVars[] = { + "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE", "SCGI", + "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", + "HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENTICATION", + "HTTP_CONNECTION", "HTTP_HOST", + "HTTP_IF_NONE_MATCH", "HTTP_IF_MODIFIED_SINCE", + "HTTP_USER_AGENT", "HTTP_REFERER", "PATH_INFO", "PATH_TRANSLATED", + "QUERY_STRING", "REMOTE_ADDR", "REMOTE_PORT", + "REMOTE_USER", "REQUEST_METHOD", "REQUEST_SCHEME", + "REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_NAME", + "SERVER_PROTOCOL", "HOME", "FOSSIL_HOME", "USERNAME", "USER", + "FOSSIL_USER", "SQLITE_TMPDIR", "TMPDIR", + "TEMP", "TMP", "FOSSIL_VFS", + "FOSSIL_FORCE_TICKET_MODERATION", "FOSSIL_FORCE_WIKI_MODERATION", + "FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS", + "TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST", + "CONTENT_TYPE", "CONTENT_LENGTH", + }; + int i; + for(i=0; i<count(azCgiVars); i++) (void)P(azCgiVars[i]); +} + +/* +** Print all query parameters on standard output. +** This is used for testing and debugging. +** +** Omit the values of the cookies unless showAll is true. +** +** The eDest parameter determines where the output is shown: +** +** eDest==0: Rendering as HTML into the CGI reply +** eDest==1: Written to fossil_trace +** eDest==2: Written to cgi_debug +** eDest==3: Written to out (Used only by fossil_errorlog()) +*/ +void cgi_print_all(int showAll, unsigned int eDest, FILE *out){ + int i; + cgi_parameter("",""); /* Force the parameters into sorted order */ + for(i=0; i<nUsedQP; i++){ + const char *zName = aParamQP[i].zName; + const char *zValue = aParamQP[i].zValue; + if( fossil_stricmp("HTTP_COOKIE",zName)==0 + || fossil_strnicmp("fossil-",zName,7)==0 + ){ + if( !showAll ) continue; + if( eDest==3 ) zValue = "..."; + } + switch( eDest ){ + case 0: { + cgi_printf("%h = %h <br>\n", zName, zValue); + break; + } + case 1: { + fossil_trace("%s = %s\n", zName, zValue); + break; + } + case 2: { + cgi_debug("%s = %s\n", zName, zValue); + break; + } + case 3: { + if( zValue!=0 && strlen(zValue)>100 ){ + fprintf(out,"%s = %.100s...\n", zName, zValue); + }else{ + fprintf(out,"%s = %s\n", zName, zValue); + } + break; + } + } + } +} + +/* +** Put information about the N-th parameter into arguments. +** Return non-zero on success, and return 0 if there is no N-th parameter. +*/ +int cgi_param_info( + int N, + const char **pzName, + const char **pzValue, + int *pbIsQP +){ + if( N>=0 && N<nUsedQP ){ + *pzName = aParamQP[N].zName; + *pzValue = aParamQP[N].zValue; + *pbIsQP = aParamQP[N].isQP; + return 1; + }else{ + *pzName = 0; + *pzValue = 0; + *pbIsQP = 0; + return 0; + } +} + +/* +** Export all untagged query parameters (but not cookies or environment +** variables) as hidden values of a form. +*/ +void cgi_query_parameters_to_hidden(void){ + int i; + const char *zN, *zV; + for(i=0; i<nUsedQP; i++){ + if( aParamQP[i].isQP==0 || aParamQP[i].cTag ) continue; + zN = aParamQP[i].zName; + zV = aParamQP[i].zValue; + @ <input type="hidden" name="%h(zN)" value="%h(zV)"> + } +} + +/* +** Export all untagged query parameters (but not cookies or environment +** variables) to the HQuery object. +*/ +void cgi_query_parameters_to_url(HQuery *p){ + int i; + for(i=0; i<nUsedQP; i++){ + if( aParamQP[i].isQP==0 || aParamQP[i].cTag ) continue; + url_add_parameter(p, aParamQP[i].zName, aParamQP[i].zValue); + } +} + +/* +** Reconstruct the URL into memory obtained from fossil_malloc() and +** return a pointer to that URL. +*/ +char *cgi_reconstruct_original_url(void){ + int i; + char cSep = '?'; + Blob url; + blob_init(&url, 0, 0); + blob_appendf(&url, "%s/%s", g.zBaseURL, g.zPath); + for(i=0; i<nUsedQP; i++){ + if( aParamQP[i].isQP ){ + struct QParam *p = &aParamQP[i]; + if( p->zValue && p->zValue[0] ){ + blob_appendf(&url, "%c%t=%t", cSep, p->zName, p->zValue); + }else{ + blob_appendf(&url, "%c%t", cSep, p->zName); + } + cSep = '&'; + } + } + return blob_str(&url); +} + +/* +** Tag query parameter zName so that it is not exported by +** cgi_query_parameters_to_hidden(). Or if zName==0, then +** untag all query parameters. +*/ +void cgi_tag_query_parameter(const char *zName){ + int i; + if( zName==0 ){ + for(i=0; i<nUsedQP; i++) aParamQP[i].cTag = 0; + }else{ + for(i=0; i<nUsedQP; i++){ + if( strcmp(zName,aParamQP[i].zName)==0 ) aParamQP[i].cTag = 1; + } + } +} + +/* +** This routine works like "printf" except that it has the +** extra formatting capabilities such as %h and %t. +*/ +void cgi_printf(const char *zFormat, ...){ + va_list ap; + va_start(ap,zFormat); + vxprintf(pContent,zFormat,ap); + va_end(ap); +} + +/* +** This routine works like "vprintf" except that it has the +** extra formatting capabilities such as %h and %t. +*/ +void cgi_vprintf(const char *zFormat, va_list ap){ + vxprintf(pContent,zFormat,ap); +} + + +/* +** Send a reply indicating that the HTTP request was malformed +*/ +static NORETURN void malformed_request(const char *zMsg, ...){ + va_list ap; + char *z; + va_start(ap, zMsg); + z = vmprintf(zMsg, ap); + va_end(ap); + cgi_set_status(400, "Bad Request"); + zReplyMimeType = "text/plain"; + if( g.zReqType==0 ) g.zReqType = "WWW"; + if( g.zReqType[0]=='C' && PD("SERVER_SOFTWARE",0)!=0 ){ + const char *zServer = PD("SERVER_SOFTWARE",""); + cgi_printf("Bad CGI Request from \"%s\": %s\n",zServer,z); + }else{ + cgi_printf("Bad %s Request: %s\n", g.zReqType, z); + } + fossil_free(z); + cgi_reply(); + fossil_exit(0); +} + +/* +** Panic and die while processing a webpage. +*/ +NORETURN void cgi_panic(const char *zFormat, ...){ + va_list ap; + cgi_reset_content(); +#ifdef FOSSIL_ENABLE_JSON + if( g.json.isJsonMode ){ + char * zMsg; + va_start(ap, zFormat); + zMsg = vmprintf(zFormat,ap); + va_end(ap); + json_err( FSL_JSON_E_PANIC, zMsg, 1 ); + free(zMsg); + fossil_exit( g.isHTTP ? 0 : 1 ); + }else +#endif /* FOSSIL_ENABLE_JSON */ + { + cgi_set_status(500, "Internal Server Error"); + cgi_printf( + "<html><body><h1>Internal Server Error</h1>\n" + "<plaintext>" + ); + va_start(ap, zFormat); + vxprintf(pContent,zFormat,ap); + va_end(ap); + cgi_reply(); + fossil_exit(1); + } +} + +/* z[] is the value of an X-FORWARDED-FOR: line in an HTTP header. +** Return a pointer to a string containing the real IP address, or a +** NULL pointer to stick with the IP address previously computed and +** loaded into g.zIpAddr. +*/ +static const char *cgi_accept_forwarded_for(const char *z){ + int i; + if( !cgi_is_loopback(g.zIpAddr) ){ + /* Only accept X-FORWARDED-FOR if input coming from the local machine */ + return 0; + } + i = strlen(z)-1; + while( i>=0 && z[i]!=',' && !fossil_isspace(z[i]) ) i--; + return &z[++i]; +} + +/* +** 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 *extract_token(char *zInput, char **zLeftOver){ + char *zResult = 0; + if( zInput==0 ){ + if( zLeftOver ) *zLeftOver = 0; + return 0; + } + while( fossil_isspace(*zInput) ){ zInput++; } + zResult = zInput; + while( *zInput && !fossil_isspace(*zInput) ){ zInput++; } + if( *zInput ){ + *zInput = 0; + zInput++; + while( fossil_isspace(*zInput) ){ zInput++; } + } + if( zLeftOver ){ *zLeftOver = zInput; } + return zResult; +} + +/* +** 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; + +/* +** Determine the IP address on the other side of a connection. +** Return a pointer to a string. Or return 0 if unable. +** +** The string is held in a static buffer that is overwritten on +** each call. +*/ +char *cgi_remote_ip(int fd){ + address remoteAddr; + socklen_t size = sizeof(remoteAddr); + static char zHost[NI_MAXHOST]; + if( getpeername(0, &remoteAddr.sa, &size) ){ + return 0; + } + if( getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0, + NI_NUMERICHOST) ){ + return 0; + } + return zHost; +} + +/* +** This routine handles a single HTTP request which is coming in on +** g.httpIn and which replies on g.httpOut +** +** The HTTP request is read from g.httpIn and is used to initialize +** entries in the cgi_parameter() hash, as if those entries were +** environment variables. A call to cgi_init() completes +** the setup. Once all the setup is finished, this procedure returns +** and subsequent code handles the actual generation of the webpage. +*/ +void cgi_handle_http_request(const char *zIpAddr){ + char *z, *zToken; + int i; + const char *zScheme = "http"; + char zLine[2000]; /* A single line of input. */ + g.fullHttpReply = 1; + g.zReqType = "HTTP"; + + if( cgi_fgets(zLine, sizeof(zLine))==0 ){ + malformed_request("missing header"); + } + blob_append(&g.httpHeader, zLine, -1); + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken==0 ){ + malformed_request("malformed HTTP header"); + } + if( fossil_strcmp(zToken,"GET")!=0 + && fossil_strcmp(zToken,"POST")!=0 + && fossil_strcmp(zToken,"HEAD")!=0 + ){ + malformed_request("unsupported HTTP method: \"%s\" - Fossil only supports " + "GET, POST, and HEAD", zToken); + } + cgi_setenv("GATEWAY_INTERFACE","CGI/1.0"); + cgi_setenv("REQUEST_METHOD",zToken); + zToken = extract_token(z, &z); + if( zToken==0 ){ + malformed_request("malformed URI in the HTTP header"); + } + cgi_setenv("REQUEST_URI", zToken); + cgi_setenv("SCRIPT_NAME", ""); + for(i=0; zToken[i] && zToken[i]!='?'; i++){} + if( zToken[i] ) zToken[i++] = 0; + cgi_setenv("PATH_INFO", zToken); + cgi_setenv("QUERY_STRING", &zToken[i]); + if( zIpAddr==0 ){ + zIpAddr = cgi_remote_ip(fossil_fileno(g.httpIn)); + } + if( zIpAddr ){ + cgi_setenv("REMOTE_ADDR", zIpAddr); + g.zIpAddr = fossil_strdup(zIpAddr); + } + + /* Get all the optional fields that follow the first line. + */ + while( cgi_fgets(zLine,sizeof(zLine)) ){ + char *zFieldName; + char *zVal; + + cgi_trace(zLine); + blob_append(&g.httpHeader, zLine, -1); + zFieldName = extract_token(zLine,&zVal); + if( zFieldName==0 || *zFieldName==0 ) break; + while( fossil_isspace(*zVal) ){ zVal++; } + i = strlen(zVal); + while( i>0 && fossil_isspace(zVal[i-1]) ){ i--; } + zVal[i] = 0; + for(i=0; zFieldName[i]; i++){ + zFieldName[i] = fossil_tolower(zFieldName[i]); + } + if( fossil_strcmp(zFieldName,"accept-encoding:")==0 ){ + cgi_setenv("HTTP_ACCEPT_ENCODING", zVal); + }else if( fossil_strcmp(zFieldName,"content-length:")==0 ){ + cgi_setenv("CONTENT_LENGTH", zVal); + }else if( fossil_strcmp(zFieldName,"content-type:")==0 ){ + cgi_setenv("CONTENT_TYPE", zVal); + }else if( fossil_strcmp(zFieldName,"cookie:")==0 ){ + cgi_setenv("HTTP_COOKIE", zVal); + }else if( fossil_strcmp(zFieldName,"https:")==0 ){ + cgi_setenv("HTTPS", zVal); + zScheme = "https"; + }else if( fossil_strcmp(zFieldName,"host:")==0 ){ + char *z; + cgi_setenv("HTTP_HOST", zVal); + z = strchr(zVal, ':'); + if( z ) z[0] = 0; + cgi_setenv("SERVER_NAME", zVal); + }else if( fossil_strcmp(zFieldName,"if-none-match:")==0 ){ + cgi_setenv("HTTP_IF_NONE_MATCH", zVal); + }else if( fossil_strcmp(zFieldName,"if-modified-since:")==0 ){ + cgi_setenv("HTTP_IF_MODIFIED_SINCE", zVal); + }else if( fossil_strcmp(zFieldName,"referer:")==0 ){ + cgi_setenv("HTTP_REFERER", zVal); + }else if( fossil_strcmp(zFieldName,"user-agent:")==0 ){ + cgi_setenv("HTTP_USER_AGENT", zVal); + }else if( fossil_strcmp(zFieldName,"authorization:")==0 ){ + cgi_setenv("HTTP_AUTHORIZATION", zVal); + }else if( fossil_strcmp(zFieldName,"accept-language:")==0 ){ + cgi_setenv("HTTP_ACCEPT_LANGUAGE", zVal); + }else if( fossil_strcmp(zFieldName,"x-forwarded-for:")==0 ){ + const char *zIpAddr = cgi_accept_forwarded_for(zVal); + if( zIpAddr!=0 ){ + g.zIpAddr = fossil_strdup(zIpAddr); + cgi_replace_parameter("REMOTE_ADDR", g.zIpAddr); + } + }else if( fossil_strcmp(zFieldName,"range:")==0 ){ + int x1 = 0; + int x2 = 0; + if( sscanf(zVal,"bytes=%d-%d",&x1,&x2)==2 && x1>=0 && x1<=x2 ){ + rangeStart = x1; + rangeEnd = x2+1; + } + } + } + cgi_setenv("REQUEST_SCHEME",zScheme); + cgi_init(); + cgi_trace(0); +} + +/* +** This routine handles a single HTTP request from an SSH client which is +** coming in on g.httpIn and which replies on g.httpOut +** +** Once all the setup is finished, this procedure returns +** and subsequent code handles the actual generation of the webpage. +** +** It is called in a loop so some variables will need to be replaced +*/ +void cgi_handle_ssh_http_request(const char *zIpAddr){ + static int nCycles = 0; + static char *zCmd = 0; + char *z, *zToken; + char *zMethod; + int i; + size_t n; + char zLine[2000]; /* A single line of input. */ + + assert( !g.httpUseSSL ); +#ifdef FOSSIL_ENABLE_JSON + if( nCycles==0 ){ json_bootstrap_early(); } +#endif + if( zIpAddr ){ + if( nCycles==0 ){ + cgi_setenv("REMOTE_ADDR", zIpAddr); + g.zIpAddr = fossil_strdup(zIpAddr); + } + }else{ + fossil_fatal("missing SSH IP address"); + } + g.zReqType = "HTTP"; + if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ + malformed_request("missing HTTP header"); + } + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken==0 ){ + malformed_request("malformed HTTP header"); + } + + if( fossil_strcmp(zToken, "echo")==0 ){ + /* start looking for probes to complete transport_open */ + zCmd = cgi_handle_ssh_probes(zLine, sizeof(zLine), z, zToken); + if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ + malformed_request("missing HTTP header"); + } + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken==0 ){ + malformed_request("malformed HTTP header"); + } + }else if( zToken && strlen(zToken)==0 && zCmd ){ + /* transport_flip request and continued transport_open */ + cgi_handle_ssh_transport(zCmd); + if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ + malformed_request("missing HTTP header"); + } + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken==0 ){ + malformed_request("malformed HTTP header"); + } + } + + zMethod = fossil_strdup(zToken); + if( fossil_strcmp(zToken,"GET")!=0 && fossil_strcmp(zToken,"POST")!=0 + && fossil_strcmp(zToken,"HEAD")!=0 ){ + malformed_request("unsupported HTTP method"); + } + + if( nCycles==0 ){ + cgi_setenv("GATEWAY_INTERFACE","CGI/1.0"); + cgi_setenv("REQUEST_METHOD",zToken); + } + + zToken = extract_token(z, &z); + if( zToken==0 ){ + malformed_request("malformed URL in HTTP header"); + } + n = strlen(g.zRepositoryName); + if( fossil_strncmp(g.zRepositoryName, zToken, n)==0 + && (zToken[n]=='/' || zToken[n]==0) + && fossil_strcmp(zMethod,"GET")==0 + ){ + zToken += n; + if( zToken && strlen(zToken)==0 ){ + malformed_request("malformed URL in HTTP header"); + } + } + if( nCycles==0 ){ + cgi_setenv("REQUEST_URI", zToken); + cgi_setenv("SCRIPT_NAME", ""); + } + + for(i=0; zToken[i] && zToken[i]!='?'; i++){} + if( zToken[i] ) zToken[i++] = 0; + if( nCycles==0 ){ + cgi_setenv("PATH_INFO", zToken); + cgi_setenv("QUERY_STRING",&zToken[i]); + }else{ + cgi_replace_parameter("PATH_INFO", fossil_strdup(zToken)); + cgi_replace_parameter("QUERY_STRING",fossil_strdup(&zToken[i])); + } + + /* Get all the optional fields that follow the first line. + */ + while( fgets(zLine,sizeof(zLine),g.httpIn) ){ + char *zFieldName; + char *zVal; + + cgi_trace(zLine); + zFieldName = extract_token(zLine,&zVal); + if( zFieldName==0 || *zFieldName==0 ) break; + while( fossil_isspace(*zVal) ){ zVal++; } + i = strlen(zVal); + while( i>0 && fossil_isspace(zVal[i-1]) ){ i--; } + zVal[i] = 0; + for(i=0; zFieldName[i]; i++){ + zFieldName[i] = fossil_tolower(zFieldName[i]); + } + if( fossil_strcmp(zFieldName,"content-length:")==0 ){ + if( nCycles==0 ){ + cgi_setenv("CONTENT_LENGTH", zVal); + }else{ + cgi_replace_parameter("CONTENT_LENGTH", zVal); + } + }else if( fossil_strcmp(zFieldName,"content-type:")==0 ){ + if( nCycles==0 ){ + cgi_setenv("CONTENT_TYPE", zVal); + } + }else if( fossil_strcmp(zFieldName,"host:")==0 ){ + if( nCycles==0 ){ + cgi_setenv("HTTP_HOST", zVal); + } + }else if( fossil_strcmp(zFieldName,"user-agent:")==0 ){ + if( nCycles==0 ){ + cgi_setenv("HTTP_USER_AGENT", zVal); + } + }else if( fossil_strcmp(zFieldName,"x-fossil-transport:")==0 ){ + if( fossil_strnicmp(zVal, "ssh", 3)==0 ){ + if( nCycles==0 ){ + g.fSshClient |= CGI_SSH_FOSSIL; + g.fullHttpReply = 0; + } + } + } + } + + if( nCycles==0 ){ + if( ! ( g.fSshClient & CGI_SSH_FOSSIL ) ){ + /* did not find new fossil ssh transport */ + g.fSshClient &= ~CGI_SSH_CLIENT; + g.fullHttpReply = 1; + cgi_replace_parameter("REMOTE_ADDR", "127.0.0.1"); + } + } + + cgi_reset_content(); + cgi_destination(CGI_BODY); + + cgi_init(); + cgi_trace(0); + nCycles++; +} + +/* +** This routine handles the old fossil SSH probes +*/ +char *cgi_handle_ssh_probes(char *zLine, int zSize, char *z, char *zToken){ + /* Start looking for probes */ + assert( !g.httpUseSSL ); + while( fossil_strcmp(zToken, "echo")==0 ){ + zToken = extract_token(z, &z); + if( zToken==0 ){ + malformed_request("malformed probe"); + } + if( fossil_strncmp(zToken, "test", 4)==0 || + fossil_strncmp(zToken, "probe-", 6)==0 ){ + fprintf(g.httpOut, "%s\n", zToken); + fflush(g.httpOut); + }else{ + malformed_request("malformed probe"); + } + if( fgets(zLine, zSize, g.httpIn)==0 ){ + malformed_request("malformed probe"); + } + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken==0 ){ + malformed_request("malformed probe"); + } + } + + /* Got all probes now first transport_open is completed + ** so return the command that was requested + */ + g.fSshClient |= CGI_SSH_COMPAT; + return fossil_strdup(zToken); +} + +/* +** This routine handles the old fossil SSH transport_flip +** and transport_open communications if detected. +*/ +void cgi_handle_ssh_transport(const char *zCmd){ + char *z, *zToken; + char zLine[2000]; /* A single line of input. */ + + assert( !g.httpUseSSL ); + /* look for second newline of transport_flip */ + if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ + malformed_request("incorrect transport_flip"); + } + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken && strlen(zToken)==0 ){ + /* look for path to fossil */ + if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ + if( zCmd==0 ){ + malformed_request("missing fossil command"); + }else{ + /* no new command so exit */ + fossil_exit(0); + } + } + cgi_trace(zLine); + zToken = extract_token(zLine, &z); + if( zToken==0 ){ + malformed_request("malformed fossil command"); + } + /* see if we've seen the command */ + if( zCmd && zCmd[0] && fossil_strcmp(zToken, zCmd)==0 ){ + return; + }else{ + malformed_request("transport_open failed"); + } + }else{ + malformed_request("transport_flip failed"); + } +} + +/* +** This routine handles a single SCGI request which is coming in on +** g.httpIn and which replies on g.httpOut +** +** The SCGI request is read from g.httpIn and is used to initialize +** entries in the cgi_parameter() hash, as if those entries were +** environment variables. A call to cgi_init() completes +** the setup. Once all the setup is finished, this procedure returns +** and subsequent code handles the actual generation of the webpage. +*/ +void cgi_handle_scgi_request(void){ + char *zHdr; + char *zToFree; + int nHdr = 0; + int nRead; + int c, n, m; + + assert( !g.httpUseSSL ); + while( (c = fgetc(g.httpIn))!=EOF && fossil_isdigit((char)c) ){ + nHdr = nHdr*10 + (char)c - '0'; + } + if( nHdr<16 ) malformed_request("SCGI header too short"); + zToFree = zHdr = fossil_malloc(nHdr); + nRead = (int)fread(zHdr, 1, nHdr, g.httpIn); + if( nRead<nHdr ) malformed_request("cannot read entire SCGI header"); + nHdr = nRead; + while( nHdr ){ + for(n=0; n<nHdr && zHdr[n]; n++){} + for(m=n+1; m<nHdr && zHdr[m]; m++){} + if( m>=nHdr ) malformed_request("SCGI header formatting error"); + cgi_set_parameter(zHdr, zHdr+n+1); + zHdr += m+1; + nHdr -= m+1; + } + fossil_free(zToFree); + (void)fgetc(g.httpIn); /* Read past the "," separating header from content */ + cgi_init(); +} + +#if INTERFACE +/* +** Bitmap values for the flags parameter to cgi_http_server(). +*/ +#define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */ +#define HTTP_SERVER_SCGI 0x0002 /* SCGI instead of HTTP */ +#define HTTP_SERVER_HAD_REPOSITORY 0x0004 /* Was the repository open? */ +#define HTTP_SERVER_HAD_CHECKOUT 0x0008 /* Was a checkout open? */ +#define HTTP_SERVER_REPOLIST 0x0010 /* Allow repo listing */ +#define HTTP_SERVER_NOFORK 0x0020 /* Do not call fork() */ +#define HTTP_SERVER_UNIXSOCKET 0x0040 /* Use a unix-domain socket */ + +#endif /* INTERFACE */ + +/* +** Maximum number of child processes that we can have running +** at one time. Set this to 0 for "no limit". +*/ +#ifndef FOSSIL_MAX_CONNECTIONS +# define FOSSIL_MAX_CONNECTIONS 1000 +#endif + +/* +** 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 cgi_http_server( + int mnPort, int mxPort, /* Range of TCP ports to try */ + const char *zBrowser, /* Run this browser, if not NULL */ + const char *zIpAddr, /* Bind to this IP address, if not null */ + int flags /* HTTP_SERVER_* flags */ +){ +#if defined(_WIN32) + /* Use win32_http_server() instead */ + fossil_exit(1); +#else + int listen4 = -1; /* Main socket; IPv4 or unix-domain */ + int listen6 = -1; /* Aux socket for corresponding IPv6 */ + int mxListen = -1; /* Maximum of listen4 and listen6 */ + int connection; /* An incoming connection */ + int nRequest = 0; /* Number of requests handled so far */ + fd_set readfds; /* Set of file descriptors for select() */ + 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() */ + struct sockaddr_in6 inaddr6; /* Address for IPv6 */ + struct sockaddr_in inaddr4; /* Address for IPv4 */ + struct sockaddr_un uxaddr; /* The address for unix-domain sockets */ + int opt = 1; /* setsockopt flag */ + int rc; /* Result code from system calls */ + int iPort = mnPort; /* Port to try to use */ + const char *zRequestType; /* Type of requests to listen for */ + + + if( flags & HTTP_SERVER_SCGI ){ + zRequestType = "SCGI"; + }else if( g.httpUseSSL ){ + zRequestType = "TLS-encrypted HTTPS"; + }else{ + zRequestType = "HTTP"; + } + + if( flags & HTTP_SERVER_UNIXSOCKET ){ + /* CASE 1: A unix socket named g.zSockName. After creation, set the + ** permissions on the new socket to g.zSockMode and make the + ** owner of the socket be g.zSockOwner. + */ + assert( g.zSockName!=0 ); + memset(&uxaddr, 0, sizeof(uxaddr)); + if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){ + fossil_fatal("name of unix socket too big: %s\nmax size: %d\n", + g.zSockName, (int)sizeof(uxaddr.sun_path)); + } + if( file_isdir(g.zSockName, ExtFILE)!=0 ){ + if( !file_issocket(g.zSockName) ){ + fossil_fatal("cannot name socket \"%s\" because another object" + " with that name already exists", g.zSockName); + }else{ + unlink(g.zSockName); + } + } + uxaddr.sun_family = AF_UNIX; + strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1); + listen4 = socket(AF_UNIX, SOCK_STREAM, 0); + if( listen4<0 ){ + fossil_fatal("unable to create a unix socket named %s", + g.zSockName); + } + mxListen = listen4; + listen6 = -1; + + /* Set the access permission for the new socket. Default to 0660. + ** But use an alternative specified by --socket-mode if available. + ** Do this before bind() to avoid a race condition. */ + if( g.zSockMode ){ + file_set_mode(g.zSockName, listen4, g.zSockMode, 0); + }else{ + file_set_mode(g.zSockName, listen4, "0660", 1); + } + rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr)); + /* Set the owner of the socket if requested by --socket-owner. This + ** must wait until after bind(), after the filesystem object has been + ** created. See https://lkml.org/lkml/2004/11/1/84 and + ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */ + if( g.zSockOwner ){ + file_set_owner(g.zSockName, listen4, g.zSockOwner); + } + fossil_print("Listening for %s requests on unix socket %s\n", + zRequestType, g.zSockName); + fflush(stdout); + }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){ + /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort. + */ + assert( mnPort==mxPort ); + memset(&inaddr6, 0, sizeof(inaddr6)); + inaddr6.sin6_family = AF_INET6; + inaddr6.sin6_port = htons(iPort); + if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){ + fossil_fatal("not a valid IPv6 address: %s", zIpAddr); + } + listen6 = socket(AF_INET6, SOCK_STREAM, 0); + if( listen6>0 ){ + opt = 1; + setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6)); + if( rc<0 ){ + close(listen6); + listen6 = -1; + } + } + if( listen6<0 ){ + fossil_fatal("cannot open a listening socket on [%s]:%d", + zIpAddr, mnPort); + } + mxListen = listen6; + listen4 = -1; + fossil_print("Listening for %s requests on [%s]:%d\n", + zRequestType, zIpAddr, iPort); + fflush(stdout); + }else if( zIpAddr && zIpAddr[0] ){ + /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort. + */ + assert( mnPort==mxPort ); + memset(&inaddr4, 0, sizeof(inaddr4)); + inaddr4.sin_family = AF_INET; + inaddr4.sin_port = htons(iPort); + if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1"; + inaddr4.sin_addr.s_addr = inet_addr(zIpAddr); + if( inaddr4.sin_addr.s_addr == INADDR_NONE ){ + fossil_fatal("not a valid IPv4 address: %s", zIpAddr); + } + listen4 = socket(AF_INET, SOCK_STREAM, 0); + if( listen4>0 ){ + setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4)); + if( rc<0 ){ + close(listen4); + listen4 = -1; + } + } + if( listen4<0 ){ + fossil_fatal("cannot open a listening socket on %s:%d", + zIpAddr, mnPort); + } + mxListen = listen4; + listen6 = -1; + fossil_print("Listening for %s requests on TCP port %s:%d\n", + zRequestType, zIpAddr, iPort); + fflush(stdout); + }else{ + /* CASE 4: Listen on all available IP addresses, or on only loopback + ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the + ** first available in the range of mnPort..mxPort. Listen + ** on both IPv4 and IPv6, if possible. The TCP port scan is done + ** on IPv4. + */ + while( iPort<=mxPort ){ + const char *zProto; + memset(&inaddr4, 0, sizeof(inaddr4)); + inaddr4.sin_family = AF_INET; + inaddr4.sin_port = htons(iPort); + if( flags & HTTP_SERVER_LOCALHOST ){ + inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + }else{ + inaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + } + listen4 = socket(AF_INET, SOCK_STREAM, 0); + if( listen4>0 ){ + setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4)); + if( rc<0 ){ + close(listen4); + listen4 = -1; + } + } + if( listen4<0 ){ + iPort++; + continue; + } + mxListen = listen4; + + /* If we get here, that means we found an open TCP port at iPort for + ** IPv4. Try to set up a corresponding IPv6 socket on the same port. + */ + memset(&inaddr6, 0, sizeof(inaddr6)); + inaddr6.sin6_family = AF_INET6; + inaddr6.sin6_port = htons(iPort); + if( flags & HTTP_SERVER_LOCALHOST ){ + inaddr6.sin6_addr = in6addr_loopback; + }else{ + inaddr6.sin6_addr = in6addr_any; + } + listen6 = socket(AF_INET6, SOCK_STREAM, 0); + if( listen6>0 ){ + setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); + rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6)); + if( rc<0 ){ + close(listen6); + listen6 = -1; + } + } + if( listen6<0 ){ + zProto = "IPv4 only"; + }else{ + zProto = "IPv4 and IPv6"; + if( listen6>listen4 ) mxListen = listen6; + } + + fossil_print("Listening for %s requests on TCP port %s%d, %s\n", + zRequestType, + (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "", + iPort, zProto); + fflush(stdout); + break; + } + if( iPort>mxPort ){ + fossil_fatal("no available TCP ports in the range %d..%d", + mnPort, mxPort); + } + } + + /* If we get to this point, that means there is at least one listening + ** socket on either listen4 or listen6 and perhaps on both. */ + assert( listen4>0 || listen6>0 ); + if( listen4>0 ) listen(listen4,10); + if( listen6>0 ) listen(listen6,10); + if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){ + assert( strstr(zBrowser,"%d")!=0 ); + zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort); +#if defined(__CYGWIN__) + /* On Cygwin, we can do better than "echo" */ + if( fossil_strncmp(zBrowser, "echo ", 5)==0 ){ + wchar_t *wUrl = fossil_utf8_to_unicode(zBrowser+5); + wUrl[wcslen(wUrl)-2] = 0; /* Strip terminating " &" */ + if( (size_t)ShellExecuteW(0, L"open", wUrl, 0, 0, 1)<33 ){ + fossil_warning("cannot start browser\n"); + } + }else +#endif + if( fossil_system(zBrowser)<0 ){ + fossil_warning("cannot start browser: %s\n", zBrowser); + } + } + + /* What for incoming requests. For each request, fork() a child process + ** to deal with that request. The child process returns. The parent + ** keeps on listening and never returns. + */ + while( 1 ){ +#if FOSSIL_MAX_CONNECTIONS>0 + while( nchildren>=FOSSIL_MAX_CONNECTIONS ){ + if( wait(0)>=0 ) nchildren--; + } +#endif + delay.tv_sec = 0; + delay.tv_usec = 100000; + FD_ZERO(&readfds); + assert( listen4>0 || listen6>0 ); + if( listen4>0 ) FD_SET( listen4, &readfds); + if( listen6>0 ) FD_SET( listen6, &readfds); + select( mxListen+1, &readfds, 0, 0, &delay); + if( listen4>0 && FD_ISSET(listen4, &readfds) ){ + lenaddr = sizeof(inaddr4); + connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr); + }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){ + lenaddr = sizeof(inaddr6); + connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr); + }else{ + connection = -1; + } + if( connection>=0 ){ + if( flags & HTTP_SERVER_NOFORK ){ + child = 0; + }else{ + child = fork(); + } + if( child!=0 ){ + if( child>0 ){ + nchildren++; + nRequest++; + } + close(connection); + }else{ + int nErr = 0, fd; + g.zSockName = 0 /* avoid deleting the socket via atexit() */; + close(0); + fd = dup(connection); + if( fd!=0 ) nErr++; + close(1); + fd = dup(connection); + if( fd!=1 ) nErr++; + if( 0 && !g.fAnyTrace ){ + close(2); + fd = dup(connection); + if( fd!=2 ) nErr++; + } + close(connection); + if( listen4>0 ) close(listen4); + if( listen6>0 ) close(listen6); + g.nPendingRequest = nchildren+1; + g.nRequest = nRequest+1; + return nErr; + } + } + /* Bury dead children */ + if( nchildren ){ + while(1){ + int iStatus = 0; + pid_t x = waitpid(-1, &iStatus, WNOHANG); + if( x<=0 ) break; + if( WIFSIGNALED(iStatus) && g.fAnyTrace ){ + fprintf(stderr, "/***** Child %d exited on signal %d (%s) *****/\n", + x, WTERMSIG(iStatus), strsignal(WTERMSIG(iStatus))); + } + nchildren--; + } + } + } + /* NOT REACHED */ + fossil_exit(1); +#endif + /* NOT REACHED */ + return 0; +} + + +/* +** Name of days and months. +*/ +static const char *const azDays[] = + {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", 0}; +static const char *const azMonths[] = + {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 0}; + + +/* +** Returns an RFC822-formatted time string suitable for HTTP headers. +** The timezone is always GMT. The value returned is always a +** string obtained from mprintf() and must be freed using fossil_free() +** to avoid a memory leak. +** +** See http://www.faqs.org/rfcs/rfc822.html, section 5 +** and http://www.faqs.org/rfcs/rfc2616.html, section 3.3. +*/ +char *cgi_rfc822_datestamp(time_t now){ + struct tm *pTm; + pTm = gmtime(&now); + if( pTm==0 ){ + return mprintf(""); + }else{ + return mprintf("%s, %d %s %02d %02d:%02d:%02d +0000", + azDays[pTm->tm_wday], pTm->tm_mday, azMonths[pTm->tm_mon], + pTm->tm_year+1900, pTm->tm_hour, pTm->tm_min, pTm->tm_sec); + } +} + +/* +** Returns an ISO8601-formatted time string suitable for debugging +** purposes. +** +** The value returned is always a string obtained from mprintf() and must +** be freed using fossil_free() to avoid a memory leak. +*/ +char *cgi_iso8601_datestamp(void){ + struct tm *pTm; + time_t now = time(0); + pTm = gmtime(&now); + if( pTm==0 ){ + return mprintf(""); + }else{ + return mprintf("%04d-%02d-%02d %02d:%02d:%02d", + pTm->tm_year+1900, pTm->tm_mon+1, pTm->tm_mday, + pTm->tm_hour, pTm->tm_min, pTm->tm_sec); + } +} + +/* +** Parse an RFC822-formatted timestamp as we'd expect from HTTP and return +** a Unix epoch time. <= zero is returned on failure. +** +** Note that this won't handle all the _allowed_ HTTP formats, just the +** most popular one (the one generated by cgi_rfc822_datestamp(), actually). +*/ +time_t cgi_rfc822_parsedate(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( !fossil_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; +} + +/* +** Check the objectTime against the If-Modified-Since request header. If the +** object time isn't any newer than the header, we immediately send back +** a 304 reply and exit. +*/ +void cgi_modified_since(time_t objectTime){ + const char *zIf = P("HTTP_IF_MODIFIED_SINCE"); + if( zIf==0 ) return; + if( objectTime > cgi_rfc822_parsedate(zIf) ) return; + cgi_set_status(304,"Not Modified"); + cgi_reset_content(); + cgi_reply(); + fossil_exit(0); +} + +/* +** Check to see if the remote client is SSH and return +** its IP or return default +*/ +const char *cgi_ssh_remote_addr(const char *zDefault){ + char *zIndex; + const char *zSshConn = fossil_getenv("SSH_CONNECTION"); + + if( zSshConn && zSshConn[0] ){ + char *zSshClient = fossil_strdup(zSshConn); + if( (zIndex = strchr(zSshClient,' '))!=0 ){ + zSshClient[zIndex-zSshClient] = '\0'; + return zSshClient; + } + } + return zDefault; +} + +/* +** Return true if information is coming from the loopback network. +*/ +int cgi_is_loopback(const char *zIpAddr){ + return fossil_strcmp(zIpAddr, "127.0.0.1")==0 || + fossil_strcmp(zIpAddr, "::ffff:127.0.0.1")==0 || + fossil_strcmp(zIpAddr, "::1")==0; +} + +/* +** Return true if the HTTP request is likely to be from a small-screen +** mobile device. +** +** The returned value is a guess. Use it only for setting up defaults. +*/ +int cgi_from_mobile(void){ + const char *zAgent = P("HTTP_USER_AGENT"); + if( zAgent==0 ) return 0; + if( sqlite3_strglob("*iPad*", zAgent)==0 ) return 0; + return sqlite3_strlike("%mobile%", zAgent, 0)==0; +} + +/* +** Look for query or POST parameters that: +** +** (1) Have not been used +** (2) Appear to be malicious attempts to break into or otherwise +** harm the system, for example via SQL injection +** +** If any such parameters are seen, a 418 ("I'm a teapot") return is +** generated and processing aborts - this routine does not return. +** +** When Fossil is launched via CGI from althttpd, the 418 return signals +** the webserver to put the requestor IP address into "timeout", blocking +** subsequent requests for 5 minutes. +** +** Fossil is not subject to any SQL injections, as far as anybody knows. +** This routine is not necessary for the security of the system (though +** an extra layer of security never hurts). The main purpose here is +** to shutdown malicious attack spiders and prevent them from burning +** lots of CPU cycles and bogging down the website. In other words, the +** objective of this routine is to help prevent denial-of-service. +** +** Usage Hint: Put a call to this routine as late in the webpage +** implementation as possible, ideally just before it begins doing +** potentially CPU-intensive computations and after all query parameters +** have been consulted. +*/ +void cgi_check_for_malice(void){ + struct QParam * pParam; + int i; + for(i=0; i<nUsedQP; ++i){ + pParam = &aParamQP[i]; + if( 0==pParam->isFetched + && pParam->zValue!=0 + && pParam->zName!=0 + && fossil_islower(pParam->zName[0]) + ){ + cgi_value_spider_check(pParam->zValue, pParam->zName); + } + } +} |
