/* ** Copyright (c) 2019 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@sqlite.org ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code to invoke CGI-based extensions to the ** Fossil server via the /ext webpage. ** ** The /ext webpage acts like a recursive webserver, relaying the ** HTTP request to some other component - usually another CGI. ** ** Before doing the relay, /ext examines the login cookie to see ** if the HTTP request is coming from a validated user, and if so ** /ext sets some additional environment variables that the extension ** CGI script can use. In this way, the extension CGI scripts use the ** same login system as the main repository, and appear to be ** an integrated part of the repository. */ #include "config.h" #include "extcgi.h" #include /* ** These are the environment variables that should be set for CGI ** extension programs: */ static const char *azCgiEnv[] = { "AUTH_TYPE", "AUTH_CONTENT", "CONTENT_LENGTH", "CONTENT_TYPE", "DOCUMENT_ROOT", "FOSSIL_CAPABILITIES", "FOSSIL_NONCE", "FOSSIL_REPOSITORY", "FOSSIL_URI", "FOSSIL_USER", "GATEWAY_INTERFACE", "HTTPS", "HTTP_ACCEPT", /* "HTTP_ACCEPT_ENCODING", // omitted from sub-cgi */ "HTTP_COOKIE", "HTTP_HOST", "HTTP_IF_MODIFIED_SINCE", "HTTP_IF_NONE_MATCH", "HTTP_REFERER", "HTTP_USER_AGENT", "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REMOTE_USER", "REQUEST_METHOD", "REQUEST_SCHEME", "REQUEST_URI", "SCRIPT_DIRECTORY", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_NAME", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE", }; /* ** Check a pathname to determine if it is acceptable for use as ** extension CGI. Some pathnames are excluded for security reasons. ** Return NULL on success or a static error string if there is ** a failure. */ const char *ext_pathname_ok(const char *zName){ int i; const char *zFailReason = 0; for(i=0; zName[i]; i++){ char c = zName[i]; if( (c=='.' || c=='-') && (i==0 || zName[i-1]=='/') ){ zFailReason = "path element begins with '.' or '-'"; break; } if( !fossil_isalnum(c) && c!='_' && c!='-' && c!='.' && c!='/' ){ zFailReason = "illegal character in path"; break; } } return zFailReason; } /* ** The *pzPath input is a pathname obtained from mprintf(). ** ** If ** ** (1) zPathname is the name of a directory, and ** (2) the name ends with "/", and ** (3) the directory contains a file named index.html, index.wiki, ** or index.md (in that order) ** ** then replace the input with a revised name that includes the index.* ** file and return non-zero (true). If any condition is not met, return ** zero and leave the input pathname unchanged. */ static int isDirWithIndexFile(char **pzPath){ static const char *azIndexNames[] = { "index.html", "index.wiki", "index.md" }; int i; if( file_isdir(*pzPath, ExtFILE)!=1 ) return 0; if( sqlite3_strglob("*/", *pzPath)!=0 ) return 0; for(i=0; i=nRoot+1 ); style_set_current_page("ext/%s", &zScript[nRoot+1]); zMime = mimetype_from_name(zScript); if( zMime==0 ) zMime = "application/octet-stream"; if( !file_isexe(zScript, ExtFILE) ){ /* File is not executable. Must be a regular file. In that case, ** disallow extra path elements */ if( zPath[nScript]!=0 ){ zFailReason = "extra path elements after filename"; goto ext_not_found; } blob_read_from_file(&reply, zScript, ExtFILE); document_render(&reply, zMime, zName, zName); return; } /* If we reach this point, that means we are dealing with an executable ** file name zScript. Run that file as CGI. */ cgi_replace_parameter("DOCUMENT_ROOT", g.zExtRoot); cgi_replace_parameter("SCRIPT_FILENAME", zScript); cgi_replace_parameter("SCRIPT_NAME", mprintf("%T/ext/%T",g.zTop,zScript+nRoot+1)); cgi_replace_parameter("SCRIPT_DIRECTORY", file_dirname(zScript)); cgi_replace_parameter("PATH_INFO", zName + strlen(zScript+nRoot+1)); if( g.zLogin ){ cgi_replace_parameter("REMOTE_USER", g.zLogin); cgi_set_parameter_nocopy("FOSSIL_USER", g.zLogin, 0); } cgi_set_parameter_nocopy("FOSSIL_NONCE", style_nonce(), 0); cgi_set_parameter_nocopy("FOSSIL_REPOSITORY", g.zRepositoryName, 0); cgi_set_parameter_nocopy("FOSSIL_URI", g.zTop, 0); cgi_set_parameter_nocopy("FOSSIL_CAPABILITIES", db_text("","SELECT fullcap(cap) FROM user WHERE login=%Q", g.zLogin ? g.zLogin : "nobody"), 0); zSrvSw = P("SERVER_SOFTWARE"); if( zSrvSw==0 ){ zSrvSw = get_version(); }else{ char *z = mprintf("fossil version %s", get_version()); if( strncmp(zSrvSw,z,strlen(z)-4)!=0 ){ zSrvSw = mprintf("%z, %s", z, zSrvSw); } } cgi_replace_parameter("SERVER_SOFTWARE", zSrvSw); cgi_replace_parameter("GATEWAY_INTERFACE","CGI/1.0"); for(i=0; i0 ){ size_t nSent, toSend; unsigned char *data = (unsigned char*)blob_buffer(&g.cgiIn); toSend = (size_t)blob_size(&g.cgiIn); do{ nSent = fwrite(data, 1, toSend, toChild); if( nSent<=0 ){ zFailReason = "unable to send all content to the CGI child process"; goto ext_not_found; } toSend -= nSent; data += nSent; }while( toSend>0 ); fflush(toChild); } if( g.perm.Debug && P("fossil-ext-debug")!=0 ){ /* For users with Debug privilege, if the "fossil-ext-debug" query ** parameter exists, then show raw output from the CGI */ zMime = "text/plain"; }else{ while( fgets(zLine,sizeof(zLine),fromChild) ){ for(i=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){} zLine[i] = 0; if( i==0 ) break; if( fossil_strnicmp(zLine,"Location:",9)==0 ){ fclose(fromChild); fclose(toChild); cgi_redirect(&zLine[10]); /* no return */ }else if( fossil_strnicmp(zLine,"Status:",7)==0 ){ int j; for(i=7; fossil_isspace(zLine[i]); i++){} for(j=i; fossil_isdigit(zLine[j]); j++){} while( fossil_isspace(zLine[j]) ){ j++; } cgi_set_status(atoi(&zLine[i]), &zLine[j]); }else if( fossil_strnicmp(zLine,"Content-Length:",15)==0 ){ nContent = atoi(&zLine[15]); }else if( fossil_strnicmp(zLine,"Content-Type:",13)==0 ){ int j; for(i=13; fossil_isspace(zLine[i]); i++){} for(j=i; zLine[j] && zLine[j]!=';'; j++){} zMime = mprintf("%.*s", j-i, &zLine[i]); }else{ cgi_append_header(zLine); cgi_append_header("\r\n"); } } } blob_read_from_channel(&reply, fromChild, nContent); zFailReason = 0; /* Indicate success */ ext_not_found: fossil_free(zPath); if( fromChild ){ fclose(fromChild); }else if( fdFromChild>2 ){ close(fdFromChild); } if( toChild ) fclose(toChild); if( zFailReason==0 ){ document_render(&reply, zMime, zName, zName); }else{ cgi_set_status(404, "Not Found"); @

