Fossil SCM

fossil-scm / src / http.c
Blame History Raw 912 lines
1
/*
2
** Copyright (c) 2007 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 that implements the client-side HTTP protocol
19
*/
20
#include "config.h"
21
#include "http.h"
22
#include <assert.h>
23
24
#ifdef _WIN32
25
#include <io.h>
26
#endif
27
28
29
#if INTERFACE
30
/*
31
** Bits of the mHttpFlags parameter to http_exchange()
32
*/
33
#define HTTP_USE_LOGIN 0x00001 /* Add a login card to the sync message */
34
#define HTTP_GENERIC 0x00002 /* Generic HTTP request */
35
#define HTTP_VERBOSE 0x00004 /* HTTP status messages */
36
#define HTTP_QUIET 0x00008 /* No surplus output */
37
#define HTTP_NOCOMPRESS 0x00010 /* Omit payload compression */
38
#endif
39
40
/* Maximum number of HTTP Authorization attempts */
41
#define MAX_HTTP_AUTH 2
42
43
/* Keep track of HTTP Basic Authorization failures */
44
static int fSeenHttpAuth = 0;
45
46
/* The N value for most recent http-request-N.txt and http-reply-N.txt
47
** trace files.
48
*/
49
static int traceCnt = 0;
50
51
/*
52
** Construct the "login" card with the client credentials.
53
**
54
** login LOGIN NONCE SIGNATURE
55
**
56
** The LOGIN is the user id of the client. NONCE is the sha1 checksum
57
** of all payload that follows the login card. Randomness for the
58
** NONCE must be provided in the payload (in xfer.c) (e.g. by
59
** appending a timestamp or random bytes as a comment line to the
60
** payload). SIGNATURE is the sha1 checksum of the nonce followed by
61
** the fossil-hashed version of the user's password.
62
**
63
** Write the constructed login card into pLogin. The result does not
64
** have an EOL added to it because which type of EOL it needs has to
65
** be determined later. pLogin is initialized by this routine.
66
*/
67
static void http_build_login_card(Blob * const pPayload, Blob * const pLogin){
68
Blob nonce; /* The nonce */
69
const char *zLogin; /* The user login name */
70
const char *zPw; /* The user password */
71
Blob pw; /* The nonce with user password appended */
72
Blob sig; /* The signature field */
73
74
blob_zero(pLogin);
75
if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){
76
return; /* No login card for users "nobody" and "anonymous" */
77
}
78
if( g.url.isSsh ){
79
return; /* No login card for SSH: */
80
}
81
blob_zero(&nonce);
82
blob_zero(&pw);
83
sha1sum_blob(pPayload, &nonce);
84
blob_copy(&pw, &nonce);
85
zLogin = g.url.user;
86
if( g.url.passwd ){
87
zPw = g.url.passwd;
88
}else if( g.cgiOutput ){
89
/* Password failure while doing a sync from the web interface */
90
cgi_printf("*** incorrect or missing password for user %h\n", zLogin);
91
zPw = 0;
92
}else{
93
/* Password failure while doing a sync from the command-line interface */
94
url_prompt_for_password();
95
zPw = g.url.passwd;
96
}
97
98
/* The login card wants the SHA1 hash of the password (as computed by
99
** sha1_shared_secret()), not the original password. So convert the
100
** password to its SHA1 encoding if it isn't already a SHA1 hash.
101
**
102
** We assume that a hexadecimal string of exactly 40 characters is a
103
** SHA1 hash, not an original password. If a user has a password which
104
** just happens to be a 40-character hex string, then this routine won't
105
** be able to distinguish it from a hash, the translation will not be
106
** performed, and the sync won't work.
107
*/
108
if( zPw && zPw[0] && (strlen(zPw)!=40 || !validate16(zPw,40)) ){
109
const char *zProjectCode = 0;
110
if( g.url.flags & URL_USE_PARENT ){
111
zProjectCode = db_get("parent-project-code", 0);
112
}else{
113
zProjectCode = db_get("project-code", 0);
114
}
115
zPw = sha1_shared_secret(zPw, zLogin, zProjectCode);
116
if( g.url.pwConfig!=0 && (g.url.flags & URL_REMEMBER_PW)!=0 ){
117
char *x = obscure(zPw);
118
db_set(g.url.pwConfig/*works-like:"x"*/, x, 0);
119
fossil_free(x);
120
}
121
fossil_free(g.url.passwd);
122
g.url.passwd = fossil_strdup(zPw);
123
}
124
125
blob_append(&pw, zPw, -1);
126
sha1sum_blob(&pw, &sig);
127
blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig);
128
blob_reset(&pw);
129
blob_reset(&sig);
130
blob_reset(&nonce);
131
}
132
133
/*
134
** Construct an appropriate HTTP request header. Write the header
135
** into pHdr. This routine initializes the pHdr blob. pPayload is
136
** the complete payload (including the login card if pLogin is NULL or
137
** empty) already compressed.
138
*/
139
static void http_build_header(
140
Blob *pPayload, /* the payload that will be sent */
141
Blob *pHdr, /* construct the header here */
142
Blob *pLogin, /* Login card header value or NULL */
143
const char *zAltMimetype /* Alternative mimetype */
144
){
145
int nPayload = pPayload ? blob_size(pPayload) : 0;
146
const char *zPath;
147
148
blob_zero(pHdr);
149
if( g.url.subpath ){
150
zPath = g.url.subpath;
151
}else if( g.url.path==0 || g.url.path[0]==0 ){
152
zPath = "/";
153
}else{
154
zPath = g.url.path;
155
}
156
blob_appendf(pHdr, "%s %s HTTP/1.0\r\n",
157
nPayload>0 ? "POST" : "GET", zPath);
158
if( g.url.proxyAuth ){
159
blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
160
}
161
if( g.zHttpAuth && g.zHttpAuth[0] ){
162
const char *zCredentials = g.zHttpAuth;
163
char *zEncoded = encode64(zCredentials, -1);
164
blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
165
fossil_free(zEncoded);
166
}
167
blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
168
blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
169
if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
170
if( g.syncInfo.fLoginCardMode>0
171
&& nPayload>0 && pLogin && blob_size(pLogin) ){
172
/* Add sync login card via a transient cookie. We can only do this
173
if we know the remote supports it. */
174
blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(pLogin));
175
}
176
if( nPayload ){
177
if( zAltMimetype ){
178
blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
179
}else if( g.fHttpTrace ){
180
blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
181
}else{
182
blob_appendf(pHdr, "Content-Type: application/x-fossil\r\n");
183
}
184
blob_appendf(pHdr, "Content-Length: %d\r\n", blob_size(pPayload));
185
}
186
blob_append(pHdr, "\r\n", 2);
187
}
188
189
/*
190
** Use Fossil credentials for HTTP Basic Authorization prompt
191
*/
192
static int use_fossil_creds_for_httpauth_prompt(void){
193
Blob x;
194
char c;
195
prompt_user("Use Fossil username and password (y/N)? ", &x);
196
c = blob_str(&x)[0];
197
blob_reset(&x);
198
return ( c=='y' || c=='Y' );
199
}
200
201
/*
202
** Prompt to save HTTP Basic Authorization information
203
*/
204
static int save_httpauth_prompt(void){
205
Blob x;
206
char c;
207
if( (g.url.flags & URL_REMEMBER)==0 ) return 0;
208
prompt_user("Remember Basic Authorization credentials (Y/n)? ", &x);
209
c = blob_str(&x)[0];
210
blob_reset(&x);
211
return ( c!='n' && c!='N' );
212
}
213
214
/*
215
** Get the HTTP Basic Authorization credentials from the user
216
** when 401 is received.
217
*/
218
char *prompt_for_httpauth_creds(void){
219
Blob x;
220
char *zUser;
221
char *zPw;
222
char *zPrompt;
223
char *zHttpAuth = 0;
224
if( !fossil_isatty(fossil_fileno(stdin)) ) return 0;
225
zPrompt = mprintf("\n%s authorization required by\n%s\n",
226
g.url.isHttps==1 ? "Encrypted HTTPS" : "Unencrypted HTTP", g.url.canonical);
227
fossil_print("%s", zPrompt);
228
free(zPrompt);
229
if ( g.url.user && g.url.passwd && use_fossil_creds_for_httpauth_prompt() ){
230
zHttpAuth = mprintf("%s:%s", g.url.user, g.url.passwd);
231
}else{
232
prompt_user("Basic Authorization user: ", &x);
233
zUser = mprintf("%b", &x);
234
zPrompt = mprintf("HTTP password for %b: ", &x);
235
blob_reset(&x);
236
prompt_for_password(zPrompt, &x, 0);
237
zPw = mprintf("%b", &x);
238
zHttpAuth = mprintf("%s:%s", zUser, zPw);
239
free(zUser);
240
free(zPw);
241
free(zPrompt);
242
blob_reset(&x);
243
}
244
if( save_httpauth_prompt() ){
245
set_httpauth(zHttpAuth);
246
}
247
return zHttpAuth;
248
}
249
250
/*
251
** Send content pSend to the server identified by g.url using the
252
** external program given by g.zHttpCmd. Capture the reply from that
253
** program and load it into pReply.
254
**
255
** This routine implements the --transport-command option for "fossil sync".
256
*/
257
static int http_exchange_external(
258
Blob *pSend, /* Message to be sent */
259
Blob *pReply, /* Write the reply here */
260
int mHttpFlags, /* Flags. See above */
261
const char *zAltMimetype /* Alternative mimetype if not NULL */
262
){
263
char *zUplink;
264
char *zDownlink;
265
char *zCmd;
266
char *zFullUrl;
267
int rc;
268
269
zUplink = fossil_temp_filename();
270
zDownlink = fossil_temp_filename();
271
zFullUrl = url_full(&g.url);
272
zCmd = mprintf("%s %$ %$ %$", g.zHttpCmd, zFullUrl,zUplink,zDownlink);
273
fossil_free(zFullUrl);
274
blob_write_to_file(pSend, zUplink);
275
if( g.fHttpTrace ){
276
fossil_print("RUN %s\n", zCmd);
277
}
278
rc = fossil_system(zCmd);
279
if( rc ){
280
fossil_warning("Transport command failed: %s\n", zCmd);
281
}
282
fossil_free(zCmd);
283
file_delete(zUplink);
284
if( file_size(zDownlink, ExtFILE)<0 ){
285
blob_zero(pReply);
286
}else{
287
blob_read_from_file(pReply, zDownlink, ExtFILE);
288
file_delete(zDownlink);
289
}
290
return rc;
291
}
292
293
/* If iTruth<0 then guess as to whether or not a PATH= argument is required
294
** when using ssh to run fossil on a remote machine name zHostname. Return
295
** true if a PATH= should be provided and 0 if not.
296
**
297
** If iTruth is 1 or 0 then that means that the PATH= is or is not required,
298
** respectively. Record this fact for future reference.
299
**
300
** If iTruth is 99 or more, then toggle the value that will be returned
301
** for future iTruth==(-1) queries.
302
*/
303
int ssh_needs_path_argument(const char *zHostname, int iTruth){
304
int ans = 0; /* Default to "no" */
305
char *z = mprintf("use-path-for-ssh:%s", zHostname);
306
if( iTruth<0 ){
307
if( db_get_boolean(z/*works-like:"x"*/, 0) ) ans = 1;
308
}else{
309
if( iTruth>=99 ){
310
iTruth = !db_get_boolean(z/*works-like:"x"*/, 0);
311
}
312
if( iTruth ){
313
ans = 1;
314
db_set(z/*works-like:"x"*/, "1", 1);
315
}else{
316
db_unset(z/*works-like:"x"*/, 1);
317
}
318
}
319
fossil_free(z);
320
return ans;
321
}
322
323
/*
324
** COMMAND: test-ssh-needs-path
325
**
326
** Usage: fossil test-ssh-needs-path ?HOSTNAME? ?BOOLEAN?
327
**
328
** With one argument, show whether or not the PATH= argument is included
329
** by default for HOSTNAME. If the second argument is a boolean, then
330
** change the value.
331
**
332
** With no arguments, show all hosts for which ssh-needs-path is true.
333
*/
334
void test_ssh_needs_path(void){
335
db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
336
db_open_config(0,0);
337
if( g.argc>=3 ){
338
const char *zHost = g.argv[2];
339
int a = -1;
340
int rc;
341
if( g.argc>=4 ) a = is_truth(g.argv[3]);
342
rc = ssh_needs_path_argument(zHost, a);
343
fossil_print("%-20s %s\n", zHost, rc ? "yes" : "no");
344
}else{
345
Stmt s;
346
db_swap_connections();
347
db_prepare(&s, "SELECT substr(name,18) FROM global_config"
348
" WHERE name GLOB 'use-path-for-ssh:*'");
349
while( db_step(&s)==SQLITE_ROW ){
350
const char *zHost = db_column_text(&s,0);
351
fossil_print("%-20s yes\n", zHost);
352
}
353
db_finalize(&s);
354
db_swap_connections();
355
}
356
}
357
358
/* Add an appropriate PATH= argument to the SSH command under construction
359
** in pCmd.
360
**
361
** About This Feature
362
** ==================
363
**
364
** On some ssh servers (Macs in particular are guilty of this) the PATH
365
** variable in the shell that runs the command that is sent to the remote
366
** host contains a limited number of read-only system directories:
367
**
368
** /usr/bin:/bin:/usr/sbin:/sbin
369
**
370
** The fossil executable cannot be installed into any of those directories
371
** because they are locked down, and so the "fossil" command cannot run.
372
**
373
** To work around this, the fossil command is prefixed with the PATH=
374
** argument, inserted by this function, to augment the PATH with additional
375
** directories in which the fossil executable is often found.
376
**
377
** But other ssh servers are confused by this initial PATH= argument.
378
** Some ssh servers have a list of programs that they are allowed to run
379
** and will fail if the first argument is not on that list, and PATH=....
380
** is not on that list.
381
**
382
** So that various commands that use ssh can run seamlessly on a variety
383
** of systems (commands that use ssh include "fossil sync" with an ssh:
384
** URL and the "fossil patch pull" and "fossil patch push" commands where
385
** the destination directory starts with HOSTNAME: or USER@HOSTNAME:.)
386
** the following algorithm is used:
387
**
388
** * First try running the fossil without any PATH= argument. If that
389
** works (and it does on a majority of systems) then we are done.
390
**
391
** * If the first attempt fails, then try again after adding the
392
** PATH= prefix argument. (This function is what adds that
393
** argument.) If the retry works, then remember that fact using
394
** the use-path-for-ssh:HOSTNAME setting so that the first step
395
** is skipped on subsequent uses of the same command.
396
**
397
** See the forum thread at
398
** https://fossil-scm.org/forum/forumpost/4903cb4b691af7ce for more
399
** background.
400
**
401
** See also:
402
**
403
** * The ssh_needs_path_argument() function above.
404
** * The test-ssh-needs-path command that shows the settings
405
** that cache whether or not a PATH= is needed for a particular
406
** HOSTNAME.
407
*/
408
void ssh_add_path_argument(Blob *pCmd){
409
blob_append_escaped_arg(pCmd,
410
"PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
411
}
412
413
/*
414
** Return the complete text of the last HTTP reply as saved in the
415
** http-reply-N.txt file. This only works if run using --httptrace.
416
** Without the --httptrace option, this routine returns a NULL pointer.
417
** It still might return a NULL pointer if for some reason it cannot
418
** find and open the last http-reply-N.txt file.
419
*/
420
char *http_last_trace_reply(void){
421
Blob x;
422
int n;
423
char *zFilename;
424
if( g.fHttpTrace==0 ) return 0;
425
zFilename = mprintf("http-reply-%d.txt", traceCnt);
426
n = blob_read_from_file(&x, zFilename, ExtFILE);
427
fossil_free(zFilename);
428
if( n<=0 ) return 0;
429
return blob_str(&x);
430
}
431
432
/*
433
** Sign the content in pSend, compress it, and send it to the server
434
** via HTTP or HTTPS. Get a reply, uncompress the reply, and store the reply
435
** in pRecv. pRecv is assumed to be uninitialized when
436
** this routine is called - this routine will initialize it.
437
**
438
** The server address is contain in the "g" global structure. The
439
** url_parse() routine should have been called prior to this routine
440
** in order to fill this structure appropriately.
441
*/
442
int http_exchange(
443
Blob *pSend, /* Message to be sent */
444
Blob *pReply, /* Write the reply here */
445
int mHttpFlags, /* Flags. See above */
446
int maxRedirect, /* Max number of redirects */
447
const char *zAltMimetype /* Alternative mimetype if not NULL */
448
){
449
Blob login; /* The login card */
450
Blob payload; /* The complete payload including login card */
451
Blob hdr; /* The HTTP request header */
452
int closeConnection; /* True to close the connection when done */
453
int iLength; /* Expected length of the reply payload */
454
int rc = 0; /* Result code */
455
int iHttpVersion; /* Which version of HTTP protocol server uses */
456
char *zLine; /* A single line of the reply header */
457
int i; /* Loop counter */
458
int isError = 0; /* True if the reply is an error message */
459
int isCompressed = 1; /* True if the reply is compressed */
460
461
if( g.zHttpCmd!=0 ){
462
/* Handle the --transport-command option for "fossil sync" and similar */
463
return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype);
464
}
465
466
/* Activate the PATH= auxiliary argument to the ssh command if that
467
** is called for.
468
*/
469
if( g.url.isSsh
470
&& (g.url.flags & URL_SSH_RETRY)==0
471
&& g.db!=0
472
&& ssh_needs_path_argument(g.url.hostname, -1)
473
){
474
g.url.flags |= URL_SSH_PATH;
475
}
476
477
if( transport_open(&g.url) ){
478
fossil_warning("%s", transport_errmsg(&g.url));
479
return 1;
480
}
481
/* Construct the login card and prepare the complete payload */
482
blob_zero(&login);
483
if( blob_size(pSend)==0 ){
484
blob_zero(&payload);
485
}else{
486
if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
487
if( g.syncInfo.fLoginCardMode ){
488
/* The login card will be sent via an HTTP header and/or URL flag. */
489
if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
490
/* Maintenance note: we cannot blob_swap(pSend,&payload) here
491
** because the HTTP 401 and redirect response handling below
492
** needs pSend unmodified. payload won't be modified after
493
** this point, so we can make it a proxy for pSend for
494
** zero heap memory. */
495
blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
496
}else{
497
blob_compress(pSend, &payload);
498
}
499
}else{
500
/* Prepend the login card (if set) to the payload */
501
if( blob_size(&login) ){
502
blob_append_char(&login, '\n');
503
}
504
if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
505
payload = login;
506
login = empty_blob/*transfer ownership*/;
507
blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
508
}else{
509
blob_compress2(&login, pSend, &payload);
510
blob_reset(&login);
511
}
512
}
513
}
514
515
/* Construct the HTTP request header */
516
http_build_header(&payload, &hdr, &login, zAltMimetype);
517
518
/* When tracing, write the transmitted HTTP message both to standard
519
** output and into a file. The file can then be used to drive the
520
** server-side like this:
521
**
522
** ./fossil test-http <http-request-1.txt
523
*/
524
if( g.fHttpTrace ){
525
char *zOutFile;
526
FILE *out;
527
traceCnt++;
528
zOutFile = mprintf("http-request-%d.txt", traceCnt);
529
out = fopen(zOutFile, "wb");
530
if( out ){
531
fwrite(blob_buffer(&hdr), 1, blob_size(&hdr), out);
532
fwrite(blob_buffer(&payload), 1, blob_size(&payload), out);
533
fclose(out);
534
}
535
free(zOutFile);
536
zOutFile = mprintf("http-reply-%d.txt", traceCnt);
537
out = fopen(zOutFile, "wb");
538
transport_log(out);
539
free(zOutFile);
540
}
541
542
/*
543
** Send the request to the server.
544
*/
545
if( mHttpFlags & HTTP_VERBOSE ){
546
fossil_print("URL: %s\n", g.url.canonical);
547
fossil_print("Sending %d byte header and %d byte payload\n",
548
blob_size(&hdr), blob_size(&payload));
549
}
550
transport_send(&g.url, &hdr);
551
transport_send(&g.url, &payload);
552
blob_reset(&hdr);
553
blob_reset(&payload);
554
transport_flip(&g.url);
555
if( mHttpFlags & HTTP_VERBOSE ){
556
fossil_print("IP-Address: %s\n", g.zIpAddr);
557
}
558
559
/*
560
** Read and interpret the server reply
561
*/
562
closeConnection = 1;
563
iLength = -1;
564
iHttpVersion = -1;
565
while( (zLine = transport_receive_line(&g.url))!=0 && zLine[0]!=0 ){
566
if( mHttpFlags & HTTP_VERBOSE ){
567
fossil_print("Read: [%s]\n", zLine);
568
}
569
if( fossil_strnicmp(zLine, "http/1.", 7)==0 ){
570
if( sscanf(zLine, "HTTP/1.%d %d", &iHttpVersion, &rc)!=2 ) goto write_err;
571
if( rc==401 ){
572
if( fSeenHttpAuth++ < MAX_HTTP_AUTH ){
573
if( g.zHttpAuth ){
574
if( g.zHttpAuth ) free(g.zHttpAuth);
575
}
576
g.zHttpAuth = prompt_for_httpauth_creds();
577
transport_close(&g.url);
578
return http_exchange(pSend, pReply, mHttpFlags,
579
maxRedirect, zAltMimetype);
580
}
581
}
582
if( rc!=200 && rc!=301 && rc!=302 && rc!=307 && rc!=308 ){
583
int ii;
584
for(ii=7; zLine[ii] && zLine[ii]!=' '; ii++){}
585
while( zLine[ii]==' ' ) ii++;
586
if( (mHttpFlags & HTTP_QUIET)==0 ){
587
fossil_warning("server says: %s", &zLine[ii]);
588
}
589
goto write_err;
590
}
591
if( iHttpVersion==0 ){
592
closeConnection = 1;
593
}else{
594
closeConnection = 0;
595
}
596
}else if( g.url.isSsh && fossil_strnicmp(zLine, "status:", 7)==0 ){
597
if( sscanf(zLine, "Status: %d", &rc)!=1 ) goto write_err;
598
if( rc!=200 && rc!=301 && rc!=302 && rc!=307 && rc!=308 ){
599
int ii;
600
for(ii=7; zLine[ii] && zLine[ii]!=' '; ii++){}
601
while( zLine[ii]==' ' ) ii++;
602
fossil_warning("server says: %s", &zLine[ii]);
603
goto write_err;
604
}
605
if( iHttpVersion<0 ) iHttpVersion = 1;
606
closeConnection = 0;
607
}else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
608
for(i=15; fossil_isspace(zLine[i]); i++){}
609
iLength = atoi(&zLine[i]);
610
}else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
611
if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
612
closeConnection = 1;
613
}else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){
614
closeConnection = 0;
615
}
616
}else if( ( rc==301 || rc==302 || rc==307 || rc==308 ) &&
617
fossil_strnicmp(zLine, "location:", 9)==0 ){
618
int i, j;
619
int wasHttps;
620
int priorUrlFlags;
621
622
if ( --maxRedirect == 0){
623
if( (mHttpFlags & HTTP_QUIET)==0 ){
624
fossil_warning("redirect limit exceeded");
625
}
626
goto write_err;
627
}
628
for(i=9; zLine[i] && zLine[i]==' '; i++){}
629
if( zLine[i]==0 ){
630
fossil_warning("malformed redirect: %s", zLine);
631
goto write_err;
632
}
633
j = strlen(zLine) - 1;
634
while( j>4 && fossil_strcmp(&zLine[j-4],"/xfer")==0 ){
635
j -= 4;
636
zLine[j] = 0;
637
}
638
if( (mHttpFlags & HTTP_QUIET)==0 ){
639
fossil_print("redirect with status %d to %s\n", rc, &zLine[i]);
640
}
641
if( g.url.isFile || g.url.isSsh ){
642
if( (mHttpFlags & HTTP_QUIET)==0 ){
643
fossil_warning("cannot redirect from %s to %s", g.url.canonical,
644
&zLine[i]);
645
}
646
goto write_err;
647
}
648
wasHttps = g.url.isHttps;
649
priorUrlFlags = g.url.flags;
650
url_parse(&zLine[i], 0);
651
if( wasHttps && !g.url.isHttps ){
652
if( (mHttpFlags & HTTP_QUIET)==0 ){
653
fossil_warning("cannot redirect from HTTPS to HTTP");
654
}
655
goto write_err;
656
}
657
if( g.url.isSsh || g.url.isFile ){
658
if( (mHttpFlags & HTTP_QUIET)==0 ){
659
fossil_warning("cannot redirect to %s", &zLine[i]);
660
}
661
goto write_err;
662
}
663
transport_close(&g.url);
664
transport_global_shutdown(&g.url);
665
fSeenHttpAuth = 0;
666
if( g.zHttpAuth ) free(g.zHttpAuth);
667
g.zHttpAuth = get_httpauth();
668
if( (rc==301 || rc==308) && (priorUrlFlags & URL_REMEMBER)!=0 ){
669
g.url.flags |= URL_REMEMBER;
670
url_remember();
671
}
672
return http_exchange(pSend, pReply, mHttpFlags,
673
maxRedirect, zAltMimetype);
674
}else if( fossil_strnicmp(zLine, "content-type: ", 14)==0 ){
675
if( fossil_strnicmp(&zLine[14], "application/x-fossil-debug", -1)==0 ){
676
isCompressed = 0;
677
}else if( fossil_strnicmp(&zLine[14],
678
"application/x-fossil-uncompressed", -1)==0 ){
679
isCompressed = 0;
680
}else{
681
if( mHttpFlags & HTTP_GENERIC ){
682
if( mHttpFlags & HTTP_NOCOMPRESS ) isCompressed = 0;
683
}else if( fossil_strnicmp(&zLine[14], "application/x-fossil", -1)!=0 ){
684
isError = 1;
685
}
686
}
687
}
688
}
689
if( iHttpVersion<0 ){
690
/* We got nothing back from the server. If using the ssh: protocol,
691
** this might mean we need to add or remove the PATH=... argument
692
** to the SSH command being sent. If that is the case, retry the
693
** request after adding or removing the PATH= argument.
694
*/
695
if( g.url.isSsh /* This is an SSH: sync */
696
&& (g.url.flags & URL_SSH_EXE)==0 /* Does not have ?fossil=.... */
697
&& (g.url.flags & URL_SSH_RETRY)==0 /* Not retried already */
698
){
699
/* Retry after flipping the SSH_PATH setting */
700
transport_close(&g.url);
701
if( (mHttpFlags & HTTP_QUIET)==0 ){
702
fossil_print(
703
"First attempt to run fossil on %s using SSH failed.\n"
704
"Retrying %s the PATH= argument.\n",
705
g.url.hostname,
706
(g.url.flags & URL_SSH_PATH)!=0 ? "without" : "with"
707
);
708
}
709
g.url.flags ^= URL_SSH_PATH|URL_SSH_RETRY;
710
rc = http_exchange(pSend,pReply,mHttpFlags,0,zAltMimetype);
711
if( rc==0 && g.db!=0 ){
712
(void)ssh_needs_path_argument(g.url.hostname,
713
(g.url.flags & URL_SSH_PATH)!=0);
714
}
715
return rc;
716
}else{
717
/* The problem could not be corrected by retrying. Report the
718
** the error. */
719
if( mHttpFlags & HTTP_QUIET ){
720
/* no-op */
721
}else if( g.url.isSsh && !g.fSshTrace ){
722
fossil_warning("server did not reply: "
723
" rerun with --sshtrace for diagnostics");
724
}else{
725
fossil_warning("server did not reply");
726
}
727
goto write_err;
728
}
729
}
730
if( rc!=200 ){
731
if( mHttpFlags & HTTP_QUIET ) goto write_err;
732
fossil_warning("\"location:\" missing from %d redirect reply", rc);
733
goto write_err;
734
}
735
736
/*
737
** Extract the reply payload that follows the header
738
*/
739
blob_zero(pReply);
740
if( iLength==0 ){
741
/* No content to read */
742
}else if( iLength>0 ){
743
/* Read content of a known length */
744
int iRecvLen; /* Received length of the reply payload */
745
blob_resize(pReply, iLength);
746
iRecvLen = transport_receive(&g.url, blob_buffer(pReply), iLength);
747
if( mHttpFlags & HTTP_VERBOSE ){
748
fossil_print("Reply received: %d of %d bytes\n", iRecvLen, iLength);
749
}
750
if( iRecvLen != iLength ){
751
if( mHttpFlags & HTTP_QUIET ) goto write_err;
752
fossil_warning("response truncated: got %d bytes of %d",
753
iRecvLen, iLength);
754
goto write_err;
755
}
756
}else{
757
/* Read content until end-of-file */
758
int iRecvLen; /* Received length of the reply payload */
759
unsigned int nReq = 1000;
760
unsigned int nPrior = 0;
761
closeConnection = 1;
762
do{
763
nReq *= 2;
764
blob_resize(pReply, nPrior+nReq);
765
iRecvLen = transport_receive(&g.url, &pReply->aData[nPrior], (int)nReq);
766
nPrior += iRecvLen;
767
pReply->nUsed = nPrior;
768
}while( iRecvLen==nReq && nReq<0x20000000 );
769
if( mHttpFlags & HTTP_VERBOSE ){
770
fossil_print("Reply received: %u bytes (w/o content-length)\n", nPrior);
771
}
772
}
773
if( isError ){
774
char *z;
775
int i, j;
776
z = blob_str(pReply);
777
for(i=j=0; z[i]; i++, j++){
778
if( z[i]=='<' ){
779
while( z[i] && z[i]!='>' ) i++;
780
if( z[i]==0 ) break;
781
}
782
z[j] = z[i];
783
}
784
z[j] = 0;
785
if( mHttpFlags & HTTP_QUIET ){
786
/* no-op */
787
}else if( mHttpFlags & HTTP_VERBOSE ){
788
fossil_warning("server sends error: %s", z);
789
}else{
790
fossil_warning("server sends error");
791
}
792
goto write_err;
793
}
794
if( isCompressed ) blob_uncompress(pReply, pReply);
795
796
/*
797
** Close the connection to the server if appropriate.
798
**
799
** FIXME: There is some bug in the lower layers that prevents the
800
** connection from remaining open. The easiest fix for now is to
801
** simply close and restart the connection for each round-trip.
802
**
803
** For SSH we will leave the connection open.
804
*/
805
if( ! g.url.isSsh ) closeConnection = 1; /* FIX ME */
806
if( closeConnection ){
807
transport_close(&g.url);
808
}else{
809
transport_rewind(&g.url);
810
}
811
return 0;
812
813
/*
814
** Jump to here if an error is seen.
815
*/
816
write_err:
817
g.iResultCode = 1;
818
transport_close(&g.url);
819
return 1;
820
}
821
822
/*
823
** COMMAND: test-httpmsg
824
**
825
** Usage: %fossil test-httpmsg ?OPTIONS? URL ?PAYLOAD? ?OUTPUT?
826
**
827
** Send an HTTP message to URL and get the reply. PAYLOAD is a file containing
828
** the payload, or "-" to read payload from standard input. A POST message
829
** is sent if PAYLOAD is specified and is non-empty. If PAYLOAD is omitted
830
** or is an empty file, then a GET message is sent.
831
**
832
** If a second filename (OUTPUT) is given after PAYLOAD, then the reply
833
** is written into that second file instead of being written on standard
834
** output. Use the "--out OUTPUT" option to specify an output file for
835
** a GET request where there is no PAYLOAD.
836
**
837
** Options:
838
** --compress Use ZLIB compression on the payload
839
** --mimetype TYPE Mimetype of the payload
840
** --no-cert-verify Disable TLS cert verification
841
** --out FILE Store the reply in FILE
842
** --subpath PATH HTTP request path for ssh: and file: URLs
843
** -v Verbose output
844
** --xfer PAYLOAD in a Fossil xfer protocol message
845
*/
846
void test_httpmsg_command(void){
847
const char *zMimetype;
848
const char *zInFile;
849
const char *zOutFile;
850
const char *zSubpath;
851
Blob in, out;
852
unsigned int mHttpFlags = HTTP_GENERIC|HTTP_NOCOMPRESS;
853
854
zMimetype = find_option("mimetype",0,1);
855
zOutFile = find_option("out","o",1);
856
if( find_option("verbose","v",0)!=0 ) mHttpFlags |= HTTP_VERBOSE;
857
if( find_option("compress",0,0)!=0 ) mHttpFlags &= ~HTTP_NOCOMPRESS;
858
if( find_option("no-cert-verify",0,0)!=0 ){
859
#ifdef FOSSIL_ENABLE_SSL
860
ssl_disable_cert_verification();
861
#endif
862
}
863
if( find_option("xfer",0,0)!=0 ){
864
mHttpFlags |= HTTP_USE_LOGIN;
865
mHttpFlags &= ~HTTP_GENERIC;
866
}
867
if( find_option("ipv4",0,0) ) g.eIPvers = 1;
868
if( find_option("ipv6",0,0) ) g.eIPvers = 2;
869
zSubpath = find_option("subpath",0,1);
870
verify_all_options();
871
if( g.argc<3 || g.argc>5 ){
872
usage("URL ?PAYLOAD? ?OUTPUT?");
873
}
874
zInFile = g.argc>=4 ? g.argv[3] : 0;
875
if( g.argc==5 ){
876
if( zOutFile ){
877
fossil_fatal("output file specified twice: \"--out %s\" and \"%s\"",
878
zOutFile, g.argv[4]);
879
}
880
zOutFile = g.argv[4];
881
}
882
url_parse(g.argv[2], 0);
883
if( g.url.protocol[0]!='h' ){
884
if( zSubpath==0 ){
885
fossil_fatal("the --subpath option is required for %s://",g.url.protocol);
886
}else{
887
g.url.subpath = fossil_strdup(zSubpath);
888
}
889
}
890
if( zInFile ){
891
blob_read_from_file(&in, zInFile, ExtFILE);
892
if( zMimetype==0 && (mHttpFlags & HTTP_GENERIC)!=0 ){
893
if( fossil_strcmp(zInFile,"-")==0 ){
894
zMimetype = "application/x-unknown";
895
}else{
896
zMimetype = mimetype_from_name(zInFile);
897
}
898
}
899
}else{
900
blob_init(&in, 0, 0);
901
}
902
blob_init(&out, 0, 0);
903
if( (mHttpFlags & HTTP_VERBOSE)==0 && zOutFile==0 ){
904
zOutFile = "-";
905
mHttpFlags |= HTTP_QUIET;
906
}
907
http_exchange(&in, &out, mHttpFlags, 4, zMimetype);
908
if( zOutFile ) blob_write_to_file(&out, zOutFile);
909
blob_zero(&in);
910
blob_zero(&out);
911
}
912

Keyboard Shortcuts

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