Fossil SCM

fossil-scm / src / extcgi.c
Blame History Raw 439 lines
1
/*
2
** Copyright (c) 2019 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code to invoke CGI-based extensions to the
19
** Fossil server via the /ext webpage.
20
**
21
** The /ext webpage acts like a recursive webserver, relaying the
22
** HTTP request to some other component - usually another CGI.
23
**
24
** Before doing the relay, /ext examines the login cookie to see
25
** if the HTTP request is coming from a validated user, and if so
26
** /ext sets some additional environment variables that the extension
27
** CGI script can use. In this way, the extension CGI scripts use the
28
** same login system as the main repository, and appear to be
29
** an integrated part of the repository.
30
*/
31
#include "config.h"
32
#include "extcgi.h"
33
#include <assert.h>
34
35
/*
36
** These are the environment variables that should be set for CGI
37
** extension programs:
38
*/
39
static const char *azCgiEnv[] = {
40
"AUTH_TYPE",
41
"AUTH_CONTENT",
42
"CONTENT_LENGTH",
43
"CONTENT_TYPE",
44
"DOCUMENT_ROOT",
45
"FOSSIL_CAPABILITIES",
46
"FOSSIL_NONCE",
47
"FOSSIL_REPOSITORY",
48
"FOSSIL_URI",
49
"FOSSIL_USER",
50
"GATEWAY_INTERFACE",
51
"HTTPS",
52
"HTTP_ACCEPT",
53
/* "HTTP_ACCEPT_ENCODING", // omitted from sub-cgi */
54
"HTTP_ACCEPT_LANGUAGE",
55
"HTTP_COOKIE",
56
"HTTP_HOST",
57
"HTTP_IF_MODIFIED_SINCE",
58
"HTTP_IF_NONE_MATCH",
59
"HTTP_REFERER",
60
"HTTP_USER_AGENT",
61
"PATH_INFO",
62
"QUERY_STRING",
63
"REMOTE_ADDR",
64
"REMOTE_USER",
65
"REQUEST_METHOD",
66
"REQUEST_SCHEME",
67
"REQUEST_URI",
68
"SCRIPT_DIRECTORY",
69
"SCRIPT_FILENAME",
70
"SCRIPT_NAME",
71
"SERVER_NAME",
72
"SERVER_PORT",
73
"SERVER_PROTOCOL",
74
"SERVER_SOFTWARE",
75
};
76
77
/*
78
** Check a pathname to determine if it is acceptable for use as
79
** extension CGI. Some pathnames are excluded for security reasons.
80
** Return NULL on success or a static error string if there is
81
** a failure.
82
*/
83
const char *ext_pathname_ok(const char *zName){
84
int i;
85
const char *zFailReason = 0;
86
for(i=0; zName[i]; i++){
87
char c = zName[i];
88
if( (c=='.' || c=='-') && (i==0 || zName[i-1]=='/') ){
89
zFailReason = "path element begins with '.' or '-'";
90
break;
91
}
92
if( !fossil_isalnum(c) && c!='_' && c!='-' && c!='.' && c!='/' ){
93
zFailReason = "illegal character in path";
94
break;
95
}
96
}
97
return zFailReason;
98
}
99
100
/*
101
** The *pzPath input is a pathname obtained from mprintf().
102
**
103
** If
104
**
105
** (1) zPathname is the name of a directory, and
106
** (2) the name ends with "/", and
107
** (3) the directory contains a file named index.html, index.wiki,
108
** or index.md (in that order)
109
**
110
** then replace the input with a revised name that includes the index.*
111
** file and return non-zero (true). If any condition is not met, return
112
** zero and leave the input pathname unchanged.
113
*/
114
static int isDirWithIndexFile(char **pzPath){
115
static const char *azIndexNames[] = {
116
"index.html", "index.wiki", "index.md"
117
};
118
int i;
119
if( file_isdir(*pzPath, ExtFILE)!=1 ) return 0;
120
if( sqlite3_strglob("*/", *pzPath)!=0 ) return 0;
121
for(i=0; i<(int)(sizeof(azIndexNames)/sizeof(azIndexNames[0])); i++){
122
char *zNew = mprintf("%s%s", *pzPath, azIndexNames[i]);
123
if( file_isfile(zNew, ExtFILE) ){
124
fossil_free(*pzPath);
125
*pzPath = zNew;
126
return 1;
127
}
128
fossil_free(zNew);
129
}
130
return 0;
131
}
132
133
/*
134
** WEBPAGE: ext raw-content
135
**
136
** Relay an HTTP request to secondary CGI after first checking the
137
** login credentials and setting auxiliary environment variables
138
** so that the secondary CGI can be aware of the credentials and
139
** capabilities of the Fossil user.
140
**
141
** The /ext page is only functional if the "extroot: DIR" setting is
142
** found in the CGI script that launched Fossil, or if the "--extroot DIR"
143
** flag is present when Fossil is launched using the "server", "ui", or
144
** "http" commands. DIR must be an absolute pathname (relative to the
145
** chroot jail) of the root of the file hierarchy that implements the CGI
146
** functionality. Executable files are CGI. Non-executable files are
147
** static content.
148
**
149
** The path after the /ext is the path to the CGI script or static file
150
** relative to DIR. For security, this path may not contain characters
151
** other than ASCII letters or digits, ".", "-", "/", and "_". If the
152
** "." or "-" characters are present in the path then they may not follow
153
** a "/".
154
**
155
** If the path after /ext ends with "/" and is the name of a directory then
156
** that directory is searched for files named "index.html", "index.wiki",
157
** and "index.md" (in that order) and if found, those filenames are
158
** appended to the path.
159
*/
160
void ext_page(void){
161
const char *zName = P("name"); /* Path information after /ext */
162
char *zPath = 0; /* Complete path from extroot */
163
int nRoot; /* Number of bytes in the extroot name */
164
char *zScript = 0; /* Name of the CGI script */
165
int nScript = 0; /* Bytes in the CGI script name */
166
const char *zFailReason = "???";/* Reason for failure */
167
int i; /* Loop counter */
168
const char *zMime = 0; /* MIME type of the reply */
169
int fdFromChild = -1; /* File descriptor for reading from child */
170
FILE *toChild = 0; /* FILE for sending to child */
171
FILE *fromChild = 0; /* FILE for reading from child */
172
int pidChild = 0; /* Process id of the child */
173
int rc; /* Reply code from subroutine call */
174
int nContent = -1; /* Content length */
175
const char *zPathInfo; /* Original PATH_INFO value */
176
char *zRestrictTag; /* Tag to restrict specific documents */
177
Blob reply; /* The reply */
178
char zLine[1000]; /* One line of the CGI reply */
179
const char *zSrvSw; /* SERVER_SOFTWARE */
180
181
zPathInfo = P("PATH_INFO");
182
login_check_credentials();
183
blob_init(&reply, 0, 0);
184
if( g.zExtRoot==0 ){
185
zFailReason = "extroot is not set";
186
goto ext_not_found;
187
}
188
if( file_is_absolute_path(g.zExtRoot)==0 ){
189
zFailReason = "extroot is a relative pathname";
190
goto ext_not_found;
191
}
192
if( zName==0 ){
193
zFailReason = "no path beyond /ext";
194
goto ext_not_found;
195
}
196
zFailReason = ext_pathname_ok(zName);
197
if( zFailReason ) goto ext_not_found;
198
zFailReason = "???";
199
if( file_isdir(g.zExtRoot,ExtFILE)!=1 ){
200
zFailReason = "extroot is not a directory";
201
goto ext_not_found;
202
}
203
zPath = mprintf("%s/%s", g.zExtRoot, zName);
204
nRoot = (int)strlen(g.zExtRoot);
205
if( file_isfile(zPath, ExtFILE) || isDirWithIndexFile(&zPath) ){
206
nScript = (int)strlen(zPath);
207
zScript = zPath;
208
}else{
209
for(i=nRoot+1; zPath[i]; i++){
210
char c = zPath[i];
211
if( c=='/' ){
212
int isDir, isFile;
213
zPath[i] = 0;
214
isDir = file_isdir(zPath, ExtFILE);
215
isFile = isDir==2 ? file_isfile(zPath, ExtFILE) : 0;
216
zPath[i] = c;
217
if( isDir==0 ){
218
zFailReason = "path does not match any file or script";
219
goto ext_not_found;
220
}
221
if( isFile!=0 ){
222
zScript = mprintf("%.*s", i, zPath);
223
nScript = i;
224
break;
225
}
226
}
227
}
228
}
229
if( nScript==0 ){
230
zFailReason = "path does not match any file or script";
231
goto ext_not_found;
232
}
233
assert( nScript>=nRoot+1 );
234
style_set_current_page("ext/%s", &zScript[nRoot+1]);
235
zRestrictTag = mprintf("ext/%s", &zScript[nRoot+1]);
236
if( robot_restrict(zRestrictTag) ) return;
237
fossil_free(zRestrictTag);
238
zMime = P("mimetype");
239
if( zMime==0 ) zMime = mimetype_from_name(zScript);
240
if( zMime==0 ) zMime = "application/octet-stream";
241
if( !file_isexe(zScript, ExtFILE) ){
242
/* File is not executable. Must be a regular file. In that case,
243
** disallow extra path elements */
244
if( zPath[nScript]!=0 ){
245
zFailReason = "extra path elements after filename";
246
goto ext_not_found;
247
}
248
blob_read_from_file(&reply, zScript, ExtFILE);
249
document_render(&reply, zMime, zName, zName);
250
return;
251
}
252
253
/* If we reach this point, that means we are dealing with an executable
254
** file name zScript. Run that file as CGI.
255
*/
256
cgi_replace_parameter("DOCUMENT_ROOT", g.zExtRoot);
257
cgi_replace_parameter("SCRIPT_FILENAME", zScript);
258
cgi_replace_parameter("SCRIPT_NAME",
259
mprintf("%T/ext/%T",g.zTop,zScript+nRoot+1));
260
cgi_replace_parameter("SCRIPT_DIRECTORY", file_dirname(zScript));
261
cgi_replace_parameter("PATH_INFO", zName + strlen(zScript+nRoot+1));
262
if( g.zLogin ){
263
cgi_replace_parameter("REMOTE_USER", g.zLogin);
264
cgi_set_parameter_nocopy("FOSSIL_USER", g.zLogin, 0);
265
}
266
cgi_set_parameter_nocopy("FOSSIL_NONCE", style_nonce(), 0);
267
cgi_set_parameter_nocopy("FOSSIL_REPOSITORY", g.zRepositoryName, 0);
268
cgi_set_parameter_nocopy("FOSSIL_URI", g.zTop, 0);
269
cgi_set_parameter_nocopy("FOSSIL_CAPABILITIES",
270
db_text("","SELECT fullcap(cap) FROM user WHERE login=%Q",
271
g.zLogin ? g.zLogin : "nobody"), 0);
272
zSrvSw = P("SERVER_SOFTWARE");
273
if( zSrvSw==0 ){
274
zSrvSw = get_version();
275
}else{
276
char *z = mprintf("fossil version %s", get_version());
277
if( strncmp(zSrvSw,z,strlen(z)-4)!=0 ){
278
zSrvSw = mprintf("%z, %s", z, zSrvSw);
279
}
280
}
281
cgi_replace_parameter("SERVER_SOFTWARE", zSrvSw);
282
cgi_replace_parameter("GATEWAY_INTERFACE","CGI/1.0");
283
for(i=0; i<(int)(sizeof(azCgiEnv)/sizeof(azCgiEnv[0])); i++){
284
(void)P(azCgiEnv[i]);
285
}
286
fossil_clearenv();
287
for(i=0; i<(int)(sizeof(azCgiEnv)/sizeof(azCgiEnv[0])); i++){
288
const char *zVal = P(azCgiEnv[i]);
289
if( zVal ) fossil_setenv(azCgiEnv[i], zVal);
290
}
291
fossil_setenv("HTTP_ACCEPT_ENCODING","");
292
rc = popen2(zScript, &fdFromChild, &toChild, &pidChild, 1);
293
if( rc ){
294
zFailReason = "cannot exec CGI child process";
295
goto ext_not_found;
296
}
297
fromChild = fdopen(fdFromChild, "rb");
298
if( fromChild==0 ){
299
zFailReason = "cannot open FILE to read from CGI child process";
300
goto ext_not_found;
301
}
302
if( blob_size(&g.cgiIn)>0 ){
303
size_t nSent, toSend;
304
unsigned char *data = (unsigned char*)blob_buffer(&g.cgiIn);
305
toSend = (size_t)blob_size(&g.cgiIn);
306
do{
307
nSent = fwrite(data, 1, toSend, toChild);
308
if( nSent<=0 ){
309
zFailReason = "unable to send all content to the CGI child process";
310
goto ext_not_found;
311
}
312
toSend -= nSent;
313
data += nSent;
314
}while( toSend>0 );
315
fflush(toChild);
316
}
317
if( g.perm.Debug && P("fossil-ext-debug")!=0 ){
318
/* For users with Debug privilege, if the "fossil-ext-debug" query
319
** parameter exists, then show raw output from the CGI */
320
zMime = "text/plain";
321
}else{
322
while( fgets(zLine,sizeof(zLine),fromChild) ){
323
for(i=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){}
324
zLine[i] = 0;
325
if( i==0 ) break;
326
if( fossil_strnicmp(zLine,"Location:",9)==0 ){
327
fclose(fromChild);
328
fclose(toChild);
329
cgi_redirect(&zLine[10]); /* no return */
330
}else if( fossil_strnicmp(zLine,"Status:",7)==0 ){
331
int j;
332
for(i=7; fossil_isspace(zLine[i]); i++){}
333
for(j=i; fossil_isdigit(zLine[j]); j++){}
334
while( fossil_isspace(zLine[j]) ){ j++; }
335
cgi_set_status(atoi(&zLine[i]), &zLine[j]);
336
}else if( fossil_strnicmp(zLine,"Content-Length:",15)==0 ){
337
nContent = atoi(&zLine[15]);
338
}else if( fossil_strnicmp(zLine,"Content-Type:",13)==0 ){
339
int j;
340
for(i=13; fossil_isspace(zLine[i]); i++){}
341
for(j=i; zLine[j] && zLine[j]!=';'; j++){}
342
zMime = mprintf("%.*s", j-i, &zLine[i]);
343
}else{
344
cgi_append_header(zLine);
345
cgi_append_header("\r\n");
346
}
347
}
348
}
349
blob_read_from_channel(&reply, fromChild, nContent);
350
zFailReason = 0; /* Indicate success */
351
352
ext_not_found:
353
fossil_free(zPath);
354
if( fromChild ){
355
fclose(fromChild);
356
}else if( fdFromChild>2 ){
357
close(fdFromChild);
358
}
359
if( toChild ) fclose(toChild);
360
if( zFailReason==0 ){
361
document_render(&reply, zMime, zName, zName);
362
}else{
363
cgi_set_status(404, "Not Found");
364
@ <h1>Not Found</h1>
365
@ <p>Page not found: %h(zPathInfo)</p>
366
if( g.perm.Debug ){
367
@ <p>Reason for failure: %h(zFailReason)</p>
368
}
369
}
370
return;
371
}
372
373
/*
374
** Create a temporary SFILE table and fill it with one entry for each file
375
** in the extension document root directory (g.zExtRoot). The SFILE table
376
** looks like this:
377
**
378
** CREATE TEMP TABLE sfile(
379
** pathname TEXT PRIMARY KEY,
380
** isexe BOOLEAN
381
** ) WITHOUT ROWID;
382
*/
383
void ext_files(void){
384
Blob base;
385
db_multi_exec(
386
"CREATE TEMP TABLE sfile(\n"
387
" pathname TEXT PRIMARY KEY,\n"
388
" isexe BOOLEAN\n"
389
") WITHOUT ROWID;"
390
);
391
blob_init(&base, g.zExtRoot, -1);
392
vfile_scan(&base, blob_size(&base),
393
SCAN_ALL|SCAN_ISEXE,
394
0, 0, ExtFILE);
395
blob_zero(&base);
396
}
397
398
/*
399
** WEBPAGE: extfilelist
400
**
401
** List all files in the extension CGI document root and its subfolders.
402
*/
403
void ext_filelist_page(void){
404
Stmt q;
405
login_check_credentials();
406
if( !g.perm.Admin ){
407
login_needed(0);
408
return;
409
}
410
ext_files();
411
style_set_current_feature("extcgi");
412
style_header("CGI Extension Filelist");
413
@ <table border="0" cellspacing="0" cellpadding="3">
414
@ <tbody>
415
db_prepare(&q, "SELECT pathname, isexe FROM sfile"
416
" ORDER BY pathname");
417
while( db_step(&q)==SQLITE_ROW ){
418
const char *zName = db_column_text(&q,0);
419
int isExe = db_column_int(&q,1);
420
@ <tr>
421
if( ext_pathname_ok(zName)!=0 ){
422
@ <td><span style="opacity:0.5;">%h(zName)</span></td>
423
@ <td>data file</td>
424
}else{
425
@ <td><a href="%R/ext/%h(zName)">%h(zName)</a></td>
426
if( isExe ){
427
@ <td>CGI</td>
428
}else{
429
@ <td>static content</td>
430
}
431
}
432
@ </tr>
433
}
434
db_finalize(&q);
435
@ </tbody>
436
@ </table>
437
style_finish_page();
438
}
439

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button