Index: src/allrepo.c ================================================================== --- src/allrepo.c +++ src/allrepo.c @@ -134,10 +134,14 @@ ** ** setting Run the "setting", "set", or "unset" commands on all ** set repositories. These command are particularly useful in ** unset conjunction with the "max-loadavg" setting which cannot ** otherwise be set globally. +** +** server Run the "ui" or "server" commands on all repositories. +** ui The root URI gives a listing of all repos. +** ** ** In addition, the following maintenance operations are supported: ** ** add Add all the repositories named to the set of repositories ** tracked by Fossil. Normally Fossil is able to keep up with @@ -189,10 +193,16 @@ n = strlen(g.argv[2]); db_open_config(1, 0); blob_zero(&extra); zCmd = g.argv[2]; if( !login_is_nobody() ) blob_appendf(&extra, " -U %s", g.zLogin); + if( strncmp(zCmd, "ui", n)==0 || strncmp(zCmd, "server", n)==0 ){ + g.argv[1] = g.argv[2]; + g.argv[2] = "/"; + cmd_webserver(); + return; + } if( strncmp(zCmd, "list", n)==0 || strncmp(zCmd,"ls",n)==0 ){ zCmd = "list"; useCheckouts = find_option("ckout","c",0)!=0; }else if( strncmp(zCmd, "clean", n)==0 ){ zCmd = "clean --chdir"; @@ -355,11 +365,11 @@ showLabel = 1; collect_argv(&extra, 3); }else{ fossil_fatal("\"all\" subcommand should be one of: " "add cache changes clean dbstat extras fts-config ignore " - "info list ls pull push rebuild setting sync unset"); + "info list ls pull push rebuild server setting sync ui unset"); } verify_all_options(); zFossil = quoteFilename(g.nameOfExe); db_multi_exec("CREATE TEMP TABLE repolist(name,tag);"); if( useCheckouts ){ Index: src/file.c ================================================================== --- src/file.c +++ src/file.c @@ -1435,5 +1435,20 @@ int i; for(i=2; i0 && zDir[i]!='/'; i--){} if( zDir[i]!='/' ) fossil_fatal("bad repository name: %s", zRepo); if( i>0 ){ @@ -1182,48 +1184,88 @@ } /* ** Generate a web-page that lists all repositories located under the ** g.zRepositoryName directory and return non-zero. +** +** For the special case when g.zRepositoryName a non-chroot-jail "/", +** compose the list using the "repo:" entries in the global_config +** table of the configuration database. These entries comprise all +** of the repositories known to the "all" command. The special case +** processing is disallowed for chroot jails because g.zRepositoryName +** is always "/" inside a chroot jail and so it cannot be used as a flag +** to signal the special processing in that case. The special case +** processing is intended for the "fossil all ui" command which never +** runs in a chroot jail anyhow. ** ** Or, if no repositories can be located beneath g.zRepositoryName, ** return 0. */ static int repo_list_page(void){ Blob base; int n = 0; assert( g.db==0 ); - blob_init(&base, g.zRepositoryName, -1); - sqlite3_open(":memory:", &g.db); - db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); - db_multi_exec("CREATE TABLE vfile(pathname);"); - vfile_scan(&base, blob_size(&base), 0, 0, 0); - db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"); + if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ + /* For the special case of the "repository directory" being "/", + ** show all of the repositories named in the ~/.fossil database. + ** + ** On unix systems, then entries are of the form "repo:/home/..." + ** and on Windows systems they are like "repo:C:/Users/...". We want + ** to skip the first 6 characters on unix and the first 5 characters + ** on Windows. + */ + db_open_config(1, 0); +#ifdef _WIN32 + db_multi_exec( + "CREATE TEMP VIEW sfile AS" + " SELECT substr(name,6) AS 'pathname' FROM global_config" + " WHERE name GLOB 'repo:*'" + ); +#else + db_multi_exec( + "CREATE TEMP VIEW sfile AS" + " SELECT substr(name,7) AS 'pathname' FROM global_config" + " WHERE name GLOB 'repo:*'" + ); +#endif + }else{ + /* The default case: All repositories under the g.zRepositoryName + ** directory. + */ + blob_init(&base, g.zRepositoryName, -1); + sqlite3_open(":memory:", &g.db); + db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); + db_multi_exec("CREATE TABLE vfile(pathname);"); + vfile_scan(&base, blob_size(&base), 0, 0, 0); + db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"); + } + @ + @ + @ + @ Repository List + @ + @ n = db_int(0, "SELECT count(*) FROM sfile"); if( n>0 ){ Stmt q; - @ - @ - @ - @ Repository List - @ - @ @

Available Repositories:

@
    db_prepare(&q, "SELECT pathname, substr(pathname,-7,-100000)||'/home'" " FROM sfile ORDER BY pathname COLLATE nocase;"); while( db_step(&q)==SQLITE_ROW ){ const char *zName = db_column_text(&q, 0); const char *zUrl = db_column_text(&q, 1); - @
  1. %h(zName)
  2. + @
  3. %h(zName)
  4. } @
- @ - @ - cgi_reply(); + }else{ + @

No Repositories Found

} + @ + @ + cgi_reply(); sqlite3_close(g.db); g.db = 0; return n; } @@ -1252,15 +1294,15 @@ static void process_one_web_page( const char *zNotFound, /* Redirect here on a 404 if not NULL */ Glob *pFileGlob, /* Deliver static files matching */ int allowRepoList /* Send repo list for "/" URL */ ){ - const char *zPathInfo; - const char *zDirPathInfo; + const char *zPathInfo = PD("PATH_INFO", ""); char *zPath = NULL; int i; const CmdOrPage *pCmd = 0; + const char *zBase = g.zRepositoryName; /* Handle universal query parameters */ if( PB("utc") ){ g.fTimeFormat = 1; }else if( PB("localtime") ){ @@ -1268,90 +1310,138 @@ } /* If the repository has not been opened already, then find the ** repository based on the first element of PATH_INFO and open it. */ - zDirPathInfo = zPathInfo = PD("PATH_INFO",""); - /* For the PATH_INFO that will be used to help build the final - ** g.zBaseURL and g.zTop (only), skip over the initial directory - ** portion of PATH_INFO; otherwise, it may be duplicated. - */ - if( g.zTop ){ - int nTop = strlen(g.zTop); - if ( strncmp(zDirPathInfo, g.zTop, nTop)==0 ){ - zDirPathInfo += nTop; - } - } if( !g.repositoryOpen ){ - char *zRepo, *zToFree; - const char *zOldScript = PD("SCRIPT_NAME", ""); - char *zNewScript; - int j, k; - i64 szFile; + char *zRepo; /* Candidate repository name */ + char *zToFree = 0; /* Malloced memory that needs to be freed */ + const char *zCleanRepo; /* zRepo with surplus leading "/" removed */ + const char *zOldScript = PD("SCRIPT_NAME", ""); /* Original SCRIPT_NAME */ + char *zNewScript; /* Revised SCRIPT_NAME after processing */ + int j, k; /* Loop variables */ + i64 szFile; /* File size of the candidate repository */ i = zPathInfo[0]!=0; + if( fossil_strcmp(g.zRepositoryName, "/")==0 ){ + zBase++; +#ifdef _WIN32 + if( sqlite3_strglob("/[a-zA-Z]:/*", zPathInfo)==0 ) i = 4; +#endif + } while( 1 ){ while( zPathInfo[i] && zPathInfo[i]!='/' ){ i++; } - zRepo = zToFree = mprintf("%s%.*s.fossil",g.zRepositoryName,i,zPathInfo); + + /* The candidate repository name is some prefix of the PATH_INFO + ** with ".fossil" appended */ + zRepo = zToFree = mprintf("%s%.*s.fossil",zBase,i,zPathInfo); + if( g.fHttpTrace ){ + @ + fprintf(stderr, "# looking for repository named \"%s\"\n", zRepo); + } + - /* To avoid mischief, make sure the repository basename contains no + /* For safety -- to prevent an attacker from accessing arbitrary disk + ** files by sending a maliciously crafted request URI to a public + ** server -- make sure the repository basename contains no ** characters other than alphanumerics, "/", "_", "-", and ".", and ** that "-" never occurs immediately after a "/" and that "." is always ** surrounded by two alphanumerics. Any character that does not ** satisfy these constraints is converted into "_". */ szFile = 0; - for(j=strlen(g.zRepositoryName)+1, k=0; zRepo[j] && k + fprintf(stderr, "# unsafe pathname rejected: %s\n", zRepo); + } break; } + + /* Check to see if a file name zRepo exists. If a file named zRepo + ** does not exist, szFile will become -1. If the file does exist, + ** then szFile will become zero (for an empty file) or positive. + ** Special case: Assume any file with a basename of ".fossil" does + ** not exist. + */ + zCleanRepo = file_cleanup_fullpath(zRepo); if( szFile==0 && sqlite3_strglob("*/.fossil",zRepo)!=0 ){ - if( zRepo[0]=='/' && zRepo[1]=='/' ){ zRepo++; j--; } - szFile = file_size(zRepo); - /* this should only be set from the --baseurl option, not CGI */ - if( g.zBaseURL && g.zBaseURL[0]!=0 && g.zTop && g.zTop[0]!=0 && - file_isdir(g.zRepositoryName)==1 ){ - if( zPathInfo==zDirPathInfo ){ - g.zBaseURL = mprintf("%s%.*s", g.zBaseURL, i, zPathInfo); - g.zTop = mprintf("%s%.*s", g.zTop, i, zPathInfo); - } + szFile = file_size(zCleanRepo); + if( g.fHttpTrace ){ + @ + fprintf(stderr, "# file_size(%s) = %lld\n", zCleanRepo, szFile); } } + + /* If no file named by zRepo exists, remove the added ".fossil" suffix + ** and check to see if there is a file or directory with the same + ** name as the raw PATH_INFO text. + */ if( szFile<0 && i>0 ){ const char *zMimetype; - assert( fossil_strcmp(&zRepo[j], ".fossil")==0 ); - zRepo[j] = 0; - if( zPathInfo[i]=='/' && file_isdir(zRepo)==1 ){ + assert( fossil_strcmp(&zRepo[j], ".fossil")==0 ); + zRepo[j] = 0; /* Remove the ".fossil" suffix */ + + /* The PATH_INFO prefix seen so far is a valid directory. + ** Continue the loop with the next element of the PATH_INFO */ + if( zPathInfo[i]=='/' && file_isdir(zCleanRepo)==1 ){ fossil_free(zToFree); i++; continue; } + + /* If zRepo is the name of an ordinary file that matches the + ** "--file GLOB" pattern, then the CGI reply is the text of + ** of the file. + ** + ** For safety, do not allow any file whose name contains ".fossil" + ** to be returned this way, to prevent complete repositories from + ** being delivered accidently. This is not intended to be a + ** general-purpose web server. The "--file GLOB" mechanism is + ** designed to allow the delivery of a few static images or HTML + ** pages. + */ if( pFileGlob!=0 - && file_isfile(zRepo) - && glob_match(pFileGlob, zRepo) + && file_isfile(zCleanRepo) + && glob_match(pFileGlob, file_cleanup_fullpath(zRepo)) && sqlite3_strglob("*.fossil*",zRepo)!=0 && (zMimetype = mimetype_from_name(zRepo))!=0 && strcmp(zMimetype, "application/x-fossil-artifact")!=0 ){ Blob content; - blob_read_from_file(&content, zRepo); + blob_read_from_file(&content, file_cleanup_fullpath(zRepo)); cgi_set_content_type(zMimetype); cgi_set_content(&content); cgi_reply(); return; } zRepo[j] = '.'; } + /* If we reach this point, it means that the search of the PATH_INFO + ** string is finished. Either zRepo contains the name of the + ** repository to be used, or else no repository could be found an + ** some kind of error response is required. + */ if( szFile<1024 ){ set_base_url(0); if( strcmp(zPathInfo,"/")==0 && allowRepoList && repo_list_page() ){ @@ -1371,34 +1461,59 @@ } return; } break; } + + /* Add the repository name (without the ".fossil" suffix) to the end + ** of SCRIPT_NAME and g.zTop and g.zBaseURL and remove the repository + ** name from the beginning of PATH_INFO. + */ zNewScript = mprintf("%s%.*s", zOldScript, i, zPathInfo); + if( g.zTop ) g.zTop = mprintf("%s%.*s", g.zTop, i, zPathInfo); + if( g.zBaseURL ) g.zBaseURL = mprintf("%s%.*s", g.zBaseURL, i, zPathInfo); cgi_replace_parameter("PATH_INFO", &zPathInfo[i+1]); zPathInfo += i; cgi_replace_parameter("SCRIPT_NAME", zNewScript); - db_open_repository(zRepo); + db_open_repository(file_cleanup_fullpath(zRepo)); if( g.fHttpTrace ){ + @ + @ + @ fprintf(stderr, "# repository: [%s]\n" - "# new PATH_INFO = [%s]\n" - "# new SCRIPT_NAME = [%s]\n", + "# translated PATH_INFO = [%s]\n" + "# translated SCRIPT_NAME = [%s]\n", zRepo, zPathInfo, zNewScript); + if( g.zTop ){ + @ + fprintf(stderr, "# translated g.zTop = [%s]\n", g.zTop); + } + if( g.zBaseURL ){ + @ + fprintf(stderr, "# translated g.zBaseURL = [%s]\n", g.zBaseURL); + } } } - /* Find the page that the user has requested, construct and deliver that - ** page. + /* At this point, the appropriate repository database file will have + ** been opened. Use the first element of PATH_INFO as the page name + ** and deliver the appropriate page back to the user. */ if( g.zContentType && strncmp(g.zContentType, "application/x-fossil", 20)==0 ){ + /* Special case: If the content mimetype shows that it is "fossil sync" + ** payload, then pretend that the PATH_INFO is /xfer so that we always + ** invoke the sync page. */ zPathInfo = "/xfer"; } set_base_url(0); if( zPathInfo==0 || zPathInfo[0]==0 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){ + /* Second special case: If the PATH_INFO is blank, issue a redirect to + ** the home page identified by the "index-page" setting in the repository + ** CONFIG table, to "/index" if there no "index-page" setting. */ #ifdef FOSSIL_ENABLE_JSON if(g.json.isJsonMode){ json_err(FSL_JSON_E_RESOURCE_NOT_FOUND,NULL,1); fossil_exit(0); } @@ -1415,56 +1530,10 @@ g.zPath = &zPath[1]; for(i=1; zPath[i] && zPath[i]!='/'; i++){} if( zPath[i]=='/' ){ zPath[i] = 0; g.zExtra = &zPath[i+1]; - -#ifdef FOSSIL_ENABLE_SUBREPOSITORY - char *zAltRepo = 0; - /* 2016-09-21: Subrepos are undocumented and apparently no longer work. - ** So they are now removed unless the -DFOSSIL_ENABLE_SUBREPOSITORY - ** compile-time option is used. If there are no complaints after - ** a while, we can delete the code entirely. - */ - /* Look for sub-repositories. A sub-repository is another repository - ** that accepts the login credentials of the current repository. A - ** subrepository is identified by a CONFIG table entry "subrepo:NAME" - ** where NAME is the first component of the path. The value of the - ** the CONFIG entries is the string "USER:FILENAME" where USER is the - ** USER name to log in as in the subrepository and FILENAME is the - ** repository filename. - */ - zAltRepo = db_text(0, "SELECT value FROM config WHERE name='subrepo:%q'", - g.zPath); - if( zAltRepo ){ - int nHost; - int jj; - char *zUser = zAltRepo; - login_check_credentials(); - for(jj=0; zAltRepo[jj] && zAltRepo[jj]!=':'; jj++){} - if( zAltRepo[jj]==':' ){ - zAltRepo[jj] = 0; - zAltRepo += jj+1; - }else{ - zUser = "nobody"; - } - if( g.zLogin==0 || g.zLogin[0]==0 ) zUser = "nobody"; - if( zAltRepo[0]!='/' ){ - zAltRepo = mprintf("%s/../%s", g.zRepositoryName, zAltRepo); - file_simplify_name(zAltRepo, -1, 0); - } - db_close(1); - db_open_repository(zAltRepo); - login_as_user(zUser); - g.perm.Password = 0; - zPath += i; - nHost = g.zTop - g.zBaseURL; - g.zBaseURL = mprintf("%z/%s", g.zBaseURL, g.zPath); - g.zTop = g.zBaseURL + nHost; - continue; - } -#endif /* FOSSIL_ENABLE_SUBREPOSITORY */ }else{ g.zExtra = 0; } break; } @@ -2203,10 +2272,15 @@ ** list of glob patterns given by --files and that have known suffixes ** such as ".txt" or ".html" or ".jpeg" and do not match the pattern ** "*.fossil*" will be served as static content. With the "ui" command, ** the REPOSITORY can only be a directory if the --notfound option is ** also present. +** +** For the special case REPOSITORY name of "/", the list global configuration +** database is consulted for a list of all known repositories. The --repolist +** option is implied by this special case. See also the "fossil all ui" +** command. ** ** By default, the "ui" command provides full administrative access without ** having to log in. This can be disabled by turning off the "localauth" ** setting. Automatic login for the "server" command is available if the ** --localauth option is present and the "localauth" setting is off and the @@ -2377,11 +2451,15 @@ if( g.fHttpTrace || g.fSqlTrace ){ fprintf(stderr, "====== SERVER pid %d =======\n", getpid()); } g.cgiOutput = 1; find_server_repository(2, 0); - g.zRepositoryName = enter_chroot_jail(g.zRepositoryName, noJail); + if( fossil_strcmp(g.zRepositoryName,"/")==0 ){ + allowRepoList = 1; + }else{ + g.zRepositoryName = enter_chroot_jail(g.zRepositoryName, noJail); + } if( flags & HTTP_SERVER_SCGI ){ cgi_handle_scgi_request(); }else{ cgi_handle_http_request(0); }