Fossil SCM

fossil-scm / src / server.c
Blame History Raw 568 lines
1
/*
2
** Copyright (c) 2006 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 module contains code to implement CGI, HTTP, SCGI, and FastCGI
19
** servers.
20
*/
21
#include "config.h"
22
#include "server.h"
23
#include <sys/types.h>
24
#include <sys/stat.h>
25
#if defined(_WIN32)
26
# include <windows.h>
27
#else
28
# include <errno.h> /* errno global */
29
#endif
30
31
/*
32
** If g.argv[2] exists then it is either the name of a repository
33
** that will be used by a server, or else it is a directory that
34
** contains multiple repositories that can be served. If g.argv[2]
35
** is a directory, the repositories it contains must be named
36
** "*.fossil". If g.argv[2] does not exists, then we must be within
37
** a check-out and the repository to be served is the repository of
38
** that check-out.
39
**
40
** Open the repository to be served if it is known. If g.argv[2] is
41
** a directory full of repositories, then set g.zRepositoryName to
42
** the name of that directory and the specific repository will be
43
** opened later by process_one_web_page() based on the content of
44
** the PATH_INFO variable.
45
**
46
** If disallowDir is set, then the directory full of repositories method
47
** is disallowed.
48
*/
49
static void find_server_repository(int disallowDir){
50
if( g.argc<3 ){
51
db_must_be_within_tree();
52
}else if( file_isdir(g.argv[2])==1 ){
53
if( disallowDir ){
54
fossil_fatal("\"%s\" is a directory, not a repository file", g.argv[2]);
55
}else{
56
g.zRepositoryName = mprintf("%s", g.argv[2]);
57
file_simplify_name(g.zRepositoryName, -1, 0);
58
}
59
}else{
60
db_open_repository(g.argv[2]);
61
}
62
}
63
64
#if !defined(_WIN32)
65
#if !defined(__DARWIN__) && !defined(__APPLE__) && !defined(__HAIKU__)
66
/*
67
** Search for an executable on the PATH environment variable.
68
** Return true (1) if found and false (0) if not found.
69
*/
70
static int binaryOnPath(const char *zBinary){
71
const char *zPath = fossil_getenv("PATH");
72
char *zFull;
73
int i;
74
int bExists;
75
while( zPath && zPath[0] ){
76
while( zPath[0]==':' ) zPath++;
77
for(i=0; zPath[i] && zPath[i]!=':'; i++){}
78
zFull = mprintf("%.*s/%s", i, zPath, zBinary);
79
bExists = file_access(zFull, X_OK);
80
fossil_free(zFull);
81
if( bExists==0 ) return 1;
82
zPath += i;
83
}
84
return 0;
85
}
86
#endif
87
#endif
88
89
/*
90
** If running as root, chroot to the directory containing the
91
** repository zRepo and then drop root privileges. Return the
92
** new repository name.
93
**
94
** zRepo might be a directory itself. In that case chroot into
95
** the directory zRepo.
96
**
97
** Assume the user-id and group-id of the repository, or if zRepo
98
** is a directory, of that directory.
99
*/
100
char *enter_chroot_jail(char *zRepo){
101
#if !defined(_WIN32)
102
if( getuid()==0 ){
103
int i;
104
struct stat sStat;
105
Blob dir;
106
char *zDir;
107
108
file_canonical_name(zRepo, &dir, 0);
109
zDir = blob_str(&dir);
110
if( file_isdir(zDir)==1 ){
111
if( file_chdir(zDir, 1) ){
112
fossil_fatal("unable to chroot into %s", zDir);
113
}
114
zRepo = "/";
115
}else{
116
for(i=strlen(zDir)-1; i>0 && zDir[i]!='/'; i--){}
117
if( zDir[i]!='/' ) fossil_panic("bad repository name: %s", zRepo);
118
if( i>0 ){
119
zDir[i] = 0;
120
if( file_chdir(zDir, 1) ){
121
fossil_fatal("unable to chroot into %s", zDir);
122
}
123
zDir[i] = '/';
124
}
125
zRepo = &zDir[i];
126
}
127
if( stat(zRepo, &sStat)!=0 ){
128
fossil_fatal("cannot stat() repository: %s", zRepo);
129
}
130
i = setgid(sStat.st_gid);
131
i = i || setuid(sStat.st_uid);
132
if(i){
133
fossil_fatal("setgid/uid() failed with errno %d", errno);
134
}
135
if( g.db!=0 ){
136
db_close(1);
137
db_open_repository(zRepo);
138
}
139
}
140
#endif
141
return zRepo;
142
}
143
144
/* If the CGI program contains one or more lines of the form
145
**
146
** redirect: repository-filename http://hostname/path/%s
147
**
148
** then control jumps here. Search each repository for an artifact ID
149
** that matches the "name" CGI parameter and for the first match,
150
** redirect to the corresponding URL with the "name" CGI parameter
151
** inserted. Paint an error page if no match is found.
152
**
153
** If there is a line of the form:
154
**
155
** redirect: * URL
156
**
157
** Then a redirect is made to URL if no match is found. Otherwise a
158
** very primitive error message is returned.
159
*/
160
static void redirect_web_page(int nRedirect, char **azRedirect){
161
int i; /* Loop counter */
162
const char *zNotFound = 0; /* Not found URL */
163
const char *zName = P("name");
164
set_base_url(0);
165
if( zName==0 ){
166
zName = P("SCRIPT_NAME");
167
if( zName && zName[0]=='/' ) zName++;
168
}
169
if( zName && validate16(zName, strlen(zName)) ){
170
for(i=0; i<nRedirect; i++){
171
if( fossil_strcmp(azRedirect[i*2],"*")==0 ){
172
zNotFound = azRedirect[i*2+1];
173
continue;
174
}
175
db_open_repository(azRedirect[i*2]);
176
if( db_exists("SELECT 1 FROM blob WHERE uuid GLOB '%s*'", zName) ){
177
cgi_redirectf(azRedirect[i*2+1], zName);
178
return;
179
}
180
db_close(1);
181
}
182
}
183
if( zNotFound ){
184
cgi_redirectf(zNotFound, zName);
185
}else{
186
@ <html>
187
@ <head><title>No Such Object</title></head>
188
@ <body>
189
@ <p>No such object: <b>%h(zName)</b></p>
190
@ </body>
191
cgi_reply();
192
}
193
}
194
195
/*
196
** COMMAND: cgi*
197
**
198
** Usage: %fossil ?cgi? SCRIPT
199
**
200
** The SCRIPT argument is the name of a file that is the CGI script
201
** that is being run. The command name, "cgi", may be omitted if
202
** the GATEWAY_INTERFACE environment variable is set to "CGI" (which
203
** should always be the case for CGI scripts run by a webserver.) The
204
** SCRIPT file should look something like this:
205
**
206
** #!/usr/bin/fossil
207
** repository: /home/somebody/project.db
208
**
209
** The second line defines the name of the repository. After locating
210
** the repository, fossil will generate a webpage on stdout based on
211
** the values of standard CGI environment variables.
212
**
213
** See also: http, server, winsrv
214
*/
215
void cmd_cgi(void){
216
const char *zFile;
217
const char *zNotFound = 0;
218
char **azRedirect = 0; /* List of repositories to redirect to */
219
int nRedirect = 0; /* Number of entries in azRedirect */
220
Glob *pFileGlob = 0; /* Pattern for files */
221
Blob config, line, key, value, value2;
222
if( g.argc==3 && fossil_strcmp(g.argv[1],"cgi")==0 ){
223
zFile = g.argv[2];
224
}else{
225
zFile = g.argv[1];
226
}
227
g.httpOut = stdout;
228
g.httpIn = stdin;
229
fossil_binary_mode(g.httpOut);
230
fossil_binary_mode(g.httpIn);
231
g.cgiOutput = 1;
232
blob_read_from_file(&config, zFile);
233
while( blob_line(&config, &line) ){
234
if( !blob_token(&line, &key) ) continue;
235
if( blob_buffer(&key)[0]=='#' ) continue;
236
if( blob_eq(&key, "debug:") && blob_token(&line, &value) ){
237
g.fDebug = fossil_fopen(blob_str(&value), "ab");
238
blob_reset(&value);
239
continue;
240
}
241
if( blob_eq(&key, "HOME:") && blob_token(&line, &value) ){
242
cgi_setenv("HOME", blob_str(&value));
243
blob_reset(&value);
244
continue;
245
}
246
if( blob_eq(&key, "repository:") && blob_tail(&line, &value) ){
247
blob_trim(&value);
248
db_open_repository(blob_str(&value));
249
blob_reset(&value);
250
continue;
251
}
252
if( blob_eq(&key, "directory:") && blob_token(&line, &value) ){
253
db_close(1);
254
g.zRepositoryName = mprintf("%s", blob_str(&value));
255
blob_reset(&value);
256
continue;
257
}
258
if( blob_eq(&key, "notfound:") && blob_token(&line, &value) ){
259
zNotFound = mprintf("%s", blob_str(&value));
260
blob_reset(&value);
261
continue;
262
}
263
if( blob_eq(&key, "localauth") ){
264
g.useLocalauth = 1;
265
continue;
266
}
267
if( blob_eq(&key, "redirect:") && blob_token(&line, &value)
268
&& blob_token(&line, &value2) ){
269
nRedirect++;
270
azRedirect = fossil_realloc(azRedirect, 2*nRedirect*sizeof(char*));
271
azRedirect[nRedirect*2-2] = mprintf("%s", blob_str(&value));
272
azRedirect[nRedirect*2-1] = mprintf("%s", blob_str(&value2));
273
blob_reset(&value);
274
blob_reset(&value2);
275
continue;
276
}
277
if( blob_eq(&key, "files:") && blob_token(&line, &value) ){
278
pFileGlob = glob_create(blob_str(&value));
279
continue;
280
}
281
}
282
blob_reset(&config);
283
if( g.db==0 && g.zRepositoryName==0 && nRedirect==0 ){
284
cgi_panic("Unable to find or open the project repository");
285
}
286
cgi_init();
287
if( nRedirect ){
288
redirect_web_page(nRedirect, azRedirect);
289
}else{
290
process_one_web_page(zNotFound, pFileGlob);
291
}
292
}
293
294
295
/*
296
** undocumented format:
297
**
298
** fossil http REPOSITORY INFILE OUTFILE IPADDR
299
**
300
** The argv==6 form is used by the win32 server only.
301
**
302
** COMMAND: http*
303
**
304
** Usage: %fossil http REPOSITORY ?OPTIONS?
305
**
306
** Handle a single HTTP request appearing on stdin. The resulting webpage
307
** is delivered on stdout. This method is used to launch an HTTP request
308
** handler from inetd, for example. The argument is the name of the
309
** repository.
310
**
311
** If REPOSITORY is a directory that contains one or more repositories,
312
** either directly in REPOSITORY itself, or in subdirectories, and
313
** with names of the form "*.fossil" then the a prefix of the URL pathname
314
** selects from among the various repositories. If the pathname does
315
** not select a valid repository and the --notfound option is available,
316
** then the server redirects (HTTP code 302) to the URL of --notfound.
317
** When REPOSITORY is a directory, the pathname must contain only
318
** alphanumerics, "_", "/", "-" and "." and no "-" may occur after a "/"
319
** and every "." must be surrounded on both sides by alphanumerics or else
320
** a 404 error is returned. Static content files in the directory are
321
** returned if they match comma-separate GLOB pattern specified by --files
322
** and do not match "*.fossil*" and have a well-known suffix.
323
**
324
** The --host option can be used to specify the hostname for the server.
325
** The --https option indicates that the request came from HTTPS rather
326
** than HTTP. If --nossl is given, then SSL connections will not be available,
327
** thus also no redirecting from http: to https: will take place.
328
**
329
** If the --localauth option is given, then automatic login is performed
330
** for requests coming from localhost, if the "localauth" setting is not
331
** enabled.
332
**
333
** Options:
334
** --localauth enable automatic login for local connections
335
** --host NAME specify hostname of the server
336
** --https signal a request coming in via https
337
** --nossl signal that no SSL connections are available
338
** --notfound URL use URL as "HTTP 404, object not found" page.
339
** --files GLOB comma-separate glob patterns for static file to serve
340
** --baseurl URL base URL (useful with reverse proxies)
341
**
342
** See also: cgi, server, winsrv
343
*/
344
void cmd_http(void){
345
const char *zIpAddr;
346
const char *zNotFound;
347
const char *zHost;
348
const char *zAltBase;
349
const char *zFileGlob;
350
351
/* The winhttp module passes the --files option as --files-urlenc with
352
** the argument being URL encoded, to avoid wildcard expansion in the
353
** shell. This option is for internal use and is undocumented.
354
*/
355
zFileGlob = find_option("files-urlenc",0,1);
356
if( zFileGlob ){
357
char *z = mprintf("%s", zFileGlob);
358
dehttpize(z);
359
zFileGlob = z;
360
}else{
361
zFileGlob = find_option("files",0,1);
362
}
363
zNotFound = find_option("notfound", 0, 1);
364
g.useLocalauth = find_option("localauth", 0, 0)!=0;
365
g.sslNotAvailable = find_option("nossl", 0, 0)!=0;
366
zAltBase = find_option("baseurl", 0, 1);
367
if( zAltBase ) set_base_url(zAltBase);
368
if( find_option("https",0,0)!=0 ) cgi_replace_parameter("HTTPS","on");
369
zHost = find_option("host", 0, 1);
370
if( zHost ) cgi_replace_parameter("HTTP_HOST",zHost);
371
g.cgiOutput = 1;
372
if( g.argc!=2 && g.argc!=3 && g.argc!=6 ){
373
fossil_fatal("no repository specified");
374
}
375
g.fullHttpReply = 1;
376
if( g.argc==6 ){
377
g.httpIn = fossil_fopen(g.argv[3], "rb");
378
g.httpOut = fossil_fopen(g.argv[4], "wb");
379
zIpAddr = g.argv[5];
380
}else{
381
g.httpIn = stdin;
382
g.httpOut = stdout;
383
zIpAddr = 0;
384
}
385
find_server_repository(0);
386
g.zRepositoryName = enter_chroot_jail(g.zRepositoryName);
387
cgi_handle_http_request(zIpAddr);
388
process_one_web_page(zNotFound, glob_create(zFileGlob));
389
}
390
391
/*
392
** Note that the following command is used by ssh:// processing.
393
**
394
** COMMAND: test-http
395
** Works like the http command but gives setup permission to all users.
396
*/
397
void cmd_test_http(void){
398
Th_InitTraceLog();
399
login_set_capabilities("sx", 0);
400
g.useLocalauth = 1;
401
cgi_set_parameter("REMOTE_ADDR", "127.0.0.1");
402
g.httpIn = stdin;
403
g.httpOut = stdout;
404
find_server_repository(0);
405
g.cgiOutput = 1;
406
g.fullHttpReply = 1;
407
cgi_handle_http_request(0);
408
process_one_web_page(0, 0);
409
}
410
411
412
/*
413
** COMMAND: server*
414
** COMMAND: ui
415
**
416
** Usage: %fossil server ?OPTIONS? ?REPOSITORY?
417
** Or: %fossil ui ?OPTIONS? ?REPOSITORY?
418
**
419
** Open a socket and begin listening and responding to HTTP requests on
420
** TCP port 8080, or on any other TCP port defined by the -P or
421
** --port option. The optional argument is the name of the repository.
422
** The repository argument may be omitted if the working directory is
423
** within an open checkout.
424
**
425
** The "ui" command automatically starts a web browser after initializing
426
** the web server. The "ui" command also binds to 127.0.0.1 and so will
427
** only process HTTP traffic from the local machine.
428
**
429
** The REPOSITORY can be a directory (aka folder) that contains one or
430
** more repositories with names ending in ".fossil". In this case, the
431
** a prefix of the URL pathname is used to search the directory for an
432
** appropriate repository. To thwart mischief, the pathname in the URL must
433
** contain only alphanumerics, "_", "/", "-", and ".", and no "-" may
434
** occur after "/", and every "." must be surrounded on both sides by
435
** alphanumerics. Any pathname that does not satisfy these constraints
436
** results in a 404 error. Files in REPOSITORY that match the comma-separated
437
** list of glob patterns given by --files and that have known suffixes
438
** such as ".txt" or ".html" or ".jpeg" and do not match the pattern
439
** "*.fossil*" will be served as static content. With the "ui" command,
440
** the REPOSITORY can only be a directory if the --notfound option is
441
** also present.
442
**
443
** By default, the "ui" command provides full administrative access without
444
** having to log in. This can be disabled by setting turning off the
445
** "localauth" setting. Automatic login for the "server" command is available
446
** if the --localauth option is present and the "localauth" setting is off
447
** and the connection is from localhost. The optional REPOSITORY argument
448
** to "ui" may be a directory and will function as "server" if and only if
449
** the --notfound option is used.
450
**
451
** Options:
452
** --localauth enable automatic login for requests from localhost
453
** --localhost listen on 127.0.0.1 only (always true for "ui")
454
** -P|--port TCPPORT listen to request on port TCPPORT
455
** --th-trace trace TH1 execution (for debugging purposes)
456
** --baseurl URL Use URL as the base (useful for reverse proxies)
457
** --notfound URL Redirect
458
** --files GLOBLIST Comma-separated list of glob patterns for static files
459
**
460
** See also: cgi, http, winsrv
461
*/
462
void cmd_webserver(void){
463
int iPort, mxPort; /* Range of TCP ports allowed */
464
const char *zPort; /* Value of the --port option */
465
const char *zBrowser; /* Name of web browser program */
466
char *zBrowserCmd = 0; /* Command to launch the web browser */
467
int isUiCmd; /* True if command is "ui", not "server' */
468
const char *zNotFound; /* The --notfound option or NULL */
469
int flags = 0; /* Server flags */
470
const char *zAltBase; /* Argument to the --baseurl option */
471
const char *zFileGlob; /* Static content must match this */
472
char *zIpAddr = 0; /* Bind to this IP address */
473
474
#if defined(_WIN32)
475
const char *zStopperFile; /* Name of file used to terminate server */
476
zStopperFile = find_option("stopper", 0, 1);
477
#endif
478
479
zFileGlob = find_option("files", 0, 1);
480
g.useLocalauth = find_option("localauth", 0, 0)!=0;
481
Th_InitTraceLog();
482
zPort = find_option("port", "P", 1);
483
zNotFound = find_option("notfound", 0, 1);
484
zAltBase = find_option("baseurl", 0, 1);
485
if( zAltBase ){
486
set_base_url(zAltBase);
487
}
488
if ( find_option("localhost", 0, 0)!=0 ){
489
flags |= HTTP_SERVER_LOCALHOST;
490
}
491
if( g.argc!=2 && g.argc!=3 ) usage("?REPOSITORY?");
492
isUiCmd = g.argv[1][0]=='u';
493
if( isUiCmd ){
494
flags |= HTTP_SERVER_LOCALHOST;
495
g.useLocalauth = 1;
496
}
497
find_server_repository(isUiCmd && zNotFound==0);
498
if( zPort ){
499
int i;
500
for(i=strlen(zPort)-1; i>=0 && zPort[i]!=':'; i--){}
501
if( i>0 ){
502
zIpAddr = mprintf("%.*s", i, zPort);
503
zPort += i+1;
504
}
505
iPort = mxPort = atoi(zPort);
506
}else{
507
iPort = db_get_int("http-port", 8080);
508
mxPort = iPort+100;
509
}
510
#if !defined(_WIN32)
511
/* Unix implementation */
512
if( isUiCmd ){
513
#if !defined(__DARWIN__) && !defined(__APPLE__) && !defined(__HAIKU__)
514
zBrowser = db_get("web-browser", 0);
515
if( zBrowser==0 ){
516
static const char *const azBrowserProg[] =
517
{ "xdg-open", "gnome-open", "firefox", "google-chrome" };
518
int i;
519
zBrowser = "echo";
520
for(i=0; i<sizeof(azBrowserProg)/sizeof(azBrowserProg[0]); i++){
521
if( binaryOnPath(azBrowserProg[i]) ){
522
zBrowser = azBrowserProg[i];
523
break;
524
}
525
}
526
}
527
#else
528
zBrowser = db_get("web-browser", "open");
529
#endif
530
if( zIpAddr ){
531
zBrowserCmd = mprintf("%s http://%s:%%d/ &", zBrowser, zIpAddr);
532
}else{
533
zBrowserCmd = mprintf("%s http://localhost:%%d/ &", zBrowser);
534
}
535
}
536
db_close(1);
537
if( cgi_http_server(iPort, mxPort, zBrowserCmd, zIpAddr, flags) ){
538
fossil_fatal("unable to listen on TCP socket %d", iPort);
539
}
540
g.sslNotAvailable = 1;
541
g.httpIn = stdin;
542
g.httpOut = stdout;
543
if( g.fHttpTrace || g.fSqlTrace ){
544
fprintf(stderr, "====== SERVER pid %d =======\n", getpid());
545
}
546
g.cgiOutput = 1;
547
find_server_repository(isUiCmd && zNotFound==0);
548
g.zRepositoryName = enter_chroot_jail(g.zRepositoryName);
549
cgi_handle_http_request(0);
550
process_one_web_page(zNotFound, glob_create(zFileGlob));
551
#else
552
/* Win32 implementation */
553
if( isUiCmd ){
554
zBrowser = db_get("web-browser", "start");
555
if( zIpAddr ){
556
zBrowserCmd = mprintf("%s http://%s:%%d/ &", zBrowser, zIpAddr);
557
}else{
558
zBrowserCmd = mprintf("%s http://localhost:%%d/ &", zBrowser);
559
}
560
}
561
db_close(1);
562
if( win32_http_service(iPort, zNotFound, zFileGlob, flags) ){
563
win32_http_server(iPort, mxPort, zBrowserCmd,
564
zStopperFile, zNotFound, zFileGlob, zIpAddr, flags);
565
}
566
#endif
567
}
568

Keyboard Shortcuts

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