Not Found

@

Page not found: %h(zPathInfo)

if( g.perm.Debug ){ @

Reason for failure: %h(zFailReason)

} } return; } /* ** Create a temporary SFILE table and fill it with one entry for each file ** in the extension document root directory (g.zExtRoot). The SFILE table ** looks like this: ** ** CREATE TEMP TABLE sfile( ** pathname TEXT PRIMARY KEY, ** isexe BOOLEAN ** ) WITHOUT ROWID; */ void ext_files(void){ Blob base; db_multi_exec( "CREATE TEMP TABLE sfile(\n" " pathname TEXT PRIMARY KEY,\n" " isexe BOOLEAN\n" ") WITHOUT ROWID;" ); blob_init(&base, g.zExtRoot, -1); vfile_scan(&base, blob_size(&base), SCAN_ALL|SCAN_ISEXE, 0, 0, ExtFILE); blob_zero(&base); } /* ** WEBPAGE: extfilelist ** ** List all files in the extension CGI document root and its subfolders. */ void ext_filelist_page(void){ Stmt q; login_check_credentials(); if( !g.perm.Admin ){ login_needed(0); return; } ext_files(); style_set_current_feature("extcgi"); style_header("CGI Extension Filelist"); @ @ db_prepare(&q, "SELECT pathname, isexe FROM sfile" " ORDER BY pathname"); while( db_step(&q)==SQLITE_ROW ){ const char *zName = db_column_text(&q,0); int isExe = db_column_int(&q,1); @ if( ext_pathname_ok(zName)!=0 ){ @ @ }else{ @ if( isExe ){ @ }else{ @ } } @ } db_finalize(&q); @ @
%h(zName)data file%h(zName)CGIstatic content
style_finish_page(); }