Fossil SCM

fossil-scm / src / http_transport.c
Blame History Raw 546 lines
1
/*
2
** Copyright (c) 2009 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 implements the transport layer for the client side HTTP
19
** connection. The purpose of this layer is to provide a common interface
20
** for both HTTP and HTTPS and to provide a common "fetch one line"
21
** interface that is used for parsing the reply.
22
*/
23
#include "config.h"
24
#include "http_transport.h"
25
26
/*
27
** State information
28
*/
29
static struct {
30
int isOpen; /* True when the transport layer is open */
31
char *pBuf; /* Buffer used to hold the reply */
32
int nAlloc; /* Space allocated for transportBuf[] */
33
int nUsed ; /* Space of transportBuf[] used */
34
int iCursor; /* Next unread by in transportBuf[] */
35
i64 nSent; /* Number of bytes sent */
36
i64 nRcvd; /* Number of bytes received */
37
FILE *pFile; /* File I/O for FILE: */
38
char *zOutFile; /* Name of outbound file for FILE: */
39
char *zInFile; /* Name of inbound file for FILE: */
40
FILE *pLog; /* Log output here */
41
int bTest; /* file:// testing */
42
} transport = {
43
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
44
};
45
46
/*
47
** Information about the connection to the SSH subprocess when
48
** using the ssh:// sync method.
49
*/
50
static int sshPid; /* Process id of ssh subprocess */
51
static int sshIn; /* From ssh subprocess to this process */
52
static FILE *sshOut; /* From this to ssh subprocess */
53
54
55
/*
56
** Return the current transport error message.
57
*/
58
const char *transport_errmsg(UrlData *pUrlData){
59
#ifdef FOSSIL_ENABLE_SSL
60
if( pUrlData->isHttps ){
61
return ssl_errmsg();
62
}
63
#endif
64
return socket_errmsg();
65
}
66
67
/*
68
** Retrieve send/receive counts from the transport layer. If "resetFlag"
69
** is true, then reset the counts.
70
*/
71
void transport_stats(i64 *pnSent, i64 *pnRcvd, int resetFlag){
72
if( pnSent ) *pnSent = transport.nSent;
73
if( pnRcvd ) *pnRcvd = transport.nRcvd;
74
if( resetFlag ){
75
transport.nSent = 0;
76
transport.nRcvd = 0;
77
}
78
}
79
80
/*
81
** Check zFossil to see if it is a reasonable "fossil" command to
82
** run on the server. Do not allow an attacker to substitute something
83
** like "/bin/rm".
84
*/
85
static int is_safe_fossil_command(const char *zFossil){
86
static const char *const azSafe[] = { "*/fossil", "*/fossil.exe", "*/echo" };
87
int i;
88
for(i=0; i<(int)(sizeof(azSafe)/sizeof(azSafe[0])); i++){
89
if( sqlite3_strglob(azSafe[i], zFossil)==0 ) return 1;
90
if( strcmp(azSafe[i]+2, zFossil)==0 ) return 1;
91
}
92
return 0;
93
}
94
95
/*
96
** Default SSH command
97
*/
98
#if 0 /* was: defined(_WIN32). Windows generally has ssh now. */
99
static const char zDefaultSshCmd[] = "plink -ssh";
100
#else
101
static const char zDefaultSshCmd[] = "ssh -e none";
102
#endif
103
104
/*
105
** Initialize a Blob to the name of the configured SSH command.
106
*/
107
void transport_ssh_command(Blob *p){
108
char *zSsh; /* The base SSH command */
109
zSsh = g.zSshCmd;
110
if( zSsh==0 || zSsh[0]==0 ){
111
zSsh = db_get("ssh-command", zDefaultSshCmd);
112
}
113
blob_init(p, zSsh, -1);
114
}
115
116
/*
117
** SSH initialization of the transport layer
118
*/
119
int transport_ssh_open(UrlData *pUrlData){
120
/* For SSH we need to create and run SSH fossil http
121
** to talk to the remote machine.
122
*/
123
Blob zCmd; /* The SSH command */
124
char *zHost; /* The host name to contact */
125
126
fossil_free(g.zIpAddr);
127
g.zIpAddr = fossil_strdup(pUrlData->name);
128
transport_ssh_command(&zCmd);
129
if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){
130
blob_appendf(&zCmd, " -p %d", pUrlData->port);
131
}
132
blob_appendf(&zCmd, " -T --"); /* End of switches */
133
if( pUrlData->user && pUrlData->user[0] ){
134
zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name);
135
blob_append_escaped_arg(&zCmd, zHost, 0);
136
fossil_free(zHost);
137
}else{
138
blob_append_escaped_arg(&zCmd, pUrlData->name, 0);
139
}
140
if( (pUrlData->flags & URL_SSH_EXE)!=0
141
&& !is_safe_fossil_command(pUrlData->fossil)
142
){
143
fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
144
"the server.", pUrlData->fossil);
145
}
146
if( (pUrlData->flags & URL_SSH_EXE)==0
147
&& (pUrlData->flags & URL_SSH_PATH)!=0
148
){
149
ssh_add_path_argument(&zCmd);
150
}
151
blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1);
152
blob_append(&zCmd, " test-http", 10);
153
if( pUrlData->path && pUrlData->path[0] ){
154
blob_append_escaped_arg(&zCmd, pUrlData->path, 1);
155
}else{
156
fossil_fatal("ssh:// URI does not specify a path to the repository");
157
}
158
if( g.fSshTrace || g.fHttpTrace ){
159
fossil_print("RUN %s\n", blob_str(&zCmd)); /* Show the whole SSH command */
160
}
161
popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid, 0);
162
if( sshPid==0 ){
163
socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd);
164
}
165
blob_reset(&zCmd);
166
return sshPid==0;
167
}
168
169
/*
170
** Return true if the filename given matches the pattern that means
171
** it is a test-reply.
172
*/
173
static int transport_is_test(const char *zFilename){
174
return sqlite3_strglob("*.http-test",zFilename)==0;
175
}
176
177
/*
178
** Open a connection to the server. The server is defined by the following
179
** variables:
180
**
181
** pUrlData->name Name of the server. Ex: fossil-scm.org
182
**
183
** pUrlData->port TCP/IP port. Ex: 80
184
**
185
** pUrlData->isHttps Use TLS for the connection
186
**
187
** pUrlData->isSsh Use an SSH connection to the server. name is
188
** the SSH host name. subpath is the server
189
** method name (ex: timeline)
190
**
191
** pUrlData->isFile The .name is a filename in the local filesystem.
192
** If that file is a Fossil repo, then launch
193
** "fossil http" as a subprocess to handle the request.
194
** Or if filename ends with ".http-test" then the
195
** content of that file becomes the HTTP reply.
196
** (The latter is used for testing only.)
197
**
198
** pUrlData->subpath The method name on the request for SSH and FILE
199
**
200
** Return the number of errors.
201
*/
202
int transport_open(UrlData *pUrlData){
203
int rc = 0;
204
if( transport.isOpen==0 ){
205
if( pUrlData->isSsh ){
206
rc = transport_ssh_open(pUrlData);
207
if( rc==0 ) transport.isOpen = 1;
208
}else if( pUrlData->isHttps ){
209
#ifdef FOSSIL_ENABLE_SSL
210
rc = ssl_open_client(pUrlData);
211
if( rc==0 ) transport.isOpen = 1;
212
#else
213
socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support");
214
rc = 1;
215
#endif
216
}else if( pUrlData->isFile ){
217
if( !file_isfile(pUrlData->name, ExtFILE) ){
218
fossil_fatal("no such file: \"%s\"", pUrlData->name);
219
}
220
transport.zOutFile = fossil_temp_filename();
221
transport.zInFile = fossil_temp_filename();
222
if( !db_looks_like_a_repository(pUrlData->name) ){
223
if( transport_is_test(pUrlData->name) ){
224
/* If the URI filename ends with ".http-test" then the content
225
** of that file becomes the HTTP reply and the request is stored
226
** in a file with the same name but with ".request" added to the
227
** end. Used for testing only */
228
transport.bTest = 1;
229
sqlite3_free(transport.zOutFile);
230
transport.zOutFile = sqlite3_mprintf("%s.request",pUrlData->name);
231
sqlite3_free(transport.zInFile);
232
transport.zInFile = sqlite3_mprintf("%s",pUrlData->name);
233
}else{
234
fossil_fatal("not a fossil repository: \"%s\"", pUrlData->name);
235
}
236
}
237
transport.pFile = fossil_fopen(transport.zOutFile, "wb");
238
if( transport.pFile==0 ){
239
fossil_fatal("cannot output temporary file: %s", transport.zOutFile);
240
}
241
transport.isOpen = 1;
242
}else{
243
rc = socket_open(pUrlData);
244
if( rc==0 ) transport.isOpen = 1;
245
}
246
}
247
return rc;
248
}
249
250
/*
251
** Close the current connection
252
*/
253
void transport_close(UrlData *pUrlData){
254
if( transport.isOpen ){
255
free(transport.pBuf);
256
transport.pBuf = 0;
257
transport.nAlloc = 0;
258
transport.nUsed = 0;
259
transport.iCursor = 0;
260
if( transport.pLog ){
261
fclose(transport.pLog);
262
transport.pLog = 0;
263
}
264
if( pUrlData->isSsh ){
265
transport_ssh_close();
266
}else if( pUrlData->isHttps ){
267
#ifdef FOSSIL_ENABLE_SSL
268
ssl_close_client();
269
#endif
270
}else if( pUrlData->isFile ){
271
if( transport.pFile ){
272
fclose(transport.pFile);
273
transport.pFile = 0;
274
}
275
if( !transport.bTest ){
276
file_delete(transport.zInFile);
277
file_delete(transport.zOutFile);
278
}
279
sqlite3_free(transport.zInFile);
280
sqlite3_free(transport.zOutFile);
281
}else{
282
socket_close();
283
}
284
transport.isOpen = 0;
285
}
286
}
287
288
/*
289
** Send content over the wire.
290
*/
291
void transport_send(UrlData const *pUrlData, const Blob *toSend){
292
char *z = blob_buffer(toSend);
293
int n = blob_size(toSend);
294
transport.nSent += n;
295
if( pUrlData->isSsh ){
296
fwrite(z, 1, n, sshOut);
297
fflush(sshOut);
298
}else if( pUrlData->isHttps ){
299
#ifdef FOSSIL_ENABLE_SSL
300
int sent;
301
while( n>0 ){
302
sent = ssl_send(0, z, n);
303
/* printf("Sent %d of %d bytes\n", sent, n); fflush(stdout); */
304
if( sent<=0 ) break;
305
n -= sent;
306
}
307
#endif
308
}else if( pUrlData->isFile ){
309
fwrite(z, 1, n, transport.pFile);
310
}else{
311
int sent;
312
while( n>0 ){
313
sent = socket_send(0, z, n);
314
/* printf("Sent %d of %d bytes\n", sent, n); fflush(stdout); */
315
if( sent<=0 ) break;
316
n -= sent;
317
}
318
}
319
}
320
321
/*
322
** This routine is called when the outbound message is complete and
323
** it is time to begin receiving a reply.
324
**
325
** This is a no-op for http:, https:, and ssh:. It only does this
326
** for file:. If the file is a Fossil database, then an instance of
327
** "fossil http" is invoked in a subprocess to handle the request.
328
** Or if the filename ends with ".http-test" then the text of that
329
** file becomes the reply. The ".http-test" hack is for testing purposes.
330
*/
331
void transport_flip(UrlData *pUrlData){
332
if( pUrlData->isFile ){
333
char *zCmd;
334
fclose(transport.pFile);
335
if( !transport.bTest ){
336
zCmd = mprintf("%$ http --in %$ --out %$ --ipaddr 127.0.0.1"
337
" %$ --localauth",
338
g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name
339
);
340
if( g.fHttpTrace ) fossil_print("RUN %s\n", zCmd);
341
fossil_system(zCmd);
342
free(zCmd);
343
}
344
transport.pFile = fossil_fopen(transport.zInFile, "rb");
345
if( transport.pFile==0 ){
346
fossil_fatal("unable to open HTTP reply: \"%s\"",transport.zInFile);
347
}
348
}
349
}
350
351
/*
352
** Log all input to a file. The transport layer will take responsibility
353
** for closing the log file when it is done.
354
*/
355
void transport_log(FILE *pLog){
356
if( transport.pLog ){
357
fclose(transport.pLog);
358
transport.pLog = 0;
359
}
360
transport.pLog = pLog;
361
}
362
363
/*
364
** This routine is called when the inbound message has been received
365
** and it is time to start sending again.
366
*/
367
void transport_rewind(UrlData *pUrlData){
368
if( pUrlData->isFile ){
369
transport_close(pUrlData);
370
}
371
}
372
373
/*
374
** Read N bytes of content directly from the wire and write into
375
** the buffer.
376
*/
377
static int transport_fetch(UrlData *pUrlData, char *zBuf, int N){
378
int got;
379
if( pUrlData->isSsh ){
380
int x;
381
int wanted = N;
382
got = 0;
383
while( wanted>0 ){
384
x = read(sshIn, &zBuf[got], wanted);
385
if( x<=0 ) break;
386
got += x;
387
wanted -= x;
388
}
389
}else if( pUrlData->isHttps ){
390
#ifdef FOSSIL_ENABLE_SSL
391
got = ssl_receive(0, zBuf, N);
392
#else
393
got = 0;
394
#endif
395
}else if( pUrlData->isFile ){
396
got = fread(zBuf, 1, N, transport.pFile);
397
}else{
398
got = socket_receive(0, zBuf, N, 0);
399
}
400
/* printf("received %d of %d bytes\n", got, N); fflush(stdout); */
401
if( transport.pLog ){
402
fwrite(zBuf, 1, got, transport.pLog);
403
fflush(transport.pLog);
404
}
405
return got;
406
}
407
408
/*
409
** Read N bytes of content from the wire and store in the supplied buffer.
410
** Return the number of bytes actually received.
411
*/
412
int transport_receive(UrlData *pUrlData, char *zBuf, int N){
413
int onHand; /* Bytes current held in the transport buffer */
414
int nByte = 0; /* Bytes of content received */
415
416
onHand = transport.nUsed - transport.iCursor;
417
if( g.fSshTrace){
418
printf("Reading %d bytes with %d on hand... ", N, onHand);
419
fflush(stdout);
420
}
421
if( onHand>0 ){
422
int toMove = onHand;
423
if( toMove>N ) toMove = N;
424
/* printf("bytes on hand: %d of %d\n", toMove, N); fflush(stdout); */
425
memcpy(zBuf, &transport.pBuf[transport.iCursor], toMove);
426
transport.iCursor += toMove;
427
if( transport.iCursor>=transport.nUsed ){
428
transport.nUsed = 0;
429
transport.iCursor = 0;
430
}
431
N -= toMove;
432
zBuf += toMove;
433
nByte += toMove;
434
}
435
if( N>0 ){
436
int got = transport_fetch(pUrlData, zBuf, N);
437
if( got>0 ){
438
nByte += got;
439
transport.nRcvd += got;
440
}
441
}
442
if( g.fSshTrace ) printf("Got %d bytes\n", nByte);
443
return nByte;
444
}
445
446
/*
447
** Load up to N new bytes of content into the transport.pBuf buffer.
448
** The buffer itself might be moved. And the transport.iCursor value
449
** might be reset to 0.
450
*/
451
static void transport_load_buffer(UrlData *pUrlData, int N){
452
int i, j;
453
if( transport.nAlloc==0 ){
454
transport.nAlloc = N;
455
transport.pBuf = fossil_malloc( N );
456
transport.iCursor = 0;
457
transport.nUsed = 0;
458
}
459
if( transport.iCursor>0 ){
460
for(i=0, j=transport.iCursor; j<transport.nUsed; i++, j++){
461
transport.pBuf[i] = transport.pBuf[j];
462
}
463
transport.nUsed -= transport.iCursor;
464
transport.iCursor = 0;
465
}
466
if( transport.nUsed + N > transport.nAlloc ){
467
char *pNew;
468
transport.nAlloc = transport.nUsed + N;
469
pNew = fossil_realloc(transport.pBuf, transport.nAlloc);
470
transport.pBuf = pNew;
471
}
472
if( N>0 ){
473
i = transport_fetch(pUrlData, &transport.pBuf[transport.nUsed], N);
474
if( i>0 ){
475
transport.nRcvd += i;
476
transport.nUsed += i;
477
}
478
}
479
}
480
481
/*
482
** Fetch a single line of input where a line is all text up to the next
483
** \n character or until the end of input. Remove all trailing whitespace
484
** from the received line and zero-terminate the result. Return a pointer
485
** to the line.
486
**
487
** Each call to this routine potentially overwrites the returned buffer.
488
*/
489
char *transport_receive_line(UrlData *pUrlData){
490
int i;
491
int iStart;
492
493
i = iStart = transport.iCursor;
494
while(1){
495
if( i >= transport.nUsed ){
496
transport_load_buffer(pUrlData, pUrlData->isSsh ? 2 : 1000);
497
i -= iStart;
498
iStart = 0;
499
if( i >= transport.nUsed ){
500
transport.pBuf[i] = 0;
501
transport.iCursor = i;
502
break;
503
}
504
}
505
if( transport.pBuf[i]=='\n' ){
506
transport.iCursor = i+1;
507
while( i>=iStart && fossil_isspace(transport.pBuf[i]) ){
508
transport.pBuf[i] = 0;
509
i--;
510
}
511
break;
512
}
513
i++;
514
}
515
if( g.fSshTrace ) printf("Got line: [%s]\n", &transport.pBuf[iStart]);
516
return &transport.pBuf[iStart];
517
}
518
519
/*
520
** Global transport shutdown
521
*/
522
void transport_global_shutdown(UrlData *pUrlData){
523
if( pUrlData->isSsh ){
524
transport_ssh_close();
525
}
526
if( pUrlData->isHttps ){
527
#ifdef FOSSIL_ENABLE_SSL
528
ssl_global_shutdown();
529
#endif
530
}else{
531
socket_global_shutdown();
532
}
533
}
534
535
/*
536
** Close SSH transport.
537
*/
538
void transport_ssh_close(void){
539
if( sshPid ){
540
/*printf("Closing SSH tunnel: ");*/
541
fflush(stdout);
542
pclose2(sshIn, sshOut, sshPid);
543
sshPid = 0;
544
}
545
}
546

Keyboard Shortcuts

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