Fossil SCM

fossil-scm / src / http.c
Blame History Raw 1100 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
** Append zPath to pBlob as the target of an HTTP request line (RFC 9112
135
** section 3.2). zPath is the decoded path from the URL, so it is
136
** percent-encoded again here: each byte outside ASCII and each
137
** character that RFC 3986 section 3.3 does not allow in a path is
138
** written as %XX. '+' is encoded too, because the server decodes it
139
** as a space.
140
*/
141
static void http_append_request_target(Blob *pBlob, const char *zPath){
142
for(; *zPath; zPath++){
143
unsigned char c = (unsigned char)*zPath;
144
if( c<=0x20 || c>=0x7f || strchr("\"#%+<>?[\\]^`{|}", c)!=0 ){
145
blob_appendf(pBlob, "%%%02X", c);
146
}else{
147
blob_append_char(pBlob, (char)c);
148
}
149
}
150
}
151
152
/*
153
** Construct an appropriate HTTP request header. Write the header
154
** into pHdr. This routine initializes the pHdr blob. pPayload is
155
** the complete payload (including the login card if pLogin is NULL or
156
** empty) already compressed.
157
*/
158
static void http_build_header(
159
Blob *pPayload, /* the payload that will be sent */
160
Blob *pHdr, /* construct the header here */
161
Blob *pLogin, /* Login card header value or NULL */
162
const char *zAltMimetype /* Alternative mimetype */
163
){
164
int nPayload = pPayload ? blob_size(pPayload) : 0;
165
166
blob_zero(pHdr);
167
blob_appendf(pHdr, "%s ", nPayload>0 ? "POST" : "GET");
168
if( g.url.subpath ){
169
/* The subpath is already percent-encoded. */
170
blob_append(pHdr, g.url.subpath, -1);
171
}else if( g.url.path==0 || g.url.path[0]==0 ){
172
blob_append_char(pHdr, '/');
173
}else{
174
http_append_request_target(pHdr, g.url.path);
175
}
176
blob_appendf(pHdr, " HTTP/1.1\r\n");
177
if( g.url.proxyAuth ){
178
blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth);
179
}
180
if( g.zHttpAuth && g.zHttpAuth[0] ){
181
const char *zCredentials = g.zHttpAuth;
182
char *zEncoded = encode64(zCredentials, -1);
183
blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded);
184
fossil_free(zEncoded);
185
}
186
blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname);
187
blob_appendf(pHdr, "Connection: close\r\n");
188
blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent());
189
if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n");
190
if( g.syncInfo.fLoginCardMode>0
191
&& nPayload>0 && pLogin && blob_size(pLogin) ){
192
/* Add sync login card via a transient cookie. We can only do this
193
if we know the remote supports it. */
194
blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(pLogin));
195
}
196
if( nPayload ){
197
if( zAltMimetype ){
198
blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype);
199
}else if( g.fHttpTrace ){
200
blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n");
201
}else{
202
blob_appendf(pHdr, "Content-Type: application/x-fossil\r\n");
203
}
204
blob_appendf(pHdr, "Content-Length: %d\r\n", blob_size(pPayload));
205
}
206
blob_append(pHdr, "\r\n", 2);
207
}
208
209
/*
210
** Use Fossil credentials for HTTP Basic Authorization prompt
211
*/
212
static int use_fossil_creds_for_httpauth_prompt(void){
213
Blob x;
214
char c;
215
prompt_user("Use Fossil username and password (y/N)? ", &x);
216
c = blob_str(&x)[0];
217
blob_reset(&x);
218
return ( c=='y' || c=='Y' );
219
}
220
221
/*
222
** Prompt to save HTTP Basic Authorization information
223
*/
224
static int save_httpauth_prompt(void){
225
Blob x;
226
char c;
227
if( (g.url.flags & URL_REMEMBER)==0 ) return 0;
228
prompt_user("Remember Basic Authorization credentials (Y/n)? ", &x);
229
c = blob_str(&x)[0];
230
blob_reset(&x);
231
return ( c!='n' && c!='N' );
232
}
233
234
/*
235
** Get the HTTP Basic Authorization credentials from the user
236
** when 401 is received.
237
*/
238
char *prompt_for_httpauth_creds(void){
239
Blob x;
240
char *zUser;
241
char *zPw;
242
char *zPrompt;
243
char *zHttpAuth = 0;
244
if( !fossil_isatty(fossil_fileno(stdin)) ) return 0;
245
zPrompt = mprintf("\n%s authorization required by\n%s\n",
246
g.url.isHttps==1 ? "Encrypted HTTPS" : "Unencrypted HTTP", g.url.canonical);
247
fossil_print("%s", zPrompt);
248
free(zPrompt);
249
if ( g.url.user && g.url.passwd && use_fossil_creds_for_httpauth_prompt() ){
250
zHttpAuth = mprintf("%s:%s", g.url.user, g.url.passwd);
251
}else{
252
prompt_user("Basic Authorization user: ", &x);
253
zUser = mprintf("%b", &x);
254
zPrompt = mprintf("HTTP password for %b: ", &x);
255
blob_reset(&x);
256
prompt_for_password(zPrompt, &x, 0);
257
zPw = mprintf("%b", &x);
258
zHttpAuth = mprintf("%s:%s", zUser, zPw);
259
free(zUser);
260
free(zPw);
261
free(zPrompt);
262
blob_reset(&x);
263
}
264
if( save_httpauth_prompt() ){
265
set_httpauth(zHttpAuth);
266
}
267
return zHttpAuth;
268
}
269
270
/*
271
** Send content pSend to the server identified by g.url using the
272
** external program given by g.zHttpCmd. Capture the reply from that
273
** program and load it into pReply.
274
**
275
** This routine implements the --transport-command option for "fossil sync".
276
*/
277
static int http_exchange_external(
278
Blob *pSend, /* Message to be sent */
279
Blob *pReply, /* Write the reply here */
280
int mHttpFlags, /* Flags. See above */
281
const char *zAltMimetype /* Alternative mimetype if not NULL */
282
){
283
char *zUplink;
284
char *zDownlink;
285
char *zCmd;
286
char *zFullUrl;
287
int rc;
288
289
zUplink = fossil_temp_filename();
290
zDownlink = fossil_temp_filename();
291
zFullUrl = url_full(&g.url);
292
zCmd = mprintf("%s %$ %$ %$", g.zHttpCmd, zFullUrl,zUplink,zDownlink);
293
fossil_free(zFullUrl);
294
blob_write_to_file(pSend, zUplink);
295
if( g.fHttpTrace ){
296
fossil_print("RUN %s\n", zCmd);
297
}
298
rc = fossil_system(zCmd);
299
if( rc ){
300
fossil_warning("Transport command failed: %s\n", zCmd);
301
}
302
fossil_free(zCmd);
303
file_delete(zUplink);
304
if( file_size(zDownlink, ExtFILE)<0 ){
305
blob_zero(pReply);
306
}else{
307
blob_read_from_file(pReply, zDownlink, ExtFILE);
308
file_delete(zDownlink);
309
}
310
return rc;
311
}
312
313
/* If iTruth<0 then guess as to whether or not a PATH= argument is required
314
** when using ssh to run fossil on a remote machine name zHostname. Return
315
** true if a PATH= should be provided and 0 if not.
316
**
317
** If iTruth is 1 or 0 then that means that the PATH= is or is not required,
318
** respectively. Record this fact for future reference.
319
**
320
** If iTruth is 99 or more, then toggle the value that will be returned
321
** for future iTruth==(-1) queries.
322
*/
323
int ssh_needs_path_argument(const char *zHostname, int iTruth){
324
int ans = 0; /* Default to "no" */
325
char *z = mprintf("use-path-for-ssh:%s", zHostname);
326
if( iTruth<0 ){
327
if( db_get_boolean(z/*works-like:"x"*/, 0) ) ans = 1;
328
}else{
329
if( iTruth>=99 ){
330
iTruth = !db_get_boolean(z/*works-like:"x"*/, 0);
331
}
332
if( iTruth ){
333
ans = 1;
334
db_set(z/*works-like:"x"*/, "1", 1);
335
}else{
336
db_unset(z/*works-like:"x"*/, 1);
337
}
338
}
339
fossil_free(z);
340
return ans;
341
}
342
343
/*
344
** COMMAND: test-ssh-needs-path
345
**
346
** Usage: fossil test-ssh-needs-path ?HOSTNAME? ?BOOLEAN?
347
**
348
** With one argument, show whether or not the PATH= argument is included
349
** by default for HOSTNAME. If the second argument is a boolean, then
350
** change the value.
351
**
352
** With no arguments, show all hosts for which ssh-needs-path is true.
353
*/
354
void test_ssh_needs_path(void){
355
db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
356
db_open_config(0,0);
357
if( g.argc>=3 ){
358
const char *zHost = g.argv[2];
359
int a = -1;
360
int rc;
361
if( g.argc>=4 ) a = is_truth(g.argv[3]);
362
rc = ssh_needs_path_argument(zHost, a);
363
fossil_print("%-20s %s\n", zHost, rc ? "yes" : "no");
364
}else{
365
Stmt s;
366
db_swap_connections();
367
db_prepare(&s, "SELECT substr(name,18) FROM global_config"
368
" WHERE name GLOB 'use-path-for-ssh:*'");
369
while( db_step(&s)==SQLITE_ROW ){
370
const char *zHost = db_column_text(&s,0);
371
fossil_print("%-20s yes\n", zHost);
372
}
373
db_finalize(&s);
374
db_swap_connections();
375
}
376
}
377
378
/* Add an appropriate PATH= argument to the SSH command under construction
379
** in pCmd.
380
**
381
** About This Feature
382
** ==================
383
**
384
** On some ssh servers (Macs in particular are guilty of this) the PATH
385
** variable in the shell that runs the command that is sent to the remote
386
** host contains a limited number of read-only system directories:
387
**
388
** /usr/bin:/bin:/usr/sbin:/sbin
389
**
390
** The fossil executable cannot be installed into any of those directories
391
** because they are locked down, and so the "fossil" command cannot run.
392
**
393
** To work around this, the fossil command is prefixed with the PATH=
394
** argument, inserted by this function, to augment the PATH with additional
395
** directories in which the fossil executable is often found.
396
**
397
** But other ssh servers are confused by this initial PATH= argument.
398
** Some ssh servers have a list of programs that they are allowed to run
399
** and will fail if the first argument is not on that list, and PATH=....
400
** is not on that list.
401
**
402
** So that various commands that use ssh can run seamlessly on a variety
403
** of systems (commands that use ssh include "fossil sync" with an ssh:
404
** URL and the "fossil patch pull" and "fossil patch push" commands where
405
** the destination directory starts with HOSTNAME: or USER@HOSTNAME:.)
406
** the following algorithm is used:
407
**
408
** * First try running the fossil without any PATH= argument. If that
409
** works (and it does on a majority of systems) then we are done.
410
**
411
** * If the first attempt fails, then try again after adding the
412
** PATH= prefix argument. (This function is what adds that
413
** argument.) If the retry works, then remember that fact using
414
** the use-path-for-ssh:HOSTNAME setting so that the first step
415
** is skipped on subsequent uses of the same command.
416
**
417
** See the forum thread at
418
** https://fossil-scm.org/forum/forumpost/4903cb4b691af7ce for more
419
** background.
420
**
421
** See also:
422
**
423
** * The ssh_needs_path_argument() function above.
424
** * The test-ssh-needs-path command that shows the settings
425
** that cache whether or not a PATH= is needed for a particular
426
** HOSTNAME.
427
*/
428
void ssh_add_path_argument(Blob *pCmd){
429
blob_append_escaped_arg(pCmd,
430
"PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1);
431
}
432
433
/*
434
** Return the complete text of the last HTTP reply as saved in the
435
** http-reply-N.txt file. This only works if run using --httptrace.
436
** Without the --httptrace option, this routine returns a NULL pointer.
437
** It still might return a NULL pointer if for some reason it cannot
438
** find and open the last http-reply-N.txt file.
439
*/
440
char *http_last_trace_reply(void){
441
Blob x;
442
int n;
443
char *zFilename;
444
if( g.fHttpTrace==0 ) return 0;
445
zFilename = mprintf("http-reply-%d.txt", traceCnt);
446
n = blob_read_from_file(&x, zFilename, ExtFILE);
447
fossil_free(zFilename);
448
if( n<=0 ) return 0;
449
return blob_str(&x);
450
}
451
452
/*
453
** Return true if z is a valud extension according to RFC 9112 s 7.1.1.
454
** In addition to the ordinary checks, bare CR anywhere is invalid
455
** (section 2.2).
456
*/
457
static int http_chunk_ext_ok(const char *z){
458
if( strchr(z, '\r')!=0 ) return 0;
459
while( *z==' ' || *z=='\t' ) z++;
460
if( *z==0 ) return 1;
461
if( *z!=';' ) return 0;
462
z++;
463
while( *z==' ' || *z=='\t' ) z++;
464
return fossil_isalnum(*z)
465
|| (*z!=0 && strchr("!#$%&'*+-.^_`|~", *z)!=0);
466
}
467
468
/*
469
** Sign the content in pSend, compress it, and send it to the server
470
** via HTTP or HTTPS. Get a reply, uncompress the reply, and store the reply
471
** in pRecv. pRecv is assumed to be uninitialized when
472
** this routine is called - this routine will initialize it.
473
**
474
** The server address is contain in the "g" global structure. The
475
** url_parse() routine should have been called prior to this routine
476
** in order to fill this structure appropriately.
477
*/
478
int http_exchange(
479
Blob *pSend, /* Message to be sent */
480
Blob *pReply, /* Write the reply here */
481
int mHttpFlags, /* Flags. See above */
482
int maxRedirect, /* Max number of redirects */
483
const char *zAltMimetype /* Alternative mimetype if not NULL */
484
){
485
Blob login; /* The login card */
486
Blob payload; /* The complete payload including login card */
487
Blob hdr; /* The HTTP request header */
488
int closeConnection; /* True to close the connection when done */
489
int iLength; /* Expected length of the reply payload */
490
int rc = 0; /* Result code */
491
int iHttpVersion; /* Which version of HTTP protocol server uses */
492
int nBlank = 0; /* Empty lines to skip after an interim reply */
493
char *zLine; /* A single line of the reply header */
494
int i; /* Loop counter */
495
int isError = 0; /* True if the reply is an error message */
496
int isCompressed = 1; /* True if the reply is compressed */
497
int isChunked = 0; /* True if Transfer-Encoding: chunked */
498
499
if( g.zHttpCmd!=0 ){
500
/* Handle the --transport-command option for "fossil sync" and similar */
501
return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype);
502
}
503
504
/* Activate the PATH= auxiliary argument to the ssh command if that
505
** is called for.
506
*/
507
if( g.url.isSsh
508
&& (g.url.flags & URL_SSH_RETRY)==0
509
&& g.db!=0
510
&& ssh_needs_path_argument(g.url.hostname, -1)
511
){
512
g.url.flags |= URL_SSH_PATH;
513
}
514
515
if( transport_open(&g.url) ){
516
fossil_warning("%s", transport_errmsg(&g.url));
517
return 1;
518
}
519
/* Construct the login card and prepare the complete payload */
520
blob_zero(&login);
521
if( blob_size(pSend)==0 ){
522
blob_zero(&payload);
523
}else{
524
if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login);
525
if( g.syncInfo.fLoginCardMode ){
526
/* The login card will be sent via an HTTP header and/or URL flag. */
527
if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
528
/* Maintenance note: we cannot blob_swap(pSend,&payload) here
529
** because the HTTP 401 and redirect response handling below
530
** needs pSend unmodified. payload won't be modified after
531
** this point, so we can make it a proxy for pSend for
532
** zero heap memory. */
533
blob_init(&payload, blob_buffer(pSend), blob_size(pSend));
534
}else{
535
blob_compress(pSend, &payload);
536
}
537
}else{
538
/* Prepend the login card (if set) to the payload */
539
if( blob_size(&login) ){
540
blob_append_char(&login, '\n');
541
}
542
if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){
543
payload = login;
544
login = empty_blob/*transfer ownership*/;
545
blob_append(&payload, blob_buffer(pSend), blob_size(pSend));
546
}else{
547
blob_compress2(&login, pSend, &payload);
548
blob_reset(&login);
549
}
550
}
551
}
552
553
/* Construct the HTTP request header */
554
http_build_header(&payload, &hdr, &login, zAltMimetype);
555
556
/* When tracing, write the transmitted HTTP message both to standard
557
** output and into a file. The file can then be used to drive the
558
** server-side like this:
559
**
560
** ./fossil test-http <http-request-1.txt
561
*/
562
if( g.fHttpTrace ){
563
char *zOutFile;
564
FILE *out;
565
traceCnt++;
566
zOutFile = mprintf("http-request-%d.txt", traceCnt);
567
out = fopen(zOutFile, "wb");
568
if( out ){
569
fwrite(blob_buffer(&hdr), 1, blob_size(&hdr), out);
570
fwrite(blob_buffer(&payload), 1, blob_size(&payload), out);
571
fclose(out);
572
}
573
free(zOutFile);
574
zOutFile = mprintf("http-reply-%d.txt", traceCnt);
575
out = fopen(zOutFile, "wb");
576
transport_log(out);
577
free(zOutFile);
578
}
579
580
/*
581
** Send the request to the server.
582
*/
583
if( mHttpFlags & HTTP_VERBOSE ){
584
fossil_print("URL: %s\n", g.url.canonical);
585
fossil_print("Sending %d byte header and %d byte payload\n",
586
blob_size(&hdr), blob_size(&payload));
587
}
588
transport_send(&g.url, &hdr);
589
transport_send(&g.url, &payload);
590
blob_reset(&hdr);
591
blob_reset(&payload);
592
transport_flip(&g.url);
593
if( mHttpFlags & HTTP_VERBOSE ){
594
fossil_print("IP-Address: %s\n", g.zIpAddr);
595
}
596
597
/*
598
** Read and interpret the server reply
599
*/
600
closeConnection = 1;
601
iLength = -1;
602
iHttpVersion = -1;
603
while( (zLine = transport_receive_line(&g.url))!=0 ){
604
if( zLine[0]==0 ){
605
if( nBlank>0 ){
606
/* A server may send an empty line after an interim reply,
607
** before the status line of the final reply. */
608
nBlank--;
609
continue;
610
}
611
break;
612
}
613
nBlank = 0;
614
if( mHttpFlags & HTTP_VERBOSE ){
615
fossil_print("Read: [%s]\n", zLine);
616
}
617
if( zLine[0]==' ' || zLine[0]=='\t' ){
618
/* A line starting with a space or tab continues the previous
619
** header (obsolete line folding, RFC 9112 section 5.2) and in this
620
** case we disregard the RFC because it gets messy. This is what
621
** the llhttp project and others do. */
622
fossil_warning("server sent a folded header line");
623
goto write_err;
624
}
625
if( fossil_strnicmp(zLine, "http/1.", 7)==0 ){
626
if( sscanf(zLine, "HTTP/1.%d %d", &iHttpVersion, &rc)!=2 ) goto write_err;
627
if( rc/100==1 ){
628
/* Parse and discard an unexpected 1xx interim reply, most commonly
629
** "100 Continue". Skip the remaining header lines of the reply.
630
** A 1xx reply has no body.) */
631
while( (zLine = transport_receive_line(&g.url))!=0 && zLine[0]!=0 ){}
632
iHttpVersion = -1; /* Ensures correct error msg if connection drops */
633
if( zLine==0 ) goto write_err;
634
nBlank = 4;
635
continue;
636
}
637
if( rc==401 ){
638
if( fSeenHttpAuth++ < MAX_HTTP_AUTH ){
639
if( g.zHttpAuth ){
640
if( g.zHttpAuth ) free(g.zHttpAuth);
641
}
642
g.zHttpAuth = prompt_for_httpauth_creds();
643
transport_close(&g.url);
644
return http_exchange(pSend, pReply, mHttpFlags,
645
maxRedirect, zAltMimetype);
646
}
647
}
648
if( rc!=200 && rc!=301 && rc!=302 && rc!=307 && rc!=308 ){
649
int ii;
650
for(ii=7; zLine[ii] && zLine[ii]!=' '; ii++){}
651
while( zLine[ii]==' ' ) ii++;
652
if( (mHttpFlags & HTTP_QUIET)==0 ){
653
fossil_warning("server says: %s", &zLine[ii]);
654
}
655
goto write_err;
656
}
657
if( iHttpVersion==0 ){
658
closeConnection = 1;
659
}else{
660
closeConnection = 0;
661
}
662
}else if( g.url.isSsh && fossil_strnicmp(zLine, "status:", 7)==0 ){
663
if( sscanf(zLine, "Status: %d", &rc)!=1 ) goto write_err;
664
if( rc!=200 && rc!=301 && rc!=302 && rc!=307 && rc!=308 ){
665
int ii;
666
for(ii=7; zLine[ii] && zLine[ii]!=' '; ii++){}
667
while( zLine[ii]==' ' ) ii++;
668
fossil_warning("server says: %s", &zLine[ii]);
669
goto write_err;
670
}
671
if( iHttpVersion<0 ) iHttpVersion = 1;
672
closeConnection = 0;
673
}else if( fossil_strnicmp(zLine, "content-length:", 15)==0 ){
674
/* The value is one or more digits, optionally followed by
675
** spaces, and fits in an int. Any other value is an error, and
676
** so is a second Content-Length with a different value. */
677
sqlite3_int64 n = 0;
678
int iStart;
679
for(i=15; fossil_isspace(zLine[i]); i++){}
680
for(iStart=i; fossil_isdigit(zLine[i]) && n<=0x7fffffff; i++){
681
n = n*10 + zLine[i] - '0';
682
}
683
if( i==iStart || n>0x7fffffff ){
684
fossil_warning("server sent a bad content-length: %s", zLine);
685
goto write_err;
686
}
687
for(; fossil_isspace(zLine[i]); i++){}
688
if( zLine[i]!=0 ){
689
fossil_warning("server sent a bad content-length: %s", zLine);
690
goto write_err;
691
}
692
if( iLength>=0 && iLength!=(int)n ){
693
fossil_warning("server sent two different content-length values");
694
goto write_err;
695
}
696
iLength = (int)n;
697
}else if( fossil_strnicmp(zLine, "transfer-encoding:", 18)==0 ){
698
/* Fossil never sends a "transfer-encoding:" request header, so we may
699
** reject any encoding other than "chunked", including "gzip, chunked",
700
** "superchunked", "deflate" etc. */
701
int bOk = 0;
702
for(i=18; fossil_isspace(zLine[i]); i++){}
703
if( fossil_strnicmp(&zLine[i], "chunked", 7)==0 ){
704
bOk = zLine[i+7]==0;
705
}
706
if( !bOk ){
707
fossil_warning("unsupported transfer-encoding: %s", &zLine[18]);
708
goto write_err;
709
}
710
if( isChunked ){
711
/* (RFC 9110 section 5.3), chunked may be applied only once */
712
fossil_warning("server sent two transfer-encoding headers");
713
goto write_err;
714
}
715
isChunked = 1;
716
}else if( fossil_strnicmp(zLine, "connection:", 11)==0 ){
717
if( sqlite3_strlike("%close%", &zLine[11], 0)==0 ){
718
closeConnection = 1;
719
}else if( sqlite3_strlike("%keep-alive%", &zLine[11], 0)==0 ){
720
closeConnection = 0;
721
}
722
}else if( ( rc==301 || rc==302 || rc==307 || rc==308 ) &&
723
fossil_strnicmp(zLine, "location:", 9)==0 ){
724
int i, j;
725
int wasHttps;
726
int priorUrlFlags;
727
728
if ( --maxRedirect == 0){
729
if( (mHttpFlags & HTTP_QUIET)==0 ){
730
fossil_warning("redirect limit exceeded");
731
}
732
goto write_err;
733
}
734
for(i=9; zLine[i] && zLine[i]==' '; i++){}
735
if( zLine[i]==0 ){
736
fossil_warning("malformed redirect: %s", zLine);
737
goto write_err;
738
}
739
j = strlen(zLine) - 1;
740
while( j>4 && fossil_strcmp(&zLine[j-4],"/xfer")==0 ){
741
j -= 4;
742
zLine[j] = 0;
743
}
744
if( (mHttpFlags & HTTP_QUIET)==0 ){
745
fossil_print("redirect with status %d to %s\n", rc, &zLine[i]);
746
}
747
if( g.url.isFile || g.url.isSsh ){
748
if( (mHttpFlags & HTTP_QUIET)==0 ){
749
fossil_warning("cannot redirect from %s to %s", g.url.canonical,
750
&zLine[i]);
751
}
752
goto write_err;
753
}
754
wasHttps = g.url.isHttps;
755
priorUrlFlags = g.url.flags;
756
url_parse(&zLine[i], 0);
757
if( wasHttps && !g.url.isHttps ){
758
if( (mHttpFlags & HTTP_QUIET)==0 ){
759
fossil_warning("cannot redirect from HTTPS to HTTP");
760
}
761
goto write_err;
762
}
763
if( g.url.isSsh || g.url.isFile ){
764
if( (mHttpFlags & HTTP_QUIET)==0 ){
765
fossil_warning("cannot redirect to %s", &zLine[i]);
766
}
767
goto write_err;
768
}
769
transport_close(&g.url);
770
transport_global_shutdown(&g.url);
771
fSeenHttpAuth = 0;
772
if( g.zHttpAuth ) free(g.zHttpAuth);
773
g.zHttpAuth = get_httpauth();
774
if( (rc==301 || rc==308) && (priorUrlFlags & URL_REMEMBER)!=0 ){
775
g.url.flags |= URL_REMEMBER;
776
url_remember();
777
}
778
return http_exchange(pSend, pReply, mHttpFlags,
779
maxRedirect, zAltMimetype);
780
}else if( fossil_strnicmp(zLine, "content-type: ", 14)==0 ){
781
if( fossil_strnicmp(&zLine[14], "application/x-fossil-debug", -1)==0 ){
782
isCompressed = 0;
783
}else if( fossil_strnicmp(&zLine[14],
784
"application/x-fossil-uncompressed", -1)==0 ){
785
isCompressed = 0;
786
}else{
787
if( mHttpFlags & HTTP_GENERIC ){
788
if( mHttpFlags & HTTP_NOCOMPRESS ) isCompressed = 0;
789
}else if( fossil_strnicmp(&zLine[14], "application/x-fossil", -1)!=0 ){
790
isError = 1;
791
}
792
}
793
}
794
}
795
if( iHttpVersion<0 ){
796
/* We got nothing back from the server. If using the ssh: protocol,
797
** this might mean we need to add or remove the PATH=... argument
798
** to the SSH command being sent. If that is the case, retry the
799
** request after adding or removing the PATH= argument.
800
*/
801
if( g.url.isSsh /* This is an SSH: sync */
802
&& (g.url.flags & URL_SSH_EXE)==0 /* Does not have ?fossil=.... */
803
&& (g.url.flags & URL_SSH_RETRY)==0 /* Not retried already */
804
){
805
/* Retry after flipping the SSH_PATH setting */
806
transport_close(&g.url);
807
if( (mHttpFlags & HTTP_QUIET)==0 ){
808
fossil_print(
809
"First attempt to run fossil on %s using SSH failed.\n"
810
"Retrying %s the PATH= argument.\n",
811
g.url.hostname,
812
(g.url.flags & URL_SSH_PATH)!=0 ? "without" : "with"
813
);
814
}
815
g.url.flags ^= URL_SSH_PATH|URL_SSH_RETRY;
816
rc = http_exchange(pSend,pReply,mHttpFlags,0,zAltMimetype);
817
if( rc==0 && g.db!=0 ){
818
(void)ssh_needs_path_argument(g.url.hostname,
819
(g.url.flags & URL_SSH_PATH)!=0);
820
}
821
return rc;
822
}else{
823
/* The problem could not be corrected by retrying. Report the
824
** the error. */
825
if( mHttpFlags & HTTP_QUIET ){
826
/* no-op */
827
}else if( g.url.isSsh && !g.fSshTrace ){
828
fossil_warning("server did not reply: "
829
" rerun with --sshtrace for diagnostics");
830
}else{
831
fossil_warning("server did not reply");
832
}
833
goto write_err;
834
}
835
}
836
if( rc!=200 ){
837
if( mHttpFlags & HTTP_QUIET ) goto write_err;
838
fossil_warning("\"location:\" missing from %d redirect reply", rc);
839
goto write_err;
840
}
841
842
if( isChunked && iLength>=0 ){
843
/* RFC 7230 says to reject in this case */
844
fossil_warning("reply has both content-length and transfer-encoding");
845
goto write_err;
846
}
847
848
/*
849
** Extract the reply payload that follows the header
850
*/
851
blob_zero(pReply);
852
if( isChunked ){
853
/* Decode an HTTP/1.1 "Transfer-Encoding: chunked" reply body. Each
854
** chunk is a hex length on its own line (optionally followed by a
855
** ";extension" that is ignored), then that many payload bytes, then a
856
** bare CRLF. A zero-length chunk terminates the body, after which any
857
** trailer header lines are read and discarded up to the blank line. */
858
char *zChunk;
859
int sawTerminator = 0; /* True once the 0-length chunk is seen */
860
while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){
861
i64 nChunk = 0; /* Size of this chunk in bytes (wide, unclamped) */
862
i64 nPrior; /* Bytes already in pReply (matches blob nUsed) */
863
int j; /* Number of hex digits in the chunk size */
864
/* The size is one or more hex digits (RFC 9112 section 7.1),
865
** with no sign, "0x" or leading space. */
866
for(j=0; fossil_isXdigit(zChunk[j]) && nChunk<=0x7fffffff; j++){
867
nChunk = nChunk*16 + hex_digit_value(zChunk[j]);
868
}
869
if( j==0 ){
870
/* No hex digit consumed: a blank or malformed chunk-size line, which
871
** is the symptom of a connection that closed mid-stream. Treat it as
872
** a truncated (failed) */
873
fossil_warning("chunked reply: missing or malformed chunk size");
874
goto write_err;
875
}
876
if( nChunk>0x7fffffff ){
877
/* Larger than we will ever accept in one chunk. */
878
fossil_warning("chunked reply: invalid chunk size");
879
goto write_err;
880
}
881
if( !http_chunk_ext_ok(&zChunk[j]) ){
882
fossil_warning("chunked reply: malformed chunk size line");
883
goto write_err;
884
}
885
if( nChunk==0 ){
886
/* Final chunk: consume trailer lines up to the terminating blank. */
887
sawTerminator = 1;
888
while( (zChunk = transport_receive_line(&g.url))!=0 && zChunk[0]!=0 ){}
889
break;
890
}
891
nPrior = blob_size(pReply);
892
/* Grow the reply buffer, then restore nUsed, so that on error the
893
** blob's reported size is bytes received not claimed chunk length */
894
blob_resize(pReply, (u64)(nPrior+nChunk));
895
pReply->nUsed = (unsigned int)nPrior;
896
{
897
unsigned int nRemaining = (unsigned int)nChunk;
898
/* transport_receive() may return short; loop until the chunk is
899
** full. */
900
while( nRemaining>0 ){
901
int nGot;
902
nGot = transport_receive(&g.url, &pReply->aData[nPrior], nRemaining);
903
if( nGot<=0 ){
904
fossil_warning("chunked reply truncated");
905
goto write_err;
906
}
907
nPrior += nGot;
908
nRemaining -= nGot;
909
pReply->nUsed = (unsigned int)nPrior;
910
}
911
}
912
/* The chunk data ends with CRLF (RFC 9112 section 7.1), which
913
** reads as an empty line. Anything else means the chunk size
914
** was wrong. */
915
zChunk = transport_receive_line(&g.url);
916
if( zChunk==0 || zChunk[0]!=0 ){
917
fossil_warning("chunked reply: no line end after chunk data");
918
goto write_err;
919
}
920
}
921
if( !sawTerminator ){
922
/* The loop exited without ever seeing the 0-length terminator chunk,
923
** meaning the peer closed before the body was complete. A truncated
924
** sync must be an error, never silent success. */
925
fossil_warning("chunked reply ended without terminator");
926
goto write_err;
927
}
928
}else if( iLength==0 ){
929
/* No content to read */
930
}else if( iLength>0 ){
931
/* Read content of a known length */
932
int iRecvLen; /* Received length of the reply payload */
933
blob_resize(pReply, iLength);
934
iRecvLen = transport_receive(&g.url, blob_buffer(pReply), iLength);
935
if( mHttpFlags & HTTP_VERBOSE ){
936
fossil_print("Reply received: %d of %d bytes\n", iRecvLen, iLength);
937
}
938
if( iRecvLen != iLength ){
939
if( mHttpFlags & HTTP_QUIET ) goto write_err;
940
fossil_warning("response truncated: got %d bytes of %d",
941
iRecvLen, iLength);
942
goto write_err;
943
}
944
}else{
945
/* Read content until end-of-file */
946
int iRecvLen; /* Received length of the reply payload */
947
unsigned int nReq = 1000;
948
unsigned int nPrior = 0;
949
closeConnection = 1;
950
do{
951
nReq *= 2;
952
blob_resize(pReply, nPrior+nReq);
953
iRecvLen = transport_receive(&g.url, &pReply->aData[nPrior], (int)nReq);
954
nPrior += iRecvLen;
955
pReply->nUsed = nPrior;
956
}while( iRecvLen==nReq && nReq<0x20000000 );
957
if( mHttpFlags & HTTP_VERBOSE ){
958
fossil_print("Reply received: %u bytes (w/o content-length)\n", nPrior);
959
}
960
}
961
if( isError ){
962
char *z;
963
int i, j;
964
z = blob_str(pReply);
965
for(i=j=0; z[i]; i++, j++){
966
if( z[i]=='<' ){
967
while( z[i] && z[i]!='>' ) i++;
968
if( z[i]==0 ) break;
969
}
970
z[j] = z[i];
971
}
972
z[j] = 0;
973
if( mHttpFlags & HTTP_QUIET ){
974
/* no-op */
975
}else if( mHttpFlags & HTTP_VERBOSE ){
976
fossil_warning("server sends error: %s", z);
977
}else{
978
fossil_warning("server sends error");
979
}
980
goto write_err;
981
}
982
if( isCompressed ) blob_uncompress(pReply, pReply);
983
984
/*
985
** Close the connection to the server if appropriate.
986
**
987
** FIXME: There is some bug in the lower layers that prevents the
988
** connection from remaining open. The easiest fix for now is to
989
** simply close and restart the connection for each round-trip.
990
**
991
** For SSH we will leave the connection open.
992
*/
993
if( ! g.url.isSsh ) closeConnection = 1; /* FIX ME */
994
if( closeConnection ){
995
transport_close(&g.url);
996
}else{
997
transport_rewind(&g.url);
998
}
999
return 0;
1000
1001
/*
1002
** Jump to here if an error is seen.
1003
*/
1004
write_err:
1005
g.iResultCode = 1;
1006
transport_close(&g.url);
1007
return 1;
1008
}
1009
1010
/*
1011
** COMMAND: test-httpmsg
1012
**
1013
** Usage: %fossil test-httpmsg ?OPTIONS? URL ?PAYLOAD? ?OUTPUT?
1014
**
1015
** Send an HTTP message to URL and get the reply. PAYLOAD is a file containing
1016
** the payload, or "-" to read payload from standard input. A POST message
1017
** is sent if PAYLOAD is specified and is non-empty. If PAYLOAD is omitted
1018
** or is an empty file, then a GET message is sent.
1019
**
1020
** If a second filename (OUTPUT) is given after PAYLOAD, then the reply
1021
** is written into that second file instead of being written on standard
1022
** output. Use the "--out OUTPUT" option to specify an output file for
1023
** a GET request where there is no PAYLOAD.
1024
**
1025
** Options:
1026
** --compress Use ZLIB compression on the payload
1027
** --mimetype TYPE Mimetype of the payload
1028
** --no-cert-verify Disable TLS cert verification
1029
** --out FILE Store the reply in FILE
1030
** --subpath PATH HTTP request path for ssh: and file: URLs
1031
** -v Verbose output
1032
** --xfer PAYLOAD in a Fossil xfer protocol message
1033
*/
1034
void test_httpmsg_command(void){
1035
const char *zMimetype;
1036
const char *zInFile;
1037
const char *zOutFile;
1038
const char *zSubpath;
1039
Blob in, out;
1040
unsigned int mHttpFlags = HTTP_GENERIC|HTTP_NOCOMPRESS;
1041
1042
zMimetype = find_option("mimetype",0,1);
1043
zOutFile = find_option("out","o",1);
1044
if( find_option("verbose","v",0)!=0 ) mHttpFlags |= HTTP_VERBOSE;
1045
if( find_option("compress",0,0)!=0 ) mHttpFlags &= ~HTTP_NOCOMPRESS;
1046
if( find_option("no-cert-verify",0,0)!=0 ){
1047
#ifdef FOSSIL_ENABLE_SSL
1048
ssl_disable_cert_verification();
1049
#endif
1050
}
1051
if( find_option("xfer",0,0)!=0 ){
1052
mHttpFlags |= HTTP_USE_LOGIN;
1053
mHttpFlags &= ~HTTP_GENERIC;
1054
}
1055
if( find_option("ipv4",0,0) ) g.eIPvers = 1;
1056
if( find_option("ipv6",0,0) ) g.eIPvers = 2;
1057
zSubpath = find_option("subpath",0,1);
1058
verify_all_options();
1059
if( g.argc<3 || g.argc>5 ){
1060
usage("URL ?PAYLOAD? ?OUTPUT?");
1061
}
1062
zInFile = g.argc>=4 ? g.argv[3] : 0;
1063
if( g.argc==5 ){
1064
if( zOutFile ){
1065
fossil_fatal("output file specified twice: \"--out %s\" and \"%s\"",
1066
zOutFile, g.argv[4]);
1067
}
1068
zOutFile = g.argv[4];
1069
}
1070
url_parse(g.argv[2], 0);
1071
if( g.url.protocol[0]!='h' ){
1072
if( zSubpath==0 ){
1073
fossil_fatal("the --subpath option is required for %s://",g.url.protocol);
1074
}else{
1075
g.url.subpath = fossil_strdup(zSubpath);
1076
}
1077
}
1078
if( zInFile ){
1079
blob_read_from_file(&in, zInFile, ExtFILE);
1080
if( zMimetype==0 && (mHttpFlags & HTTP_GENERIC)!=0 ){
1081
if( fossil_strcmp(zInFile,"-")==0 ){
1082
zMimetype = "application/x-unknown";
1083
}else{
1084
zMimetype = mimetype_from_name(zInFile);
1085
}
1086
}
1087
}else{
1088
blob_init(&in, 0, 0);
1089
}
1090
blob_init(&out, 0, 0);
1091
if( (mHttpFlags & HTTP_VERBOSE)==0 && zOutFile==0 ){
1092
zOutFile = "-";
1093
mHttpFlags |= HTTP_QUIET;
1094
}
1095
http_exchange(&in, &out, mHttpFlags, 4, zMimetype);
1096
if( zOutFile ) blob_write_to_file(&out, zOutFile);
1097
blob_zero(&in);
1098
blob_zero(&out);
1099
}
1100

Keyboard Shortcuts

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