Fossil SCM
Teach the sync protocol how to work with an out-of-band login card, saving an extra server-side of the sync content which is required only to accout for an inlined login card. i.e. it saves RAM, potentially lots of it. The new login card mechanism is instead transported via an HTTP header. This also, not coincidentally, simplifies implementation of the login card in non-fossil(1) clients which are currently learning to speak the sync protocol.
Commit
18628904c30c9c55cee173738cf606cab5fcf92631b1b2331758243224ad4114
Parent
4c3e1728e1b1a9c…
14 files changed
+22
-9
+51
-22
+2
-2
+2
-2
+30
-6
+30
-6
+1
-1
+1
-1
+1
-1
+1
-1
+55
-13
+55
-13
+2
+28
-16
+22
-9
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -964,11 +964,11 @@ | ||
| 964 | 964 | ** * it is impossible for a cookie or query parameter to |
| 965 | 965 | ** override the value of an environment variable since |
| 966 | 966 | ** environment variables always have uppercase names. |
| 967 | 967 | ** |
| 968 | 968 | ** 2018-03-29: Also ignore the entry if NAME that contains any characters |
| 969 | -** other than [a-zA-Z0-9_]. There are no known exploits involving unusual | |
| 969 | +** other than [-a-zA-Z0-9_]. There are no known exploits involving unusual | |
| 970 | 970 | ** names that contain characters outside that set, but it never hurts to |
| 971 | 971 | ** be extra cautious when sanitizing inputs. |
| 972 | 972 | ** |
| 973 | 973 | ** Parameters are separated by the "terminator" character. Whitespace |
| 974 | 974 | ** before the NAME is ignored. |
| @@ -1280,36 +1280,49 @@ | ||
| 1280 | 1280 | |
| 1281 | 1281 | /* Forward declaration */ |
| 1282 | 1282 | static NORETURN void malformed_request(const char *zMsg, ...); |
| 1283 | 1283 | |
| 1284 | 1284 | /* |
| 1285 | -** Checks the QUERY_STRING environment variable, sets it up | |
| 1286 | -** via add_param_list() and, if found, applies its "skin" | |
| 1287 | -** setting. Returns 0 if no QUERY_STRING is set, 1 if it is, | |
| 1288 | -** and 2 if it sets the skin (in which case the cookie may | |
| 1289 | -** still need flushing by the page, via cookie_render()). | |
| 1285 | +** Checks the QUERY_STRING environment variable, sets it up via | |
| 1286 | +** add_param_list() and, if found, applies its "skin" setting. Returns | |
| 1287 | +** 0 if no QUERY_STRING is set, else it returns a bitmask of: | |
| 1288 | +** | |
| 1289 | +** 0x01 = QUERY_STRING was set up | |
| 1290 | +** 0x02 = "skin" URL param arg was processed | |
| 1291 | +** 0x04 = "x-f-l-c" cookie arg was processed. | |
| 1292 | +** | |
| 1293 | +* In the case of the skin, the cookie may still need flushing | |
| 1294 | +** by the page, via cookie_render(). | |
| 1290 | 1295 | */ |
| 1291 | 1296 | int cgi_setup_query_string(void){ |
| 1292 | 1297 | int rc = 0; |
| 1293 | 1298 | char * z = (char*)P("QUERY_STRING"); |
| 1294 | 1299 | if( z ){ |
| 1295 | - ++rc; | |
| 1300 | + rc = 0x01; | |
| 1296 | 1301 | z = fossil_strdup(z); |
| 1297 | 1302 | add_param_list(z, '&'); |
| 1298 | 1303 | z = (char*)P("skin"); |
| 1299 | 1304 | if( z ){ |
| 1300 | 1305 | char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM); |
| 1301 | - ++rc; | |
| 1306 | + rc |= 0x02; | |
| 1302 | 1307 | if( !zErr && P("once")==0 ){ |
| 1303 | 1308 | cookie_write_parameter("skin","skin",z); |
| 1304 | 1309 | /* Per /chat discussion, passing ?skin=... without "once" |
| 1305 | 1310 | ** implies the "udc" argument, so we force that into the |
| 1306 | 1311 | ** environment here. */ |
| 1307 | 1312 | cgi_set_parameter_nocopy("udc", "1", 1); |
| 1308 | 1313 | } |
| 1309 | 1314 | fossil_free(zErr); |
| 1310 | 1315 | } |
| 1316 | + } | |
| 1317 | + if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){ | |
| 1318 | + /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie | |
| 1319 | + ** instead of in the sync payload. */ | |
| 1320 | + rc |= 0x04; | |
| 1321 | + g.syncInfo.zLoginCard = fossil_strdup(z); | |
| 1322 | + g.syncInfo.fLoginCardMode |= 0x02; | |
| 1323 | + cgi_delete_parameter("x-f-l-c"); | |
| 1311 | 1324 | } |
| 1312 | 1325 | return rc; |
| 1313 | 1326 | } |
| 1314 | 1327 | |
| 1315 | 1328 | /* |
| @@ -2125,10 +2138,11 @@ | ||
| 2125 | 2138 | int i; |
| 2126 | 2139 | const char *zScheme = "http"; |
| 2127 | 2140 | char zLine[2000]; /* A single line of input. */ |
| 2128 | 2141 | g.fullHttpReply = 1; |
| 2129 | 2142 | g.zReqType = "HTTP"; |
| 2143 | + | |
| 2130 | 2144 | if( cgi_fgets(zLine, sizeof(zLine))==0 ){ |
| 2131 | 2145 | malformed_request("missing header"); |
| 2132 | 2146 | } |
| 2133 | 2147 | blob_append(&g.httpHeader, zLine, -1); |
| 2134 | 2148 | cgi_trace(zLine); |
| @@ -2160,11 +2174,10 @@ | ||
| 2160 | 2174 | } |
| 2161 | 2175 | if( zIpAddr ){ |
| 2162 | 2176 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 2163 | 2177 | g.zIpAddr = fossil_strdup(zIpAddr); |
| 2164 | 2178 | } |
| 2165 | - | |
| 2166 | 2179 | |
| 2167 | 2180 | /* Get all the optional fields that follow the first line. |
| 2168 | 2181 | */ |
| 2169 | 2182 | while( cgi_fgets(zLine,sizeof(zLine)) ){ |
| 2170 | 2183 | char *zFieldName; |
| 2171 | 2184 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -964,11 +964,11 @@ | |
| 964 | ** * it is impossible for a cookie or query parameter to |
| 965 | ** override the value of an environment variable since |
| 966 | ** environment variables always have uppercase names. |
| 967 | ** |
| 968 | ** 2018-03-29: Also ignore the entry if NAME that contains any characters |
| 969 | ** other than [a-zA-Z0-9_]. There are no known exploits involving unusual |
| 970 | ** names that contain characters outside that set, but it never hurts to |
| 971 | ** be extra cautious when sanitizing inputs. |
| 972 | ** |
| 973 | ** Parameters are separated by the "terminator" character. Whitespace |
| 974 | ** before the NAME is ignored. |
| @@ -1280,36 +1280,49 @@ | |
| 1280 | |
| 1281 | /* Forward declaration */ |
| 1282 | static NORETURN void malformed_request(const char *zMsg, ...); |
| 1283 | |
| 1284 | /* |
| 1285 | ** Checks the QUERY_STRING environment variable, sets it up |
| 1286 | ** via add_param_list() and, if found, applies its "skin" |
| 1287 | ** setting. Returns 0 if no QUERY_STRING is set, 1 if it is, |
| 1288 | ** and 2 if it sets the skin (in which case the cookie may |
| 1289 | ** still need flushing by the page, via cookie_render()). |
| 1290 | */ |
| 1291 | int cgi_setup_query_string(void){ |
| 1292 | int rc = 0; |
| 1293 | char * z = (char*)P("QUERY_STRING"); |
| 1294 | if( z ){ |
| 1295 | ++rc; |
| 1296 | z = fossil_strdup(z); |
| 1297 | add_param_list(z, '&'); |
| 1298 | z = (char*)P("skin"); |
| 1299 | if( z ){ |
| 1300 | char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM); |
| 1301 | ++rc; |
| 1302 | if( !zErr && P("once")==0 ){ |
| 1303 | cookie_write_parameter("skin","skin",z); |
| 1304 | /* Per /chat discussion, passing ?skin=... without "once" |
| 1305 | ** implies the "udc" argument, so we force that into the |
| 1306 | ** environment here. */ |
| 1307 | cgi_set_parameter_nocopy("udc", "1", 1); |
| 1308 | } |
| 1309 | fossil_free(zErr); |
| 1310 | } |
| 1311 | } |
| 1312 | return rc; |
| 1313 | } |
| 1314 | |
| 1315 | /* |
| @@ -2125,10 +2138,11 @@ | |
| 2125 | int i; |
| 2126 | const char *zScheme = "http"; |
| 2127 | char zLine[2000]; /* A single line of input. */ |
| 2128 | g.fullHttpReply = 1; |
| 2129 | g.zReqType = "HTTP"; |
| 2130 | if( cgi_fgets(zLine, sizeof(zLine))==0 ){ |
| 2131 | malformed_request("missing header"); |
| 2132 | } |
| 2133 | blob_append(&g.httpHeader, zLine, -1); |
| 2134 | cgi_trace(zLine); |
| @@ -2160,11 +2174,10 @@ | |
| 2160 | } |
| 2161 | if( zIpAddr ){ |
| 2162 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 2163 | g.zIpAddr = fossil_strdup(zIpAddr); |
| 2164 | } |
| 2165 | |
| 2166 | |
| 2167 | /* Get all the optional fields that follow the first line. |
| 2168 | */ |
| 2169 | while( cgi_fgets(zLine,sizeof(zLine)) ){ |
| 2170 | char *zFieldName; |
| 2171 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -964,11 +964,11 @@ | |
| 964 | ** * it is impossible for a cookie or query parameter to |
| 965 | ** override the value of an environment variable since |
| 966 | ** environment variables always have uppercase names. |
| 967 | ** |
| 968 | ** 2018-03-29: Also ignore the entry if NAME that contains any characters |
| 969 | ** other than [-a-zA-Z0-9_]. There are no known exploits involving unusual |
| 970 | ** names that contain characters outside that set, but it never hurts to |
| 971 | ** be extra cautious when sanitizing inputs. |
| 972 | ** |
| 973 | ** Parameters are separated by the "terminator" character. Whitespace |
| 974 | ** before the NAME is ignored. |
| @@ -1280,36 +1280,49 @@ | |
| 1280 | |
| 1281 | /* Forward declaration */ |
| 1282 | static NORETURN void malformed_request(const char *zMsg, ...); |
| 1283 | |
| 1284 | /* |
| 1285 | ** Checks the QUERY_STRING environment variable, sets it up via |
| 1286 | ** add_param_list() and, if found, applies its "skin" setting. Returns |
| 1287 | ** 0 if no QUERY_STRING is set, else it returns a bitmask of: |
| 1288 | ** |
| 1289 | ** 0x01 = QUERY_STRING was set up |
| 1290 | ** 0x02 = "skin" URL param arg was processed |
| 1291 | ** 0x04 = "x-f-l-c" cookie arg was processed. |
| 1292 | ** |
| 1293 | * In the case of the skin, the cookie may still need flushing |
| 1294 | ** by the page, via cookie_render(). |
| 1295 | */ |
| 1296 | int cgi_setup_query_string(void){ |
| 1297 | int rc = 0; |
| 1298 | char * z = (char*)P("QUERY_STRING"); |
| 1299 | if( z ){ |
| 1300 | rc = 0x01; |
| 1301 | z = fossil_strdup(z); |
| 1302 | add_param_list(z, '&'); |
| 1303 | z = (char*)P("skin"); |
| 1304 | if( z ){ |
| 1305 | char *zErr = skin_use_alternative(z, 2, SKIN_FROM_QPARAM); |
| 1306 | rc |= 0x02; |
| 1307 | if( !zErr && P("once")==0 ){ |
| 1308 | cookie_write_parameter("skin","skin",z); |
| 1309 | /* Per /chat discussion, passing ?skin=... without "once" |
| 1310 | ** implies the "udc" argument, so we force that into the |
| 1311 | ** environment here. */ |
| 1312 | cgi_set_parameter_nocopy("udc", "1", 1); |
| 1313 | } |
| 1314 | fossil_free(zErr); |
| 1315 | } |
| 1316 | } |
| 1317 | if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){ |
| 1318 | /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie |
| 1319 | ** instead of in the sync payload. */ |
| 1320 | rc |= 0x04; |
| 1321 | g.syncInfo.zLoginCard = fossil_strdup(z); |
| 1322 | g.syncInfo.fLoginCardMode |= 0x02; |
| 1323 | cgi_delete_parameter("x-f-l-c"); |
| 1324 | } |
| 1325 | return rc; |
| 1326 | } |
| 1327 | |
| 1328 | /* |
| @@ -2125,10 +2138,11 @@ | |
| 2138 | int i; |
| 2139 | const char *zScheme = "http"; |
| 2140 | char zLine[2000]; /* A single line of input. */ |
| 2141 | g.fullHttpReply = 1; |
| 2142 | g.zReqType = "HTTP"; |
| 2143 | |
| 2144 | if( cgi_fgets(zLine, sizeof(zLine))==0 ){ |
| 2145 | malformed_request("missing header"); |
| 2146 | } |
| 2147 | blob_append(&g.httpHeader, zLine, -1); |
| 2148 | cgi_trace(zLine); |
| @@ -2160,11 +2174,10 @@ | |
| 2174 | } |
| 2175 | if( zIpAddr ){ |
| 2176 | cgi_setenv("REMOTE_ADDR", zIpAddr); |
| 2177 | g.zIpAddr = fossil_strdup(zIpAddr); |
| 2178 | } |
| 2179 | |
| 2180 | /* Get all the optional fields that follow the first line. |
| 2181 | */ |
| 2182 | while( cgi_fgets(zLine,sizeof(zLine)) ){ |
| 2183 | char *zFieldName; |
| 2184 |
+51
-22
| --- src/http.c | ||
| +++ src/http.c | ||
| @@ -52,30 +52,33 @@ | ||
| 52 | 52 | ** Construct the "login" card with the client credentials. |
| 53 | 53 | ** |
| 54 | 54 | ** login LOGIN NONCE SIGNATURE |
| 55 | 55 | ** |
| 56 | 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 NONCE | |
| 58 | -** must be provided in the payload (in xfer.c). SIGNATURE is the sha1 | |
| 59 | -** checksum of the nonce followed by the user password. | |
| 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. | |
| 60 | 62 | ** |
| 61 | -** Write the constructed login card into pLogin. pLogin is initialized | |
| 62 | -** by this routine. | |
| 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. | |
| 63 | 66 | */ |
| 64 | -static void http_build_login_card(Blob *pPayload, Blob *pLogin){ | |
| 67 | +static void http_build_login_card(Blob * const pPayload, Blob * const pLogin){ | |
| 65 | 68 | Blob nonce; /* The nonce */ |
| 66 | 69 | const char *zLogin; /* The user login name */ |
| 67 | 70 | const char *zPw; /* The user password */ |
| 68 | 71 | Blob pw; /* The nonce with user password appended */ |
| 69 | 72 | Blob sig; /* The signature field */ |
| 70 | 73 | |
| 71 | 74 | blob_zero(pLogin); |
| 72 | 75 | if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){ |
| 73 | - return; /* If no login card for users "nobody" and "anonymous" */ | |
| 76 | + return; /* No login card for users "nobody" and "anonymous" */ | |
| 74 | 77 | } |
| 75 | 78 | if( g.url.isSsh ){ |
| 76 | - return; /* If no login card for SSH: */ | |
| 79 | + return; /* No login card for SSH: */ | |
| 77 | 80 | } |
| 78 | 81 | blob_zero(&nonce); |
| 79 | 82 | blob_zero(&pw); |
| 80 | 83 | sha1sum_blob(pPayload, &nonce); |
| 81 | 84 | blob_copy(&pw, &nonce); |
| @@ -119,32 +122,34 @@ | ||
| 119 | 122 | g.url.passwd = fossil_strdup(zPw); |
| 120 | 123 | } |
| 121 | 124 | |
| 122 | 125 | blob_append(&pw, zPw, -1); |
| 123 | 126 | sha1sum_blob(&pw, &sig); |
| 124 | - blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig); | |
| 127 | + blob_appendf(pLogin, "login %F %b %b", zLogin, &nonce, &sig); | |
| 125 | 128 | blob_reset(&pw); |
| 126 | 129 | blob_reset(&sig); |
| 127 | 130 | blob_reset(&nonce); |
| 128 | 131 | } |
| 129 | 132 | |
| 130 | 133 | /* |
| 131 | 134 | ** Construct an appropriate HTTP request header. Write the header |
| 132 | 135 | ** into pHdr. This routine initializes the pHdr blob. pPayload is |
| 133 | -** the complete payload (including the login card) already compressed. | |
| 136 | +** the complete payload (including the login card if pLogin is NULL or | |
| 137 | +** empty) already compressed. | |
| 134 | 138 | */ |
| 135 | 139 | static void http_build_header( |
| 136 | 140 | Blob *pPayload, /* the payload that will be sent */ |
| 137 | 141 | Blob *pHdr, /* construct the header here */ |
| 142 | + Blob *pLogin, /* Login card header value or NULL */ | |
| 138 | 143 | const char *zAltMimetype /* Alternative mimetype */ |
| 139 | 144 | ){ |
| 140 | 145 | int nPayload = pPayload ? blob_size(pPayload) : 0; |
| 141 | 146 | |
| 142 | 147 | blob_zero(pHdr); |
| 143 | - blob_appendf(pHdr, "%s %s%s HTTP/1.0\r\n", | |
| 144 | - nPayload>0 ? "POST" : "GET", g.url.path, | |
| 145 | - g.url.path[0]==0 ? "/" : ""); | |
| 148 | + blob_appendf(pHdr, "%s %s HTTP/1.0\r\n", | |
| 149 | + nPayload>0 ? "POST" : "GET", | |
| 150 | + (g.url.path && g.url.path[0]) ? g.url.path : "/"); | |
| 146 | 151 | if( g.url.proxyAuth ){ |
| 147 | 152 | blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth); |
| 148 | 153 | } |
| 149 | 154 | if( g.zHttpAuth && g.zHttpAuth[0] ){ |
| 150 | 155 | const char *zCredentials = g.zHttpAuth; |
| @@ -153,10 +158,16 @@ | ||
| 153 | 158 | fossil_free(zEncoded); |
| 154 | 159 | } |
| 155 | 160 | blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname); |
| 156 | 161 | blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent()); |
| 157 | 162 | if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n"); |
| 163 | + if( g.syncInfo.fLoginCardMode>0 | |
| 164 | + && nPayload>0 && pLogin && blob_size(pLogin) ){ | |
| 165 | + /* Add sync login card via a transient cookie. We can only do this | |
| 166 | + if we know the remote supports it. */ | |
| 167 | + blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(pLogin)); | |
| 168 | + } | |
| 158 | 169 | if( nPayload ){ |
| 159 | 170 | if( zAltMimetype ){ |
| 160 | 171 | blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype); |
| 161 | 172 | }else if( g.fHttpTrace ){ |
| 162 | 173 | blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n"); |
| @@ -386,11 +397,11 @@ | ||
| 386 | 397 | ** * The test-ssh-needs-path command that shows the settings |
| 387 | 398 | ** that cache whether or not a PATH= is needed for a particular |
| 388 | 399 | ** HOSTNAME. |
| 389 | 400 | */ |
| 390 | 401 | void ssh_add_path_argument(Blob *pCmd){ |
| 391 | - blob_append_escaped_arg(pCmd, | |
| 402 | + blob_append_escaped_arg(pCmd, | |
| 392 | 403 | "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1); |
| 393 | 404 | } |
| 394 | 405 | |
| 395 | 406 | /* |
| 396 | 407 | ** Return the complete text of the last HTTP reply as saved in the |
| @@ -457,28 +468,46 @@ | ||
| 457 | 468 | |
| 458 | 469 | if( transport_open(&g.url) ){ |
| 459 | 470 | fossil_warning("%s", transport_errmsg(&g.url)); |
| 460 | 471 | return 1; |
| 461 | 472 | } |
| 462 | - | |
| 463 | 473 | /* Construct the login card and prepare the complete payload */ |
| 474 | + blob_zero(&login); | |
| 464 | 475 | if( blob_size(pSend)==0 ){ |
| 465 | 476 | blob_zero(&payload); |
| 466 | 477 | }else{ |
| 467 | - blob_zero(&login); | |
| 468 | 478 | if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login); |
| 469 | - if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ | |
| 470 | - payload = login; | |
| 471 | - blob_append(&payload, blob_buffer(pSend), blob_size(pSend)); | |
| 479 | + if( g.syncInfo.fLoginCardMode ){ | |
| 480 | + /* The login card will be sent via an HTTP header and/or URL flag. */ | |
| 481 | + if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ | |
| 482 | + /* Maintenance note: we cannot blob_swap(pSend,&payload) here | |
| 483 | + ** because the HTTP 401 and redirect response handling below | |
| 484 | + ** needs pSend unmodified. payload won't be modified after | |
| 485 | + ** this point, so we can make it a proxy for pSend for | |
| 486 | + ** zero heap memory. */ | |
| 487 | + blob_init(&payload, blob_buffer(pSend), blob_size(pSend)); | |
| 488 | + }else{ | |
| 489 | + blob_compress(pSend, &payload); | |
| 490 | + } | |
| 472 | 491 | }else{ |
| 473 | - blob_compress2(&login, pSend, &payload); | |
| 474 | - blob_reset(&login); | |
| 492 | + /* Prepend the login card (if set) to the payload */ | |
| 493 | + if( blob_size(&login) ){ | |
| 494 | + blob_append_char(&login, '\n'); | |
| 495 | + } | |
| 496 | + if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ | |
| 497 | + payload = login; | |
| 498 | + login = empty_blob/*transfer ownership*/; | |
| 499 | + blob_append(&payload, blob_buffer(pSend), blob_size(pSend)); | |
| 500 | + }else{ | |
| 501 | + blob_compress2(&login, pSend, &payload); | |
| 502 | + blob_reset(&login); | |
| 503 | + } | |
| 475 | 504 | } |
| 476 | 505 | } |
| 477 | 506 | |
| 478 | 507 | /* Construct the HTTP request header */ |
| 479 | - http_build_header(&payload, &hdr, zAltMimetype); | |
| 508 | + http_build_header(&payload, &hdr, &login, zAltMimetype); | |
| 480 | 509 | |
| 481 | 510 | /* When tracing, write the transmitted HTTP message both to standard |
| 482 | 511 | ** output and into a file. The file can then be used to drive the |
| 483 | 512 | ** server-side like this: |
| 484 | 513 | ** |
| 485 | 514 |
| --- src/http.c | |
| +++ src/http.c | |
| @@ -52,30 +52,33 @@ | |
| 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 NONCE |
| 58 | ** must be provided in the payload (in xfer.c). SIGNATURE is the sha1 |
| 59 | ** checksum of the nonce followed by the user password. |
| 60 | ** |
| 61 | ** Write the constructed login card into pLogin. pLogin is initialized |
| 62 | ** by this routine. |
| 63 | */ |
| 64 | static void http_build_login_card(Blob *pPayload, Blob *pLogin){ |
| 65 | Blob nonce; /* The nonce */ |
| 66 | const char *zLogin; /* The user login name */ |
| 67 | const char *zPw; /* The user password */ |
| 68 | Blob pw; /* The nonce with user password appended */ |
| 69 | Blob sig; /* The signature field */ |
| 70 | |
| 71 | blob_zero(pLogin); |
| 72 | if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){ |
| 73 | return; /* If no login card for users "nobody" and "anonymous" */ |
| 74 | } |
| 75 | if( g.url.isSsh ){ |
| 76 | return; /* If no login card for SSH: */ |
| 77 | } |
| 78 | blob_zero(&nonce); |
| 79 | blob_zero(&pw); |
| 80 | sha1sum_blob(pPayload, &nonce); |
| 81 | blob_copy(&pw, &nonce); |
| @@ -119,32 +122,34 @@ | |
| 119 | g.url.passwd = fossil_strdup(zPw); |
| 120 | } |
| 121 | |
| 122 | blob_append(&pw, zPw, -1); |
| 123 | sha1sum_blob(&pw, &sig); |
| 124 | blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig); |
| 125 | blob_reset(&pw); |
| 126 | blob_reset(&sig); |
| 127 | blob_reset(&nonce); |
| 128 | } |
| 129 | |
| 130 | /* |
| 131 | ** Construct an appropriate HTTP request header. Write the header |
| 132 | ** into pHdr. This routine initializes the pHdr blob. pPayload is |
| 133 | ** the complete payload (including the login card) already compressed. |
| 134 | */ |
| 135 | static void http_build_header( |
| 136 | Blob *pPayload, /* the payload that will be sent */ |
| 137 | Blob *pHdr, /* construct the header here */ |
| 138 | const char *zAltMimetype /* Alternative mimetype */ |
| 139 | ){ |
| 140 | int nPayload = pPayload ? blob_size(pPayload) : 0; |
| 141 | |
| 142 | blob_zero(pHdr); |
| 143 | blob_appendf(pHdr, "%s %s%s HTTP/1.0\r\n", |
| 144 | nPayload>0 ? "POST" : "GET", g.url.path, |
| 145 | g.url.path[0]==0 ? "/" : ""); |
| 146 | if( g.url.proxyAuth ){ |
| 147 | blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth); |
| 148 | } |
| 149 | if( g.zHttpAuth && g.zHttpAuth[0] ){ |
| 150 | const char *zCredentials = g.zHttpAuth; |
| @@ -153,10 +158,16 @@ | |
| 153 | fossil_free(zEncoded); |
| 154 | } |
| 155 | blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname); |
| 156 | blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent()); |
| 157 | if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n"); |
| 158 | if( nPayload ){ |
| 159 | if( zAltMimetype ){ |
| 160 | blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype); |
| 161 | }else if( g.fHttpTrace ){ |
| 162 | blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n"); |
| @@ -386,11 +397,11 @@ | |
| 386 | ** * The test-ssh-needs-path command that shows the settings |
| 387 | ** that cache whether or not a PATH= is needed for a particular |
| 388 | ** HOSTNAME. |
| 389 | */ |
| 390 | void ssh_add_path_argument(Blob *pCmd){ |
| 391 | blob_append_escaped_arg(pCmd, |
| 392 | "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1); |
| 393 | } |
| 394 | |
| 395 | /* |
| 396 | ** Return the complete text of the last HTTP reply as saved in the |
| @@ -457,28 +468,46 @@ | |
| 457 | |
| 458 | if( transport_open(&g.url) ){ |
| 459 | fossil_warning("%s", transport_errmsg(&g.url)); |
| 460 | return 1; |
| 461 | } |
| 462 | |
| 463 | /* Construct the login card and prepare the complete payload */ |
| 464 | if( blob_size(pSend)==0 ){ |
| 465 | blob_zero(&payload); |
| 466 | }else{ |
| 467 | blob_zero(&login); |
| 468 | if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login); |
| 469 | if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ |
| 470 | payload = login; |
| 471 | blob_append(&payload, blob_buffer(pSend), blob_size(pSend)); |
| 472 | }else{ |
| 473 | blob_compress2(&login, pSend, &payload); |
| 474 | blob_reset(&login); |
| 475 | } |
| 476 | } |
| 477 | |
| 478 | /* Construct the HTTP request header */ |
| 479 | http_build_header(&payload, &hdr, zAltMimetype); |
| 480 | |
| 481 | /* When tracing, write the transmitted HTTP message both to standard |
| 482 | ** output and into a file. The file can then be used to drive the |
| 483 | ** server-side like this: |
| 484 | ** |
| 485 |
| --- src/http.c | |
| +++ src/http.c | |
| @@ -52,30 +52,33 @@ | |
| 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); |
| @@ -119,32 +122,34 @@ | |
| 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 | |
| 147 | blob_zero(pHdr); |
| 148 | blob_appendf(pHdr, "%s %s HTTP/1.0\r\n", |
| 149 | nPayload>0 ? "POST" : "GET", |
| 150 | (g.url.path && g.url.path[0]) ? g.url.path : "/"); |
| 151 | if( g.url.proxyAuth ){ |
| 152 | blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth); |
| 153 | } |
| 154 | if( g.zHttpAuth && g.zHttpAuth[0] ){ |
| 155 | const char *zCredentials = g.zHttpAuth; |
| @@ -153,10 +158,16 @@ | |
| 158 | fossil_free(zEncoded); |
| 159 | } |
| 160 | blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname); |
| 161 | blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent()); |
| 162 | if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n"); |
| 163 | if( g.syncInfo.fLoginCardMode>0 |
| 164 | && nPayload>0 && pLogin && blob_size(pLogin) ){ |
| 165 | /* Add sync login card via a transient cookie. We can only do this |
| 166 | if we know the remote supports it. */ |
| 167 | blob_appendf(pHdr, "Cookie: x-f-l-c=%T\r\n", blob_str(pLogin)); |
| 168 | } |
| 169 | if( nPayload ){ |
| 170 | if( zAltMimetype ){ |
| 171 | blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype); |
| 172 | }else if( g.fHttpTrace ){ |
| 173 | blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n"); |
| @@ -386,11 +397,11 @@ | |
| 397 | ** * The test-ssh-needs-path command that shows the settings |
| 398 | ** that cache whether or not a PATH= is needed for a particular |
| 399 | ** HOSTNAME. |
| 400 | */ |
| 401 | void ssh_add_path_argument(Blob *pCmd){ |
| 402 | blob_append_escaped_arg(pCmd, |
| 403 | "PATH=$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$PATH", 1); |
| 404 | } |
| 405 | |
| 406 | /* |
| 407 | ** Return the complete text of the last HTTP reply as saved in the |
| @@ -457,28 +468,46 @@ | |
| 468 | |
| 469 | if( transport_open(&g.url) ){ |
| 470 | fossil_warning("%s", transport_errmsg(&g.url)); |
| 471 | return 1; |
| 472 | } |
| 473 | /* Construct the login card and prepare the complete payload */ |
| 474 | blob_zero(&login); |
| 475 | if( blob_size(pSend)==0 ){ |
| 476 | blob_zero(&payload); |
| 477 | }else{ |
| 478 | if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login); |
| 479 | if( g.syncInfo.fLoginCardMode ){ |
| 480 | /* The login card will be sent via an HTTP header and/or URL flag. */ |
| 481 | if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ |
| 482 | /* Maintenance note: we cannot blob_swap(pSend,&payload) here |
| 483 | ** because the HTTP 401 and redirect response handling below |
| 484 | ** needs pSend unmodified. payload won't be modified after |
| 485 | ** this point, so we can make it a proxy for pSend for |
| 486 | ** zero heap memory. */ |
| 487 | blob_init(&payload, blob_buffer(pSend), blob_size(pSend)); |
| 488 | }else{ |
| 489 | blob_compress(pSend, &payload); |
| 490 | } |
| 491 | }else{ |
| 492 | /* Prepend the login card (if set) to the payload */ |
| 493 | if( blob_size(&login) ){ |
| 494 | blob_append_char(&login, '\n'); |
| 495 | } |
| 496 | if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ |
| 497 | payload = login; |
| 498 | login = empty_blob/*transfer ownership*/; |
| 499 | blob_append(&payload, blob_buffer(pSend), blob_size(pSend)); |
| 500 | }else{ |
| 501 | blob_compress2(&login, pSend, &payload); |
| 502 | blob_reset(&login); |
| 503 | } |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | /* Construct the HTTP request header */ |
| 508 | http_build_header(&payload, &hdr, &login, zAltMimetype); |
| 509 | |
| 510 | /* When tracing, write the transmitted HTTP message both to standard |
| 511 | ** output and into a file. The file can then be used to drive the |
| 512 | ** server-side like this: |
| 513 | ** |
| 514 |
+2
-2
| --- src/http_transport.c | ||
| +++ src/http_transport.c | ||
| @@ -141,11 +141,11 @@ | ||
| 141 | 141 | ){ |
| 142 | 142 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 143 | 143 | "the server.", pUrlData->fossil); |
| 144 | 144 | } |
| 145 | 145 | if( (pUrlData->flags & URL_SSH_EXE)==0 |
| 146 | - && (pUrlData->flags & URL_SSH_PATH)!=0 | |
| 146 | + && (pUrlData->flags & URL_SSH_PATH)!=0 | |
| 147 | 147 | ){ |
| 148 | 148 | ssh_add_path_argument(&zCmd); |
| 149 | 149 | } |
| 150 | 150 | blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); |
| 151 | 151 | blob_append(&zCmd, " test-http", 10); |
| @@ -245,11 +245,11 @@ | ||
| 245 | 245 | } |
| 246 | 246 | |
| 247 | 247 | /* |
| 248 | 248 | ** Send content over the wire. |
| 249 | 249 | */ |
| 250 | -void transport_send(UrlData *pUrlData, Blob *toSend){ | |
| 250 | +void transport_send(UrlData const *pUrlData, const Blob *toSend){ | |
| 251 | 251 | char *z = blob_buffer(toSend); |
| 252 | 252 | int n = blob_size(toSend); |
| 253 | 253 | transport.nSent += n; |
| 254 | 254 | if( pUrlData->isSsh ){ |
| 255 | 255 | fwrite(z, 1, n, sshOut); |
| 256 | 256 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -141,11 +141,11 @@ | |
| 141 | ){ |
| 142 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 143 | "the server.", pUrlData->fossil); |
| 144 | } |
| 145 | if( (pUrlData->flags & URL_SSH_EXE)==0 |
| 146 | && (pUrlData->flags & URL_SSH_PATH)!=0 |
| 147 | ){ |
| 148 | ssh_add_path_argument(&zCmd); |
| 149 | } |
| 150 | blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); |
| 151 | blob_append(&zCmd, " test-http", 10); |
| @@ -245,11 +245,11 @@ | |
| 245 | } |
| 246 | |
| 247 | /* |
| 248 | ** Send content over the wire. |
| 249 | */ |
| 250 | void transport_send(UrlData *pUrlData, Blob *toSend){ |
| 251 | char *z = blob_buffer(toSend); |
| 252 | int n = blob_size(toSend); |
| 253 | transport.nSent += n; |
| 254 | if( pUrlData->isSsh ){ |
| 255 | fwrite(z, 1, n, sshOut); |
| 256 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -141,11 +141,11 @@ | |
| 141 | ){ |
| 142 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 143 | "the server.", pUrlData->fossil); |
| 144 | } |
| 145 | if( (pUrlData->flags & URL_SSH_EXE)==0 |
| 146 | && (pUrlData->flags & URL_SSH_PATH)!=0 |
| 147 | ){ |
| 148 | ssh_add_path_argument(&zCmd); |
| 149 | } |
| 150 | blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); |
| 151 | blob_append(&zCmd, " test-http", 10); |
| @@ -245,11 +245,11 @@ | |
| 245 | } |
| 246 | |
| 247 | /* |
| 248 | ** Send content over the wire. |
| 249 | */ |
| 250 | void transport_send(UrlData const *pUrlData, const Blob *toSend){ |
| 251 | char *z = blob_buffer(toSend); |
| 252 | int n = blob_size(toSend); |
| 253 | transport.nSent += n; |
| 254 | if( pUrlData->isSsh ){ |
| 255 | fwrite(z, 1, n, sshOut); |
| 256 |
+2
-2
| --- src/http_transport.c | ||
| +++ src/http_transport.c | ||
| @@ -141,11 +141,11 @@ | ||
| 141 | 141 | ){ |
| 142 | 142 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 143 | 143 | "the server.", pUrlData->fossil); |
| 144 | 144 | } |
| 145 | 145 | if( (pUrlData->flags & URL_SSH_EXE)==0 |
| 146 | - && (pUrlData->flags & URL_SSH_PATH)!=0 | |
| 146 | + && (pUrlData->flags & URL_SSH_PATH)!=0 | |
| 147 | 147 | ){ |
| 148 | 148 | ssh_add_path_argument(&zCmd); |
| 149 | 149 | } |
| 150 | 150 | blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); |
| 151 | 151 | blob_append(&zCmd, " test-http", 10); |
| @@ -245,11 +245,11 @@ | ||
| 245 | 245 | } |
| 246 | 246 | |
| 247 | 247 | /* |
| 248 | 248 | ** Send content over the wire. |
| 249 | 249 | */ |
| 250 | -void transport_send(UrlData *pUrlData, Blob *toSend){ | |
| 250 | +void transport_send(UrlData const *pUrlData, const Blob *toSend){ | |
| 251 | 251 | char *z = blob_buffer(toSend); |
| 252 | 252 | int n = blob_size(toSend); |
| 253 | 253 | transport.nSent += n; |
| 254 | 254 | if( pUrlData->isSsh ){ |
| 255 | 255 | fwrite(z, 1, n, sshOut); |
| 256 | 256 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -141,11 +141,11 @@ | |
| 141 | ){ |
| 142 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 143 | "the server.", pUrlData->fossil); |
| 144 | } |
| 145 | if( (pUrlData->flags & URL_SSH_EXE)==0 |
| 146 | && (pUrlData->flags & URL_SSH_PATH)!=0 |
| 147 | ){ |
| 148 | ssh_add_path_argument(&zCmd); |
| 149 | } |
| 150 | blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); |
| 151 | blob_append(&zCmd, " test-http", 10); |
| @@ -245,11 +245,11 @@ | |
| 245 | } |
| 246 | |
| 247 | /* |
| 248 | ** Send content over the wire. |
| 249 | */ |
| 250 | void transport_send(UrlData *pUrlData, Blob *toSend){ |
| 251 | char *z = blob_buffer(toSend); |
| 252 | int n = blob_size(toSend); |
| 253 | transport.nSent += n; |
| 254 | if( pUrlData->isSsh ){ |
| 255 | fwrite(z, 1, n, sshOut); |
| 256 |
| --- src/http_transport.c | |
| +++ src/http_transport.c | |
| @@ -141,11 +141,11 @@ | |
| 141 | ){ |
| 142 | fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " |
| 143 | "the server.", pUrlData->fossil); |
| 144 | } |
| 145 | if( (pUrlData->flags & URL_SSH_EXE)==0 |
| 146 | && (pUrlData->flags & URL_SSH_PATH)!=0 |
| 147 | ){ |
| 148 | ssh_add_path_argument(&zCmd); |
| 149 | } |
| 150 | blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); |
| 151 | blob_append(&zCmd, " test-http", 10); |
| @@ -245,11 +245,11 @@ | |
| 245 | } |
| 246 | |
| 247 | /* |
| 248 | ** Send content over the wire. |
| 249 | */ |
| 250 | void transport_send(UrlData const *pUrlData, const Blob *toSend){ |
| 251 | char *z = blob_buffer(toSend); |
| 252 | int n = blob_size(toSend); |
| 253 | transport.nSent += n; |
| 254 | if( pUrlData->isSsh ){ |
| 255 | fwrite(z, 1, n, sshOut); |
| 256 |
+30
-6
| --- src/main.c | ||
| +++ src/main.c | ||
| @@ -288,10 +288,27 @@ | ||
| 288 | 288 | int allowSymlinks; /* Cached "allow-symlinks" option */ |
| 289 | 289 | int mainTimerId; /* Set to fossil_timer_start() */ |
| 290 | 290 | int nPendingRequest; /* # of HTTP requests in "fossil server" */ |
| 291 | 291 | int nRequest; /* Total # of HTTP request */ |
| 292 | 292 | int bAvoidDeltaManifests; /* Avoid using delta manifests if true */ |
| 293 | + | |
| 294 | + /* State for communicating specific details between the inbound HTTP | |
| 295 | + ** header parser (cgi.c), xfer.c, and http.c. */ | |
| 296 | + struct { | |
| 297 | + char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */ | |
| 298 | + int fLoginCardMode; /* If non-0, emit login cards in outbound | |
| 299 | + ** requests as a HTTP cookie instead of as | |
| 300 | + ** part of the payload. Gets activated | |
| 301 | + ** on-demand based on xfer traffic | |
| 302 | + ** contents. Values, for | |
| 303 | + ** diagnostic/debugging purposes: 0x01=CLI | |
| 304 | + ** --flag, 0x02=cgi_setup_query_string(), | |
| 305 | + ** 0x04=page_xfer(), | |
| 306 | + ** 0x08=client_sync(). */ | |
| 307 | + int remoteVersion; /* Remote fossil version. Used for negotiating | |
| 308 | + ** how to handle the login card. */ | |
| 309 | + } syncInfo; | |
| 293 | 310 | #ifdef FOSSIL_ENABLE_JSON |
| 294 | 311 | struct FossilJsonBits { |
| 295 | 312 | int isJsonMode; /* True if running in JSON mode, else |
| 296 | 313 | false. This changes how errors are |
| 297 | 314 | reported. In JSON mode we try to |
| @@ -758,10 +775,17 @@ | ||
| 758 | 775 | g.tcl.argc = g.argc; |
| 759 | 776 | g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */ |
| 760 | 777 | #endif |
| 761 | 778 | g.mainTimerId = fossil_timer_start(); |
| 762 | 779 | capture_case_sensitive_option(); |
| 780 | + g.syncInfo.fLoginCardMode = | |
| 781 | + /* The undocumented/unsupported --login-card-header provides a way | |
| 782 | + ** to force use of the feature added by the xfer-login-card branch | |
| 783 | + ** in 2025-07, intended for assisting in debugging any related | |
| 784 | + ** issues. It can be removed once we reach the level of "implicit | |
| 785 | + ** trust" in that feature. */ | |
| 786 | + find_option("login-card-header",0,0) ? 0x01 : 0; | |
| 763 | 787 | g.zVfsName = find_option("vfs",0,1); |
| 764 | 788 | if( g.zVfsName==0 ){ |
| 765 | 789 | g.zVfsName = fossil_getenv("FOSSIL_VFS"); |
| 766 | 790 | } |
| 767 | 791 | if( g.zVfsName ){ |
| @@ -1490,11 +1514,11 @@ | ||
| 1490 | 1514 | /* In order for ?skin=... to work when visiting the site from |
| 1491 | 1515 | ** a typical external link, we have to process it here, as |
| 1492 | 1516 | ** that parameter gets lost during the redirect. We "could" |
| 1493 | 1517 | ** pass the whole query string along instead, but that seems |
| 1494 | 1518 | ** unnecessary. */ |
| 1495 | - if(cgi_setup_query_string()>1){ | |
| 1519 | + if(cgi_setup_query_string() & 0x02){ | |
| 1496 | 1520 | cookie_render(); |
| 1497 | 1521 | } |
| 1498 | 1522 | cgi_redirectf("%R%s", db_get("index-page", "/index")); |
| 1499 | 1523 | } |
| 1500 | 1524 | |
| @@ -1794,22 +1818,22 @@ | ||
| 1794 | 1818 | } |
| 1795 | 1819 | |
| 1796 | 1820 | |
| 1797 | 1821 | /* Restrictions on the URI for security: |
| 1798 | 1822 | ** |
| 1799 | - ** 1. Reject characters that are not ASCII alphanumerics, | |
| 1823 | + ** 1. Reject characters that are not ASCII alphanumerics, | |
| 1800 | 1824 | ** "-", "_", ".", "/", or unicode (above ASCII). |
| 1801 | 1825 | ** In other words: No ASCII punctuation or control characters |
| 1802 | 1826 | ** other than "-", "_", "." and "/". |
| 1803 | - ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII | |
| 1827 | + ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII | |
| 1804 | 1828 | ** alphabetic character at the beginning of the name on windows. |
| 1805 | 1829 | ** 3. "-" may not occur immediately after "/" |
| 1806 | 1830 | ** 4. "." may not be adjacent to another "." or to "/" |
| 1807 | 1831 | ** |
| 1808 | 1832 | ** Any character does not satisfy these constraints a Not Found |
| 1809 | 1833 | ** error is returned. |
| 1810 | - */ | |
| 1834 | + */ | |
| 1811 | 1835 | szFile = 0; |
| 1812 | 1836 | for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){ |
| 1813 | 1837 | char c = zRepo[j]; |
| 1814 | 1838 | if( c>='a' && c<='z' ) continue; |
| 1815 | 1839 | if( c>='A' && c<='Z' ) continue; |
| @@ -3510,11 +3534,11 @@ | ||
| 3510 | 3534 | ** "fossil ui --nobrowser" on the remote system and to set up a |
| 3511 | 3535 | ** tunnel from the local machine to the remote. */ |
| 3512 | 3536 | FILE *sshIn; |
| 3513 | 3537 | Blob ssh; |
| 3514 | 3538 | int bRunning = 0; /* True when fossil starts up on the remote */ |
| 3515 | - int isRetry; /* True if on the second attempt */ | |
| 3539 | + int isRetry; /* True if on the second attempt */ | |
| 3516 | 3540 | char zLine[1000]; |
| 3517 | 3541 | |
| 3518 | 3542 | blob_init(&ssh, 0, 0); |
| 3519 | 3543 | for(isRetry=0; isRetry<2 && !bRunning; isRetry++){ |
| 3520 | 3544 | blob_reset(&ssh); |
| @@ -3549,11 +3573,11 @@ | ||
| 3549 | 3573 | if( fCreate ) blob_appendf(&ssh, " --create"); |
| 3550 | 3574 | blob_appendf(&ssh, " %$", g.argv[2]); |
| 3551 | 3575 | if( isRetry ){ |
| 3552 | 3576 | fossil_print("First attempt to run \"fossil\" on %s failed\n" |
| 3553 | 3577 | "Retry: ", zRemote); |
| 3554 | - } | |
| 3578 | + } | |
| 3555 | 3579 | fossil_print("%s\n", blob_str(&ssh)); |
| 3556 | 3580 | sshIn = popen(blob_str(&ssh), "r"); |
| 3557 | 3581 | if( sshIn==0 ){ |
| 3558 | 3582 | fossil_fatal("unable to %s", blob_str(&ssh)); |
| 3559 | 3583 | } |
| 3560 | 3584 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -288,10 +288,27 @@ | |
| 288 | int allowSymlinks; /* Cached "allow-symlinks" option */ |
| 289 | int mainTimerId; /* Set to fossil_timer_start() */ |
| 290 | int nPendingRequest; /* # of HTTP requests in "fossil server" */ |
| 291 | int nRequest; /* Total # of HTTP request */ |
| 292 | int bAvoidDeltaManifests; /* Avoid using delta manifests if true */ |
| 293 | #ifdef FOSSIL_ENABLE_JSON |
| 294 | struct FossilJsonBits { |
| 295 | int isJsonMode; /* True if running in JSON mode, else |
| 296 | false. This changes how errors are |
| 297 | reported. In JSON mode we try to |
| @@ -758,10 +775,17 @@ | |
| 758 | g.tcl.argc = g.argc; |
| 759 | g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */ |
| 760 | #endif |
| 761 | g.mainTimerId = fossil_timer_start(); |
| 762 | capture_case_sensitive_option(); |
| 763 | g.zVfsName = find_option("vfs",0,1); |
| 764 | if( g.zVfsName==0 ){ |
| 765 | g.zVfsName = fossil_getenv("FOSSIL_VFS"); |
| 766 | } |
| 767 | if( g.zVfsName ){ |
| @@ -1490,11 +1514,11 @@ | |
| 1490 | /* In order for ?skin=... to work when visiting the site from |
| 1491 | ** a typical external link, we have to process it here, as |
| 1492 | ** that parameter gets lost during the redirect. We "could" |
| 1493 | ** pass the whole query string along instead, but that seems |
| 1494 | ** unnecessary. */ |
| 1495 | if(cgi_setup_query_string()>1){ |
| 1496 | cookie_render(); |
| 1497 | } |
| 1498 | cgi_redirectf("%R%s", db_get("index-page", "/index")); |
| 1499 | } |
| 1500 | |
| @@ -1794,22 +1818,22 @@ | |
| 1794 | } |
| 1795 | |
| 1796 | |
| 1797 | /* Restrictions on the URI for security: |
| 1798 | ** |
| 1799 | ** 1. Reject characters that are not ASCII alphanumerics, |
| 1800 | ** "-", "_", ".", "/", or unicode (above ASCII). |
| 1801 | ** In other words: No ASCII punctuation or control characters |
| 1802 | ** other than "-", "_", "." and "/". |
| 1803 | ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII |
| 1804 | ** alphabetic character at the beginning of the name on windows. |
| 1805 | ** 3. "-" may not occur immediately after "/" |
| 1806 | ** 4. "." may not be adjacent to another "." or to "/" |
| 1807 | ** |
| 1808 | ** Any character does not satisfy these constraints a Not Found |
| 1809 | ** error is returned. |
| 1810 | */ |
| 1811 | szFile = 0; |
| 1812 | for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){ |
| 1813 | char c = zRepo[j]; |
| 1814 | if( c>='a' && c<='z' ) continue; |
| 1815 | if( c>='A' && c<='Z' ) continue; |
| @@ -3510,11 +3534,11 @@ | |
| 3510 | ** "fossil ui --nobrowser" on the remote system and to set up a |
| 3511 | ** tunnel from the local machine to the remote. */ |
| 3512 | FILE *sshIn; |
| 3513 | Blob ssh; |
| 3514 | int bRunning = 0; /* True when fossil starts up on the remote */ |
| 3515 | int isRetry; /* True if on the second attempt */ |
| 3516 | char zLine[1000]; |
| 3517 | |
| 3518 | blob_init(&ssh, 0, 0); |
| 3519 | for(isRetry=0; isRetry<2 && !bRunning; isRetry++){ |
| 3520 | blob_reset(&ssh); |
| @@ -3549,11 +3573,11 @@ | |
| 3549 | if( fCreate ) blob_appendf(&ssh, " --create"); |
| 3550 | blob_appendf(&ssh, " %$", g.argv[2]); |
| 3551 | if( isRetry ){ |
| 3552 | fossil_print("First attempt to run \"fossil\" on %s failed\n" |
| 3553 | "Retry: ", zRemote); |
| 3554 | } |
| 3555 | fossil_print("%s\n", blob_str(&ssh)); |
| 3556 | sshIn = popen(blob_str(&ssh), "r"); |
| 3557 | if( sshIn==0 ){ |
| 3558 | fossil_fatal("unable to %s", blob_str(&ssh)); |
| 3559 | } |
| 3560 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -288,10 +288,27 @@ | |
| 288 | int allowSymlinks; /* Cached "allow-symlinks" option */ |
| 289 | int mainTimerId; /* Set to fossil_timer_start() */ |
| 290 | int nPendingRequest; /* # of HTTP requests in "fossil server" */ |
| 291 | int nRequest; /* Total # of HTTP request */ |
| 292 | int bAvoidDeltaManifests; /* Avoid using delta manifests if true */ |
| 293 | |
| 294 | /* State for communicating specific details between the inbound HTTP |
| 295 | ** header parser (cgi.c), xfer.c, and http.c. */ |
| 296 | struct { |
| 297 | char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */ |
| 298 | int fLoginCardMode; /* If non-0, emit login cards in outbound |
| 299 | ** requests as a HTTP cookie instead of as |
| 300 | ** part of the payload. Gets activated |
| 301 | ** on-demand based on xfer traffic |
| 302 | ** contents. Values, for |
| 303 | ** diagnostic/debugging purposes: 0x01=CLI |
| 304 | ** --flag, 0x02=cgi_setup_query_string(), |
| 305 | ** 0x04=page_xfer(), |
| 306 | ** 0x08=client_sync(). */ |
| 307 | int remoteVersion; /* Remote fossil version. Used for negotiating |
| 308 | ** how to handle the login card. */ |
| 309 | } syncInfo; |
| 310 | #ifdef FOSSIL_ENABLE_JSON |
| 311 | struct FossilJsonBits { |
| 312 | int isJsonMode; /* True if running in JSON mode, else |
| 313 | false. This changes how errors are |
| 314 | reported. In JSON mode we try to |
| @@ -758,10 +775,17 @@ | |
| 775 | g.tcl.argc = g.argc; |
| 776 | g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */ |
| 777 | #endif |
| 778 | g.mainTimerId = fossil_timer_start(); |
| 779 | capture_case_sensitive_option(); |
| 780 | g.syncInfo.fLoginCardMode = |
| 781 | /* The undocumented/unsupported --login-card-header provides a way |
| 782 | ** to force use of the feature added by the xfer-login-card branch |
| 783 | ** in 2025-07, intended for assisting in debugging any related |
| 784 | ** issues. It can be removed once we reach the level of "implicit |
| 785 | ** trust" in that feature. */ |
| 786 | find_option("login-card-header",0,0) ? 0x01 : 0; |
| 787 | g.zVfsName = find_option("vfs",0,1); |
| 788 | if( g.zVfsName==0 ){ |
| 789 | g.zVfsName = fossil_getenv("FOSSIL_VFS"); |
| 790 | } |
| 791 | if( g.zVfsName ){ |
| @@ -1490,11 +1514,11 @@ | |
| 1514 | /* In order for ?skin=... to work when visiting the site from |
| 1515 | ** a typical external link, we have to process it here, as |
| 1516 | ** that parameter gets lost during the redirect. We "could" |
| 1517 | ** pass the whole query string along instead, but that seems |
| 1518 | ** unnecessary. */ |
| 1519 | if(cgi_setup_query_string() & 0x02){ |
| 1520 | cookie_render(); |
| 1521 | } |
| 1522 | cgi_redirectf("%R%s", db_get("index-page", "/index")); |
| 1523 | } |
| 1524 | |
| @@ -1794,22 +1818,22 @@ | |
| 1818 | } |
| 1819 | |
| 1820 | |
| 1821 | /* Restrictions on the URI for security: |
| 1822 | ** |
| 1823 | ** 1. Reject characters that are not ASCII alphanumerics, |
| 1824 | ** "-", "_", ".", "/", or unicode (above ASCII). |
| 1825 | ** In other words: No ASCII punctuation or control characters |
| 1826 | ** other than "-", "_", "." and "/". |
| 1827 | ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII |
| 1828 | ** alphabetic character at the beginning of the name on windows. |
| 1829 | ** 3. "-" may not occur immediately after "/" |
| 1830 | ** 4. "." may not be adjacent to another "." or to "/" |
| 1831 | ** |
| 1832 | ** Any character does not satisfy these constraints a Not Found |
| 1833 | ** error is returned. |
| 1834 | */ |
| 1835 | szFile = 0; |
| 1836 | for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){ |
| 1837 | char c = zRepo[j]; |
| 1838 | if( c>='a' && c<='z' ) continue; |
| 1839 | if( c>='A' && c<='Z' ) continue; |
| @@ -3510,11 +3534,11 @@ | |
| 3534 | ** "fossil ui --nobrowser" on the remote system and to set up a |
| 3535 | ** tunnel from the local machine to the remote. */ |
| 3536 | FILE *sshIn; |
| 3537 | Blob ssh; |
| 3538 | int bRunning = 0; /* True when fossil starts up on the remote */ |
| 3539 | int isRetry; /* True if on the second attempt */ |
| 3540 | char zLine[1000]; |
| 3541 | |
| 3542 | blob_init(&ssh, 0, 0); |
| 3543 | for(isRetry=0; isRetry<2 && !bRunning; isRetry++){ |
| 3544 | blob_reset(&ssh); |
| @@ -3549,11 +3573,11 @@ | |
| 3573 | if( fCreate ) blob_appendf(&ssh, " --create"); |
| 3574 | blob_appendf(&ssh, " %$", g.argv[2]); |
| 3575 | if( isRetry ){ |
| 3576 | fossil_print("First attempt to run \"fossil\" on %s failed\n" |
| 3577 | "Retry: ", zRemote); |
| 3578 | } |
| 3579 | fossil_print("%s\n", blob_str(&ssh)); |
| 3580 | sshIn = popen(blob_str(&ssh), "r"); |
| 3581 | if( sshIn==0 ){ |
| 3582 | fossil_fatal("unable to %s", blob_str(&ssh)); |
| 3583 | } |
| 3584 |
+30
-6
| --- src/main.c | ||
| +++ src/main.c | ||
| @@ -288,10 +288,27 @@ | ||
| 288 | 288 | int allowSymlinks; /* Cached "allow-symlinks" option */ |
| 289 | 289 | int mainTimerId; /* Set to fossil_timer_start() */ |
| 290 | 290 | int nPendingRequest; /* # of HTTP requests in "fossil server" */ |
| 291 | 291 | int nRequest; /* Total # of HTTP request */ |
| 292 | 292 | int bAvoidDeltaManifests; /* Avoid using delta manifests if true */ |
| 293 | + | |
| 294 | + /* State for communicating specific details between the inbound HTTP | |
| 295 | + ** header parser (cgi.c), xfer.c, and http.c. */ | |
| 296 | + struct { | |
| 297 | + char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */ | |
| 298 | + int fLoginCardMode; /* If non-0, emit login cards in outbound | |
| 299 | + ** requests as a HTTP cookie instead of as | |
| 300 | + ** part of the payload. Gets activated | |
| 301 | + ** on-demand based on xfer traffic | |
| 302 | + ** contents. Values, for | |
| 303 | + ** diagnostic/debugging purposes: 0x01=CLI | |
| 304 | + ** --flag, 0x02=cgi_setup_query_string(), | |
| 305 | + ** 0x04=page_xfer(), | |
| 306 | + ** 0x08=client_sync(). */ | |
| 307 | + int remoteVersion; /* Remote fossil version. Used for negotiating | |
| 308 | + ** how to handle the login card. */ | |
| 309 | + } syncInfo; | |
| 293 | 310 | #ifdef FOSSIL_ENABLE_JSON |
| 294 | 311 | struct FossilJsonBits { |
| 295 | 312 | int isJsonMode; /* True if running in JSON mode, else |
| 296 | 313 | false. This changes how errors are |
| 297 | 314 | reported. In JSON mode we try to |
| @@ -758,10 +775,17 @@ | ||
| 758 | 775 | g.tcl.argc = g.argc; |
| 759 | 776 | g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */ |
| 760 | 777 | #endif |
| 761 | 778 | g.mainTimerId = fossil_timer_start(); |
| 762 | 779 | capture_case_sensitive_option(); |
| 780 | + g.syncInfo.fLoginCardMode = | |
| 781 | + /* The undocumented/unsupported --login-card-header provides a way | |
| 782 | + ** to force use of the feature added by the xfer-login-card branch | |
| 783 | + ** in 2025-07, intended for assisting in debugging any related | |
| 784 | + ** issues. It can be removed once we reach the level of "implicit | |
| 785 | + ** trust" in that feature. */ | |
| 786 | + find_option("login-card-header",0,0) ? 0x01 : 0; | |
| 763 | 787 | g.zVfsName = find_option("vfs",0,1); |
| 764 | 788 | if( g.zVfsName==0 ){ |
| 765 | 789 | g.zVfsName = fossil_getenv("FOSSIL_VFS"); |
| 766 | 790 | } |
| 767 | 791 | if( g.zVfsName ){ |
| @@ -1490,11 +1514,11 @@ | ||
| 1490 | 1514 | /* In order for ?skin=... to work when visiting the site from |
| 1491 | 1515 | ** a typical external link, we have to process it here, as |
| 1492 | 1516 | ** that parameter gets lost during the redirect. We "could" |
| 1493 | 1517 | ** pass the whole query string along instead, but that seems |
| 1494 | 1518 | ** unnecessary. */ |
| 1495 | - if(cgi_setup_query_string()>1){ | |
| 1519 | + if(cgi_setup_query_string() & 0x02){ | |
| 1496 | 1520 | cookie_render(); |
| 1497 | 1521 | } |
| 1498 | 1522 | cgi_redirectf("%R%s", db_get("index-page", "/index")); |
| 1499 | 1523 | } |
| 1500 | 1524 | |
| @@ -1794,22 +1818,22 @@ | ||
| 1794 | 1818 | } |
| 1795 | 1819 | |
| 1796 | 1820 | |
| 1797 | 1821 | /* Restrictions on the URI for security: |
| 1798 | 1822 | ** |
| 1799 | - ** 1. Reject characters that are not ASCII alphanumerics, | |
| 1823 | + ** 1. Reject characters that are not ASCII alphanumerics, | |
| 1800 | 1824 | ** "-", "_", ".", "/", or unicode (above ASCII). |
| 1801 | 1825 | ** In other words: No ASCII punctuation or control characters |
| 1802 | 1826 | ** other than "-", "_", "." and "/". |
| 1803 | - ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII | |
| 1827 | + ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII | |
| 1804 | 1828 | ** alphabetic character at the beginning of the name on windows. |
| 1805 | 1829 | ** 3. "-" may not occur immediately after "/" |
| 1806 | 1830 | ** 4. "." may not be adjacent to another "." or to "/" |
| 1807 | 1831 | ** |
| 1808 | 1832 | ** Any character does not satisfy these constraints a Not Found |
| 1809 | 1833 | ** error is returned. |
| 1810 | - */ | |
| 1834 | + */ | |
| 1811 | 1835 | szFile = 0; |
| 1812 | 1836 | for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){ |
| 1813 | 1837 | char c = zRepo[j]; |
| 1814 | 1838 | if( c>='a' && c<='z' ) continue; |
| 1815 | 1839 | if( c>='A' && c<='Z' ) continue; |
| @@ -3510,11 +3534,11 @@ | ||
| 3510 | 3534 | ** "fossil ui --nobrowser" on the remote system and to set up a |
| 3511 | 3535 | ** tunnel from the local machine to the remote. */ |
| 3512 | 3536 | FILE *sshIn; |
| 3513 | 3537 | Blob ssh; |
| 3514 | 3538 | int bRunning = 0; /* True when fossil starts up on the remote */ |
| 3515 | - int isRetry; /* True if on the second attempt */ | |
| 3539 | + int isRetry; /* True if on the second attempt */ | |
| 3516 | 3540 | char zLine[1000]; |
| 3517 | 3541 | |
| 3518 | 3542 | blob_init(&ssh, 0, 0); |
| 3519 | 3543 | for(isRetry=0; isRetry<2 && !bRunning; isRetry++){ |
| 3520 | 3544 | blob_reset(&ssh); |
| @@ -3549,11 +3573,11 @@ | ||
| 3549 | 3573 | if( fCreate ) blob_appendf(&ssh, " --create"); |
| 3550 | 3574 | blob_appendf(&ssh, " %$", g.argv[2]); |
| 3551 | 3575 | if( isRetry ){ |
| 3552 | 3576 | fossil_print("First attempt to run \"fossil\" on %s failed\n" |
| 3553 | 3577 | "Retry: ", zRemote); |
| 3554 | - } | |
| 3578 | + } | |
| 3555 | 3579 | fossil_print("%s\n", blob_str(&ssh)); |
| 3556 | 3580 | sshIn = popen(blob_str(&ssh), "r"); |
| 3557 | 3581 | if( sshIn==0 ){ |
| 3558 | 3582 | fossil_fatal("unable to %s", blob_str(&ssh)); |
| 3559 | 3583 | } |
| 3560 | 3584 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -288,10 +288,27 @@ | |
| 288 | int allowSymlinks; /* Cached "allow-symlinks" option */ |
| 289 | int mainTimerId; /* Set to fossil_timer_start() */ |
| 290 | int nPendingRequest; /* # of HTTP requests in "fossil server" */ |
| 291 | int nRequest; /* Total # of HTTP request */ |
| 292 | int bAvoidDeltaManifests; /* Avoid using delta manifests if true */ |
| 293 | #ifdef FOSSIL_ENABLE_JSON |
| 294 | struct FossilJsonBits { |
| 295 | int isJsonMode; /* True if running in JSON mode, else |
| 296 | false. This changes how errors are |
| 297 | reported. In JSON mode we try to |
| @@ -758,10 +775,17 @@ | |
| 758 | g.tcl.argc = g.argc; |
| 759 | g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */ |
| 760 | #endif |
| 761 | g.mainTimerId = fossil_timer_start(); |
| 762 | capture_case_sensitive_option(); |
| 763 | g.zVfsName = find_option("vfs",0,1); |
| 764 | if( g.zVfsName==0 ){ |
| 765 | g.zVfsName = fossil_getenv("FOSSIL_VFS"); |
| 766 | } |
| 767 | if( g.zVfsName ){ |
| @@ -1490,11 +1514,11 @@ | |
| 1490 | /* In order for ?skin=... to work when visiting the site from |
| 1491 | ** a typical external link, we have to process it here, as |
| 1492 | ** that parameter gets lost during the redirect. We "could" |
| 1493 | ** pass the whole query string along instead, but that seems |
| 1494 | ** unnecessary. */ |
| 1495 | if(cgi_setup_query_string()>1){ |
| 1496 | cookie_render(); |
| 1497 | } |
| 1498 | cgi_redirectf("%R%s", db_get("index-page", "/index")); |
| 1499 | } |
| 1500 | |
| @@ -1794,22 +1818,22 @@ | |
| 1794 | } |
| 1795 | |
| 1796 | |
| 1797 | /* Restrictions on the URI for security: |
| 1798 | ** |
| 1799 | ** 1. Reject characters that are not ASCII alphanumerics, |
| 1800 | ** "-", "_", ".", "/", or unicode (above ASCII). |
| 1801 | ** In other words: No ASCII punctuation or control characters |
| 1802 | ** other than "-", "_", "." and "/". |
| 1803 | ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII |
| 1804 | ** alphabetic character at the beginning of the name on windows. |
| 1805 | ** 3. "-" may not occur immediately after "/" |
| 1806 | ** 4. "." may not be adjacent to another "." or to "/" |
| 1807 | ** |
| 1808 | ** Any character does not satisfy these constraints a Not Found |
| 1809 | ** error is returned. |
| 1810 | */ |
| 1811 | szFile = 0; |
| 1812 | for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){ |
| 1813 | char c = zRepo[j]; |
| 1814 | if( c>='a' && c<='z' ) continue; |
| 1815 | if( c>='A' && c<='Z' ) continue; |
| @@ -3510,11 +3534,11 @@ | |
| 3510 | ** "fossil ui --nobrowser" on the remote system and to set up a |
| 3511 | ** tunnel from the local machine to the remote. */ |
| 3512 | FILE *sshIn; |
| 3513 | Blob ssh; |
| 3514 | int bRunning = 0; /* True when fossil starts up on the remote */ |
| 3515 | int isRetry; /* True if on the second attempt */ |
| 3516 | char zLine[1000]; |
| 3517 | |
| 3518 | blob_init(&ssh, 0, 0); |
| 3519 | for(isRetry=0; isRetry<2 && !bRunning; isRetry++){ |
| 3520 | blob_reset(&ssh); |
| @@ -3549,11 +3573,11 @@ | |
| 3549 | if( fCreate ) blob_appendf(&ssh, " --create"); |
| 3550 | blob_appendf(&ssh, " %$", g.argv[2]); |
| 3551 | if( isRetry ){ |
| 3552 | fossil_print("First attempt to run \"fossil\" on %s failed\n" |
| 3553 | "Retry: ", zRemote); |
| 3554 | } |
| 3555 | fossil_print("%s\n", blob_str(&ssh)); |
| 3556 | sshIn = popen(blob_str(&ssh), "r"); |
| 3557 | if( sshIn==0 ){ |
| 3558 | fossil_fatal("unable to %s", blob_str(&ssh)); |
| 3559 | } |
| 3560 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -288,10 +288,27 @@ | |
| 288 | int allowSymlinks; /* Cached "allow-symlinks" option */ |
| 289 | int mainTimerId; /* Set to fossil_timer_start() */ |
| 290 | int nPendingRequest; /* # of HTTP requests in "fossil server" */ |
| 291 | int nRequest; /* Total # of HTTP request */ |
| 292 | int bAvoidDeltaManifests; /* Avoid using delta manifests if true */ |
| 293 | |
| 294 | /* State for communicating specific details between the inbound HTTP |
| 295 | ** header parser (cgi.c), xfer.c, and http.c. */ |
| 296 | struct { |
| 297 | char *zLoginCard; /* Inbound "x-f-l-c" Cookie header. */ |
| 298 | int fLoginCardMode; /* If non-0, emit login cards in outbound |
| 299 | ** requests as a HTTP cookie instead of as |
| 300 | ** part of the payload. Gets activated |
| 301 | ** on-demand based on xfer traffic |
| 302 | ** contents. Values, for |
| 303 | ** diagnostic/debugging purposes: 0x01=CLI |
| 304 | ** --flag, 0x02=cgi_setup_query_string(), |
| 305 | ** 0x04=page_xfer(), |
| 306 | ** 0x08=client_sync(). */ |
| 307 | int remoteVersion; /* Remote fossil version. Used for negotiating |
| 308 | ** how to handle the login card. */ |
| 309 | } syncInfo; |
| 310 | #ifdef FOSSIL_ENABLE_JSON |
| 311 | struct FossilJsonBits { |
| 312 | int isJsonMode; /* True if running in JSON mode, else |
| 313 | false. This changes how errors are |
| 314 | reported. In JSON mode we try to |
| @@ -758,10 +775,17 @@ | |
| 775 | g.tcl.argc = g.argc; |
| 776 | g.tcl.argv = copy_args(g.argc, g.argv); /* save full arguments */ |
| 777 | #endif |
| 778 | g.mainTimerId = fossil_timer_start(); |
| 779 | capture_case_sensitive_option(); |
| 780 | g.syncInfo.fLoginCardMode = |
| 781 | /* The undocumented/unsupported --login-card-header provides a way |
| 782 | ** to force use of the feature added by the xfer-login-card branch |
| 783 | ** in 2025-07, intended for assisting in debugging any related |
| 784 | ** issues. It can be removed once we reach the level of "implicit |
| 785 | ** trust" in that feature. */ |
| 786 | find_option("login-card-header",0,0) ? 0x01 : 0; |
| 787 | g.zVfsName = find_option("vfs",0,1); |
| 788 | if( g.zVfsName==0 ){ |
| 789 | g.zVfsName = fossil_getenv("FOSSIL_VFS"); |
| 790 | } |
| 791 | if( g.zVfsName ){ |
| @@ -1490,11 +1514,11 @@ | |
| 1514 | /* In order for ?skin=... to work when visiting the site from |
| 1515 | ** a typical external link, we have to process it here, as |
| 1516 | ** that parameter gets lost during the redirect. We "could" |
| 1517 | ** pass the whole query string along instead, but that seems |
| 1518 | ** unnecessary. */ |
| 1519 | if(cgi_setup_query_string() & 0x02){ |
| 1520 | cookie_render(); |
| 1521 | } |
| 1522 | cgi_redirectf("%R%s", db_get("index-page", "/index")); |
| 1523 | } |
| 1524 | |
| @@ -1794,22 +1818,22 @@ | |
| 1818 | } |
| 1819 | |
| 1820 | |
| 1821 | /* Restrictions on the URI for security: |
| 1822 | ** |
| 1823 | ** 1. Reject characters that are not ASCII alphanumerics, |
| 1824 | ** "-", "_", ".", "/", or unicode (above ASCII). |
| 1825 | ** In other words: No ASCII punctuation or control characters |
| 1826 | ** other than "-", "_", "." and "/". |
| 1827 | ** 2. Exception to rule 1: Allow /X:/ where X is any ASCII |
| 1828 | ** alphabetic character at the beginning of the name on windows. |
| 1829 | ** 3. "-" may not occur immediately after "/" |
| 1830 | ** 4. "." may not be adjacent to another "." or to "/" |
| 1831 | ** |
| 1832 | ** Any character does not satisfy these constraints a Not Found |
| 1833 | ** error is returned. |
| 1834 | */ |
| 1835 | szFile = 0; |
| 1836 | for(j=nBase+1, k=0; zRepo[j] && k<i-1; j++, k++){ |
| 1837 | char c = zRepo[j]; |
| 1838 | if( c>='a' && c<='z' ) continue; |
| 1839 | if( c>='A' && c<='Z' ) continue; |
| @@ -3510,11 +3534,11 @@ | |
| 3534 | ** "fossil ui --nobrowser" on the remote system and to set up a |
| 3535 | ** tunnel from the local machine to the remote. */ |
| 3536 | FILE *sshIn; |
| 3537 | Blob ssh; |
| 3538 | int bRunning = 0; /* True when fossil starts up on the remote */ |
| 3539 | int isRetry; /* True if on the second attempt */ |
| 3540 | char zLine[1000]; |
| 3541 | |
| 3542 | blob_init(&ssh, 0, 0); |
| 3543 | for(isRetry=0; isRetry<2 && !bRunning; isRetry++){ |
| 3544 | blob_reset(&ssh); |
| @@ -3549,11 +3573,11 @@ | |
| 3573 | if( fCreate ) blob_appendf(&ssh, " --create"); |
| 3574 | blob_appendf(&ssh, " %$", g.argv[2]); |
| 3575 | if( isRetry ){ |
| 3576 | fossil_print("First attempt to run \"fossil\" on %s failed\n" |
| 3577 | "Retry: ", zRemote); |
| 3578 | } |
| 3579 | fossil_print("%s\n", blob_str(&ssh)); |
| 3580 | sshIn = popen(blob_str(&ssh), "r"); |
| 3581 | if( sshIn==0 ){ |
| 3582 | fossil_fatal("unable to %s", blob_str(&ssh)); |
| 3583 | } |
| 3584 |
+1
-1
| --- src/url.c | ||
| +++ src/url.c | ||
| @@ -233,11 +233,11 @@ | ||
| 233 | 233 | for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} |
| 234 | 234 | if( pUrlData->path[i] ){ |
| 235 | 235 | pUrlData->path[i] = 0; |
| 236 | 236 | i++; |
| 237 | 237 | } |
| 238 | - zExe = mprintf(""); | |
| 238 | + zExe = fossil_strdup(""); | |
| 239 | 239 | while( pUrlData->path[i]!=0 ){ |
| 240 | 240 | char *zName, *zValue; |
| 241 | 241 | zName = &pUrlData->path[i]; |
| 242 | 242 | zValue = zName; |
| 243 | 243 | while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } |
| 244 | 244 |
| --- src/url.c | |
| +++ src/url.c | |
| @@ -233,11 +233,11 @@ | |
| 233 | for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} |
| 234 | if( pUrlData->path[i] ){ |
| 235 | pUrlData->path[i] = 0; |
| 236 | i++; |
| 237 | } |
| 238 | zExe = mprintf(""); |
| 239 | while( pUrlData->path[i]!=0 ){ |
| 240 | char *zName, *zValue; |
| 241 | zName = &pUrlData->path[i]; |
| 242 | zValue = zName; |
| 243 | while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } |
| 244 |
| --- src/url.c | |
| +++ src/url.c | |
| @@ -233,11 +233,11 @@ | |
| 233 | for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} |
| 234 | if( pUrlData->path[i] ){ |
| 235 | pUrlData->path[i] = 0; |
| 236 | i++; |
| 237 | } |
| 238 | zExe = fossil_strdup(""); |
| 239 | while( pUrlData->path[i]!=0 ){ |
| 240 | char *zName, *zValue; |
| 241 | zName = &pUrlData->path[i]; |
| 242 | zValue = zName; |
| 243 | while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } |
| 244 |
+1
-1
| --- src/url.c | ||
| +++ src/url.c | ||
| @@ -233,11 +233,11 @@ | ||
| 233 | 233 | for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} |
| 234 | 234 | if( pUrlData->path[i] ){ |
| 235 | 235 | pUrlData->path[i] = 0; |
| 236 | 236 | i++; |
| 237 | 237 | } |
| 238 | - zExe = mprintf(""); | |
| 238 | + zExe = fossil_strdup(""); | |
| 239 | 239 | while( pUrlData->path[i]!=0 ){ |
| 240 | 240 | char *zName, *zValue; |
| 241 | 241 | zName = &pUrlData->path[i]; |
| 242 | 242 | zValue = zName; |
| 243 | 243 | while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } |
| 244 | 244 |
| --- src/url.c | |
| +++ src/url.c | |
| @@ -233,11 +233,11 @@ | |
| 233 | for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} |
| 234 | if( pUrlData->path[i] ){ |
| 235 | pUrlData->path[i] = 0; |
| 236 | i++; |
| 237 | } |
| 238 | zExe = mprintf(""); |
| 239 | while( pUrlData->path[i]!=0 ){ |
| 240 | char *zName, *zValue; |
| 241 | zName = &pUrlData->path[i]; |
| 242 | zValue = zName; |
| 243 | while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } |
| 244 |
| --- src/url.c | |
| +++ src/url.c | |
| @@ -233,11 +233,11 @@ | |
| 233 | for(i=0; pUrlData->path[i] && pUrlData->path[i]!='?'; i++){} |
| 234 | if( pUrlData->path[i] ){ |
| 235 | pUrlData->path[i] = 0; |
| 236 | i++; |
| 237 | } |
| 238 | zExe = fossil_strdup(""); |
| 239 | while( pUrlData->path[i]!=0 ){ |
| 240 | char *zName, *zValue; |
| 241 | zName = &pUrlData->path[i]; |
| 242 | zValue = zName; |
| 243 | while( pUrlData->path[i] && pUrlData->path[i]!='=' ){ i++; } |
| 244 |
+1
-1
| --- src/user.c | ||
| +++ src/user.c | ||
| @@ -465,11 +465,11 @@ | ||
| 465 | 465 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); |
| 466 | 466 | db_unprotect(PROTECT_USER); |
| 467 | 467 | db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", |
| 468 | 468 | zSecret, uid); |
| 469 | 469 | db_protect_pop(); |
| 470 | - free(zSecret); | |
| 470 | + fossil_free(zSecret); | |
| 471 | 471 | } |
| 472 | 472 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 473 | 473 | int uid; |
| 474 | 474 | if( g.argc!=4 && g.argc!=5 ){ |
| 475 | 475 | usage("capabilities USERNAME ?PERMISSIONS?"); |
| 476 | 476 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -465,11 +465,11 @@ | |
| 465 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); |
| 466 | db_unprotect(PROTECT_USER); |
| 467 | db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", |
| 468 | zSecret, uid); |
| 469 | db_protect_pop(); |
| 470 | free(zSecret); |
| 471 | } |
| 472 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 473 | int uid; |
| 474 | if( g.argc!=4 && g.argc!=5 ){ |
| 475 | usage("capabilities USERNAME ?PERMISSIONS?"); |
| 476 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -465,11 +465,11 @@ | |
| 465 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); |
| 466 | db_unprotect(PROTECT_USER); |
| 467 | db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", |
| 468 | zSecret, uid); |
| 469 | db_protect_pop(); |
| 470 | fossil_free(zSecret); |
| 471 | } |
| 472 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 473 | int uid; |
| 474 | if( g.argc!=4 && g.argc!=5 ){ |
| 475 | usage("capabilities USERNAME ?PERMISSIONS?"); |
| 476 |
+1
-1
| --- src/user.c | ||
| +++ src/user.c | ||
| @@ -465,11 +465,11 @@ | ||
| 465 | 465 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); |
| 466 | 466 | db_unprotect(PROTECT_USER); |
| 467 | 467 | db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", |
| 468 | 468 | zSecret, uid); |
| 469 | 469 | db_protect_pop(); |
| 470 | - free(zSecret); | |
| 470 | + fossil_free(zSecret); | |
| 471 | 471 | } |
| 472 | 472 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 473 | 473 | int uid; |
| 474 | 474 | if( g.argc!=4 && g.argc!=5 ){ |
| 475 | 475 | usage("capabilities USERNAME ?PERMISSIONS?"); |
| 476 | 476 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -465,11 +465,11 @@ | |
| 465 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); |
| 466 | db_unprotect(PROTECT_USER); |
| 467 | db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", |
| 468 | zSecret, uid); |
| 469 | db_protect_pop(); |
| 470 | free(zSecret); |
| 471 | } |
| 472 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 473 | int uid; |
| 474 | if( g.argc!=4 && g.argc!=5 ){ |
| 475 | usage("capabilities USERNAME ?PERMISSIONS?"); |
| 476 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -465,11 +465,11 @@ | |
| 465 | char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); |
| 466 | db_unprotect(PROTECT_USER); |
| 467 | db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", |
| 468 | zSecret, uid); |
| 469 | db_protect_pop(); |
| 470 | fossil_free(zSecret); |
| 471 | } |
| 472 | }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ |
| 473 | int uid; |
| 474 | if( g.argc!=4 && g.argc!=5 ){ |
| 475 | usage("capabilities USERNAME ?PERMISSIONS?"); |
| 476 |
+55
-13
| --- src/xfer.c | ||
| +++ src/xfer.c | ||
| @@ -827,11 +827,11 @@ | ||
| 827 | 827 | int rc = -1; |
| 828 | 828 | char *zLogin = blob_terminate(pLogin); |
| 829 | 829 | defossilize(zLogin); |
| 830 | 830 | |
| 831 | 831 | if( fossil_strcmp(zLogin, "nobody")==0 |
| 832 | - || fossil_strcmp(zLogin,"anonymous")==0 | |
| 832 | + || fossil_strcmp(zLogin, "anonymous")==0 | |
| 833 | 833 | ){ |
| 834 | 834 | return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
| 835 | 835 | } |
| 836 | 836 | if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
| 837 | 837 | && db_get_boolean("remote_user_ok",0) ){ |
| @@ -866,11 +866,11 @@ | ||
| 866 | 866 | const char *zPw = db_column_text(&q, 0); |
| 867 | 867 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
| 868 | 868 | blob_zero(&combined); |
| 869 | 869 | blob_copy(&combined, pNonce); |
| 870 | 870 | blob_append(&combined, zSecret, -1); |
| 871 | - free(zSecret); | |
| 871 | + fossil_free(zSecret); | |
| 872 | 872 | sha1sum_blob(&combined, &hash); |
| 873 | 873 | rc = blob_constant_time_cmp(&hash, pSig); |
| 874 | 874 | blob_reset(&hash); |
| 875 | 875 | blob_reset(&combined); |
| 876 | 876 | } |
| @@ -1241,10 +1241,25 @@ | ||
| 1241 | 1241 | /* |
| 1242 | 1242 | ** If this variable is set, disable login checks. Used for debugging |
| 1243 | 1243 | ** only. |
| 1244 | 1244 | */ |
| 1245 | 1245 | static int disableLogin = 0; |
| 1246 | + | |
| 1247 | +/* | |
| 1248 | +** Must be passed the version info from pragmas | |
| 1249 | +** client-version/server-version cards. If the version info is "new | |
| 1250 | +** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login | |
| 1251 | +** card flag, else this is a no-op. | |
| 1252 | +*/ | |
| 1253 | +static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime, | |
| 1254 | + int fLoginCardMode){ | |
| 1255 | + if( iRemoteVersion>=22700 | |
| 1256 | + && (iDate > 20250727 | |
| 1257 | + || (iDate == 20250727 && iTime >= 110500)) ){ | |
| 1258 | + g.syncInfo.fLoginCardMode |= fLoginCardMode; | |
| 1259 | + } | |
| 1260 | +} | |
| 1246 | 1261 | |
| 1247 | 1262 | /* |
| 1248 | 1263 | ** The CGI/HTTP preprocessor always redirects requests with a content-type |
| 1249 | 1264 | ** of application/x-fossil or application/x-fossil-debug to this page, |
| 1250 | 1265 | ** regardless of what path was specified in the HTTP header. This allows |
| @@ -1273,10 +1288,11 @@ | ||
| 1273 | 1288 | int nUuidList = 0; |
| 1274 | 1289 | char **pzUuidList = 0; |
| 1275 | 1290 | int *pnUuidList = 0; |
| 1276 | 1291 | int uvCatalogSent = 0; |
| 1277 | 1292 | int bSendLinks = 0; |
| 1293 | + int nLogin = 0; | |
| 1278 | 1294 | |
| 1279 | 1295 | if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
| 1280 | 1296 | fossil_redirect_home(); |
| 1281 | 1297 | } |
| 1282 | 1298 | g.zLogin = "anonymous"; |
| @@ -1314,10 +1330,24 @@ | ||
| 1314 | 1330 | } |
| 1315 | 1331 | zScript = xfer_push_code(); |
| 1316 | 1332 | if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
| 1317 | 1333 | pzUuidList = &zUuidList; |
| 1318 | 1334 | pnUuidList = &nUuidList; |
| 1335 | + } | |
| 1336 | + if( g.syncInfo.zLoginCard ){ | |
| 1337 | + /* Login card received via HTTP Cookie header */ | |
| 1338 | + assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" ); | |
| 1339 | + blob_zero(&xfer.line); | |
| 1340 | + blob_append(&xfer.line, g.syncInfo.zLoginCard, -1); | |
| 1341 | + xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, | |
| 1342 | + count(xfer.aToken)); | |
| 1343 | + fossil_free( g.syncInfo.zLoginCard ); | |
| 1344 | + g.syncInfo.zLoginCard = 0; | |
| 1345 | + if( xfer.nToken==4 | |
| 1346 | + && blob_eq(&xfer.aToken[0], "login") ){ | |
| 1347 | + goto handle_login_card; | |
| 1348 | + } | |
| 1319 | 1349 | } |
| 1320 | 1350 | while( blob_line(xfer.pIn, &xfer.line) ){ |
| 1321 | 1351 | if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
| 1322 | 1352 | if( blob_size(&xfer.line)==0 ) continue; |
| 1323 | 1353 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
| @@ -1550,17 +1580,28 @@ | ||
| 1550 | 1580 | |
| 1551 | 1581 | /* login USER NONCE SIGNATURE |
| 1552 | 1582 | ** |
| 1553 | 1583 | ** The client has sent login credentials to the server. |
| 1554 | 1584 | ** Validate the login. This has to happen before anything else. |
| 1555 | - ** The client can send multiple logins. Permissions are cumulative. | |
| 1585 | + ** | |
| 1586 | + ** For many years, Fossil would accept multiple login cards with | |
| 1587 | + ** cumulative permissions. But that feature was never used. Hence | |
| 1588 | + ** it is now prohibited. Any login card after the first generates | |
| 1589 | + ** a fatal error. | |
| 1556 | 1590 | */ |
| 1557 | 1591 | if( blob_eq(&xfer.aToken[0], "login") |
| 1558 | 1592 | && xfer.nToken==4 |
| 1559 | 1593 | ){ |
| 1594 | + handle_login_card: | |
| 1595 | + nLogin++; | |
| 1560 | 1596 | if( disableLogin ){ |
| 1561 | 1597 | g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
| 1598 | + }else if( nLogin > 1 ){ | |
| 1599 | + cgi_reset_content(); | |
| 1600 | + @ error multiple\slogin\cards | |
| 1601 | + nErr++; | |
| 1602 | + break; | |
| 1562 | 1603 | }else{ |
| 1563 | 1604 | if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
| 1564 | 1605 | || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
| 1565 | 1606 | ){ |
| 1566 | 1607 | cgi_reset_content(); |
| @@ -1651,11 +1692,10 @@ | ||
| 1651 | 1692 | }else{ |
| 1652 | 1693 | xfer.nextIsPrivate = 1; |
| 1653 | 1694 | } |
| 1654 | 1695 | }else |
| 1655 | 1696 | |
| 1656 | - | |
| 1657 | 1697 | /* pragma NAME VALUE... |
| 1658 | 1698 | ** |
| 1659 | 1699 | ** The client issues pragmas to try to influence the behavior of the |
| 1660 | 1700 | ** server. These are requests only. Unknown pragmas are silently |
| 1661 | 1701 | ** ignored. |
| @@ -1694,17 +1734,20 @@ | ||
| 1694 | 1734 | ** The client announces to the server what version of Fossil it |
| 1695 | 1735 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 1696 | 1736 | ** for the specific check-in of the client. |
| 1697 | 1737 | */ |
| 1698 | 1738 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
| 1699 | - xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); | |
| 1739 | + xfer.remoteVersion = g.syncInfo.remoteVersion = | |
| 1740 | + atoi(blob_str(&xfer.aToken[2])); | |
| 1700 | 1741 | if( xfer.nToken>=5 ){ |
| 1701 | 1742 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 1702 | 1743 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 1703 | 1744 | @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
| 1704 | 1745 | @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
| 1705 | 1746 | } |
| 1747 | + xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, | |
| 1748 | + xfer.remoteTime, 0x04 ); | |
| 1706 | 1749 | }else |
| 1707 | 1750 | |
| 1708 | 1751 | /* pragma uv-hash HASH |
| 1709 | 1752 | ** |
| 1710 | 1753 | ** The client wants to make sure that unversioned files are all synced. |
| @@ -2341,18 +2384,17 @@ | ||
| 2341 | 2384 | blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
| 2342 | 2385 | zCkinLock = 0; |
| 2343 | 2386 | }else if( zClientId ){ |
| 2344 | 2387 | blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
| 2345 | 2388 | } |
| 2346 | - | |
| 2347 | 2389 | /* Append randomness to the end of the uplink message. This makes all |
| 2348 | 2390 | ** messages unique so that that the login-card nonce will always |
| 2349 | 2391 | ** be unique. |
| 2350 | 2392 | */ |
| 2351 | 2393 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 2352 | 2394 | blob_appendf(&send, "# %s\n", zRandomness); |
| 2353 | - free(zRandomness); | |
| 2395 | + fossil_free(zRandomness); | |
| 2354 | 2396 | |
| 2355 | 2397 | if( (syncFlags & SYNC_VERBOSE)!=0 |
| 2356 | 2398 | && (syncFlags & SYNC_XVERBOSE)==0 |
| 2357 | 2399 | ){ |
| 2358 | 2400 | fossil_print("waiting for server..."); |
| @@ -2725,13 +2767,10 @@ | ||
| 2725 | 2767 | |
| 2726 | 2768 | /* message MESSAGE |
| 2727 | 2769 | ** |
| 2728 | 2770 | ** A message is received from the server. Print it. |
| 2729 | 2771 | ** Similar to "error" but does not stop processing. |
| 2730 | - ** | |
| 2731 | - ** If the "login failed" message is seen, clear the sync password prior | |
| 2732 | - ** to the next cycle. | |
| 2733 | 2772 | */ |
| 2734 | 2773 | if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
| 2735 | 2774 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 2736 | 2775 | defossilize(zMsg); |
| 2737 | 2776 | if( (syncFlags & SYNC_PUSH) && zMsg |
| @@ -2757,15 +2796,18 @@ | ||
| 2757 | 2796 | ** The server announces to the server what version of Fossil it |
| 2758 | 2797 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 2759 | 2798 | ** for the specific check-in of the client. |
| 2760 | 2799 | */ |
| 2761 | 2800 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
| 2762 | - xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); | |
| 2801 | + xfer.remoteVersion = g.syncInfo.remoteVersion = | |
| 2802 | + atoi(blob_str(&xfer.aToken[2])); | |
| 2763 | 2803 | if( xfer.nToken>=5 ){ |
| 2764 | 2804 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 2765 | 2805 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 2766 | 2806 | } |
| 2807 | + xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, | |
| 2808 | + xfer.remoteTime, 0x08 ); | |
| 2767 | 2809 | } |
| 2768 | 2810 | |
| 2769 | 2811 | /* pragma uv-pull-only |
| 2770 | 2812 | ** pragma uv-push-ok |
| 2771 | 2813 | ** |
| @@ -2896,11 +2938,11 @@ | ||
| 2896 | 2938 | &recv |
| 2897 | 2939 | ); |
| 2898 | 2940 | nErr++; |
| 2899 | 2941 | break; |
| 2900 | 2942 | } |
| 2901 | - blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]); | |
| 2943 | + blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line); | |
| 2902 | 2944 | } |
| 2903 | 2945 | |
| 2904 | 2946 | if( blob_size(&xfer.err) ){ |
| 2905 | 2947 | fossil_force_newline(); |
| 2906 | 2948 | fossil_warning("%b", &xfer.err); |
| @@ -2963,11 +3005,11 @@ | ||
| 2963 | 3005 | }else{ |
| 2964 | 3006 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2965 | 3007 | content_enable_dephantomize(1); |
| 2966 | 3008 | } |
| 2967 | 3009 | db_end_transaction(0); |
| 2968 | - }; | |
| 3010 | + }; /* while(go) */ | |
| 2969 | 3011 | transport_stats(&nSent, &nRcvd, 1); |
| 2970 | 3012 | if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
| 2971 | 3013 | if( (rSkew*24.0*3600.0) > 10.0 ){ |
| 2972 | 3014 | fossil_warning("*** time skew *** server is fast by %s", |
| 2973 | 3015 | db_timespan_name(rSkew)); |
| 2974 | 3016 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -827,11 +827,11 @@ | |
| 827 | int rc = -1; |
| 828 | char *zLogin = blob_terminate(pLogin); |
| 829 | defossilize(zLogin); |
| 830 | |
| 831 | if( fossil_strcmp(zLogin, "nobody")==0 |
| 832 | || fossil_strcmp(zLogin,"anonymous")==0 |
| 833 | ){ |
| 834 | return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
| 835 | } |
| 836 | if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
| 837 | && db_get_boolean("remote_user_ok",0) ){ |
| @@ -866,11 +866,11 @@ | |
| 866 | const char *zPw = db_column_text(&q, 0); |
| 867 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
| 868 | blob_zero(&combined); |
| 869 | blob_copy(&combined, pNonce); |
| 870 | blob_append(&combined, zSecret, -1); |
| 871 | free(zSecret); |
| 872 | sha1sum_blob(&combined, &hash); |
| 873 | rc = blob_constant_time_cmp(&hash, pSig); |
| 874 | blob_reset(&hash); |
| 875 | blob_reset(&combined); |
| 876 | } |
| @@ -1241,10 +1241,25 @@ | |
| 1241 | /* |
| 1242 | ** If this variable is set, disable login checks. Used for debugging |
| 1243 | ** only. |
| 1244 | */ |
| 1245 | static int disableLogin = 0; |
| 1246 | |
| 1247 | /* |
| 1248 | ** The CGI/HTTP preprocessor always redirects requests with a content-type |
| 1249 | ** of application/x-fossil or application/x-fossil-debug to this page, |
| 1250 | ** regardless of what path was specified in the HTTP header. This allows |
| @@ -1273,10 +1288,11 @@ | |
| 1273 | int nUuidList = 0; |
| 1274 | char **pzUuidList = 0; |
| 1275 | int *pnUuidList = 0; |
| 1276 | int uvCatalogSent = 0; |
| 1277 | int bSendLinks = 0; |
| 1278 | |
| 1279 | if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
| 1280 | fossil_redirect_home(); |
| 1281 | } |
| 1282 | g.zLogin = "anonymous"; |
| @@ -1314,10 +1330,24 @@ | |
| 1314 | } |
| 1315 | zScript = xfer_push_code(); |
| 1316 | if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
| 1317 | pzUuidList = &zUuidList; |
| 1318 | pnUuidList = &nUuidList; |
| 1319 | } |
| 1320 | while( blob_line(xfer.pIn, &xfer.line) ){ |
| 1321 | if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
| 1322 | if( blob_size(&xfer.line)==0 ) continue; |
| 1323 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
| @@ -1550,17 +1580,28 @@ | |
| 1550 | |
| 1551 | /* login USER NONCE SIGNATURE |
| 1552 | ** |
| 1553 | ** The client has sent login credentials to the server. |
| 1554 | ** Validate the login. This has to happen before anything else. |
| 1555 | ** The client can send multiple logins. Permissions are cumulative. |
| 1556 | */ |
| 1557 | if( blob_eq(&xfer.aToken[0], "login") |
| 1558 | && xfer.nToken==4 |
| 1559 | ){ |
| 1560 | if( disableLogin ){ |
| 1561 | g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
| 1562 | }else{ |
| 1563 | if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
| 1564 | || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
| 1565 | ){ |
| 1566 | cgi_reset_content(); |
| @@ -1651,11 +1692,10 @@ | |
| 1651 | }else{ |
| 1652 | xfer.nextIsPrivate = 1; |
| 1653 | } |
| 1654 | }else |
| 1655 | |
| 1656 | |
| 1657 | /* pragma NAME VALUE... |
| 1658 | ** |
| 1659 | ** The client issues pragmas to try to influence the behavior of the |
| 1660 | ** server. These are requests only. Unknown pragmas are silently |
| 1661 | ** ignored. |
| @@ -1694,17 +1734,20 @@ | |
| 1694 | ** The client announces to the server what version of Fossil it |
| 1695 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 1696 | ** for the specific check-in of the client. |
| 1697 | */ |
| 1698 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
| 1699 | xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); |
| 1700 | if( xfer.nToken>=5 ){ |
| 1701 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 1702 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 1703 | @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
| 1704 | @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
| 1705 | } |
| 1706 | }else |
| 1707 | |
| 1708 | /* pragma uv-hash HASH |
| 1709 | ** |
| 1710 | ** The client wants to make sure that unversioned files are all synced. |
| @@ -2341,18 +2384,17 @@ | |
| 2341 | blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
| 2342 | zCkinLock = 0; |
| 2343 | }else if( zClientId ){ |
| 2344 | blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
| 2345 | } |
| 2346 | |
| 2347 | /* Append randomness to the end of the uplink message. This makes all |
| 2348 | ** messages unique so that that the login-card nonce will always |
| 2349 | ** be unique. |
| 2350 | */ |
| 2351 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 2352 | blob_appendf(&send, "# %s\n", zRandomness); |
| 2353 | free(zRandomness); |
| 2354 | |
| 2355 | if( (syncFlags & SYNC_VERBOSE)!=0 |
| 2356 | && (syncFlags & SYNC_XVERBOSE)==0 |
| 2357 | ){ |
| 2358 | fossil_print("waiting for server..."); |
| @@ -2725,13 +2767,10 @@ | |
| 2725 | |
| 2726 | /* message MESSAGE |
| 2727 | ** |
| 2728 | ** A message is received from the server. Print it. |
| 2729 | ** Similar to "error" but does not stop processing. |
| 2730 | ** |
| 2731 | ** If the "login failed" message is seen, clear the sync password prior |
| 2732 | ** to the next cycle. |
| 2733 | */ |
| 2734 | if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
| 2735 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 2736 | defossilize(zMsg); |
| 2737 | if( (syncFlags & SYNC_PUSH) && zMsg |
| @@ -2757,15 +2796,18 @@ | |
| 2757 | ** The server announces to the server what version of Fossil it |
| 2758 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 2759 | ** for the specific check-in of the client. |
| 2760 | */ |
| 2761 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
| 2762 | xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); |
| 2763 | if( xfer.nToken>=5 ){ |
| 2764 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 2765 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 2766 | } |
| 2767 | } |
| 2768 | |
| 2769 | /* pragma uv-pull-only |
| 2770 | ** pragma uv-push-ok |
| 2771 | ** |
| @@ -2896,11 +2938,11 @@ | |
| 2896 | &recv |
| 2897 | ); |
| 2898 | nErr++; |
| 2899 | break; |
| 2900 | } |
| 2901 | blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]); |
| 2902 | } |
| 2903 | |
| 2904 | if( blob_size(&xfer.err) ){ |
| 2905 | fossil_force_newline(); |
| 2906 | fossil_warning("%b", &xfer.err); |
| @@ -2963,11 +3005,11 @@ | |
| 2963 | }else{ |
| 2964 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2965 | content_enable_dephantomize(1); |
| 2966 | } |
| 2967 | db_end_transaction(0); |
| 2968 | }; |
| 2969 | transport_stats(&nSent, &nRcvd, 1); |
| 2970 | if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
| 2971 | if( (rSkew*24.0*3600.0) > 10.0 ){ |
| 2972 | fossil_warning("*** time skew *** server is fast by %s", |
| 2973 | db_timespan_name(rSkew)); |
| 2974 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -827,11 +827,11 @@ | |
| 827 | int rc = -1; |
| 828 | char *zLogin = blob_terminate(pLogin); |
| 829 | defossilize(zLogin); |
| 830 | |
| 831 | if( fossil_strcmp(zLogin, "nobody")==0 |
| 832 | || fossil_strcmp(zLogin, "anonymous")==0 |
| 833 | ){ |
| 834 | return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
| 835 | } |
| 836 | if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
| 837 | && db_get_boolean("remote_user_ok",0) ){ |
| @@ -866,11 +866,11 @@ | |
| 866 | const char *zPw = db_column_text(&q, 0); |
| 867 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
| 868 | blob_zero(&combined); |
| 869 | blob_copy(&combined, pNonce); |
| 870 | blob_append(&combined, zSecret, -1); |
| 871 | fossil_free(zSecret); |
| 872 | sha1sum_blob(&combined, &hash); |
| 873 | rc = blob_constant_time_cmp(&hash, pSig); |
| 874 | blob_reset(&hash); |
| 875 | blob_reset(&combined); |
| 876 | } |
| @@ -1241,10 +1241,25 @@ | |
| 1241 | /* |
| 1242 | ** If this variable is set, disable login checks. Used for debugging |
| 1243 | ** only. |
| 1244 | */ |
| 1245 | static int disableLogin = 0; |
| 1246 | |
| 1247 | /* |
| 1248 | ** Must be passed the version info from pragmas |
| 1249 | ** client-version/server-version cards. If the version info is "new |
| 1250 | ** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login |
| 1251 | ** card flag, else this is a no-op. |
| 1252 | */ |
| 1253 | static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime, |
| 1254 | int fLoginCardMode){ |
| 1255 | if( iRemoteVersion>=22700 |
| 1256 | && (iDate > 20250727 |
| 1257 | || (iDate == 20250727 && iTime >= 110500)) ){ |
| 1258 | g.syncInfo.fLoginCardMode |= fLoginCardMode; |
| 1259 | } |
| 1260 | } |
| 1261 | |
| 1262 | /* |
| 1263 | ** The CGI/HTTP preprocessor always redirects requests with a content-type |
| 1264 | ** of application/x-fossil or application/x-fossil-debug to this page, |
| 1265 | ** regardless of what path was specified in the HTTP header. This allows |
| @@ -1273,10 +1288,11 @@ | |
| 1288 | int nUuidList = 0; |
| 1289 | char **pzUuidList = 0; |
| 1290 | int *pnUuidList = 0; |
| 1291 | int uvCatalogSent = 0; |
| 1292 | int bSendLinks = 0; |
| 1293 | int nLogin = 0; |
| 1294 | |
| 1295 | if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
| 1296 | fossil_redirect_home(); |
| 1297 | } |
| 1298 | g.zLogin = "anonymous"; |
| @@ -1314,10 +1330,24 @@ | |
| 1330 | } |
| 1331 | zScript = xfer_push_code(); |
| 1332 | if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
| 1333 | pzUuidList = &zUuidList; |
| 1334 | pnUuidList = &nUuidList; |
| 1335 | } |
| 1336 | if( g.syncInfo.zLoginCard ){ |
| 1337 | /* Login card received via HTTP Cookie header */ |
| 1338 | assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" ); |
| 1339 | blob_zero(&xfer.line); |
| 1340 | blob_append(&xfer.line, g.syncInfo.zLoginCard, -1); |
| 1341 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, |
| 1342 | count(xfer.aToken)); |
| 1343 | fossil_free( g.syncInfo.zLoginCard ); |
| 1344 | g.syncInfo.zLoginCard = 0; |
| 1345 | if( xfer.nToken==4 |
| 1346 | && blob_eq(&xfer.aToken[0], "login") ){ |
| 1347 | goto handle_login_card; |
| 1348 | } |
| 1349 | } |
| 1350 | while( blob_line(xfer.pIn, &xfer.line) ){ |
| 1351 | if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
| 1352 | if( blob_size(&xfer.line)==0 ) continue; |
| 1353 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
| @@ -1550,17 +1580,28 @@ | |
| 1580 | |
| 1581 | /* login USER NONCE SIGNATURE |
| 1582 | ** |
| 1583 | ** The client has sent login credentials to the server. |
| 1584 | ** Validate the login. This has to happen before anything else. |
| 1585 | ** |
| 1586 | ** For many years, Fossil would accept multiple login cards with |
| 1587 | ** cumulative permissions. But that feature was never used. Hence |
| 1588 | ** it is now prohibited. Any login card after the first generates |
| 1589 | ** a fatal error. |
| 1590 | */ |
| 1591 | if( blob_eq(&xfer.aToken[0], "login") |
| 1592 | && xfer.nToken==4 |
| 1593 | ){ |
| 1594 | handle_login_card: |
| 1595 | nLogin++; |
| 1596 | if( disableLogin ){ |
| 1597 | g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
| 1598 | }else if( nLogin > 1 ){ |
| 1599 | cgi_reset_content(); |
| 1600 | @ error multiple\slogin\cards |
| 1601 | nErr++; |
| 1602 | break; |
| 1603 | }else{ |
| 1604 | if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
| 1605 | || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
| 1606 | ){ |
| 1607 | cgi_reset_content(); |
| @@ -1651,11 +1692,10 @@ | |
| 1692 | }else{ |
| 1693 | xfer.nextIsPrivate = 1; |
| 1694 | } |
| 1695 | }else |
| 1696 | |
| 1697 | /* pragma NAME VALUE... |
| 1698 | ** |
| 1699 | ** The client issues pragmas to try to influence the behavior of the |
| 1700 | ** server. These are requests only. Unknown pragmas are silently |
| 1701 | ** ignored. |
| @@ -1694,17 +1734,20 @@ | |
| 1734 | ** The client announces to the server what version of Fossil it |
| 1735 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 1736 | ** for the specific check-in of the client. |
| 1737 | */ |
| 1738 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
| 1739 | xfer.remoteVersion = g.syncInfo.remoteVersion = |
| 1740 | atoi(blob_str(&xfer.aToken[2])); |
| 1741 | if( xfer.nToken>=5 ){ |
| 1742 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 1743 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 1744 | @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
| 1745 | @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
| 1746 | } |
| 1747 | xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, |
| 1748 | xfer.remoteTime, 0x04 ); |
| 1749 | }else |
| 1750 | |
| 1751 | /* pragma uv-hash HASH |
| 1752 | ** |
| 1753 | ** The client wants to make sure that unversioned files are all synced. |
| @@ -2341,18 +2384,17 @@ | |
| 2384 | blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
| 2385 | zCkinLock = 0; |
| 2386 | }else if( zClientId ){ |
| 2387 | blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
| 2388 | } |
| 2389 | /* Append randomness to the end of the uplink message. This makes all |
| 2390 | ** messages unique so that that the login-card nonce will always |
| 2391 | ** be unique. |
| 2392 | */ |
| 2393 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 2394 | blob_appendf(&send, "# %s\n", zRandomness); |
| 2395 | fossil_free(zRandomness); |
| 2396 | |
| 2397 | if( (syncFlags & SYNC_VERBOSE)!=0 |
| 2398 | && (syncFlags & SYNC_XVERBOSE)==0 |
| 2399 | ){ |
| 2400 | fossil_print("waiting for server..."); |
| @@ -2725,13 +2767,10 @@ | |
| 2767 | |
| 2768 | /* message MESSAGE |
| 2769 | ** |
| 2770 | ** A message is received from the server. Print it. |
| 2771 | ** Similar to "error" but does not stop processing. |
| 2772 | */ |
| 2773 | if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
| 2774 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 2775 | defossilize(zMsg); |
| 2776 | if( (syncFlags & SYNC_PUSH) && zMsg |
| @@ -2757,15 +2796,18 @@ | |
| 2796 | ** The server announces to the server what version of Fossil it |
| 2797 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 2798 | ** for the specific check-in of the client. |
| 2799 | */ |
| 2800 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
| 2801 | xfer.remoteVersion = g.syncInfo.remoteVersion = |
| 2802 | atoi(blob_str(&xfer.aToken[2])); |
| 2803 | if( xfer.nToken>=5 ){ |
| 2804 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 2805 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 2806 | } |
| 2807 | xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, |
| 2808 | xfer.remoteTime, 0x08 ); |
| 2809 | } |
| 2810 | |
| 2811 | /* pragma uv-pull-only |
| 2812 | ** pragma uv-push-ok |
| 2813 | ** |
| @@ -2896,11 +2938,11 @@ | |
| 2938 | &recv |
| 2939 | ); |
| 2940 | nErr++; |
| 2941 | break; |
| 2942 | } |
| 2943 | blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line); |
| 2944 | } |
| 2945 | |
| 2946 | if( blob_size(&xfer.err) ){ |
| 2947 | fossil_force_newline(); |
| 2948 | fossil_warning("%b", &xfer.err); |
| @@ -2963,11 +3005,11 @@ | |
| 3005 | }else{ |
| 3006 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 3007 | content_enable_dephantomize(1); |
| 3008 | } |
| 3009 | db_end_transaction(0); |
| 3010 | }; /* while(go) */ |
| 3011 | transport_stats(&nSent, &nRcvd, 1); |
| 3012 | if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
| 3013 | if( (rSkew*24.0*3600.0) > 10.0 ){ |
| 3014 | fossil_warning("*** time skew *** server is fast by %s", |
| 3015 | db_timespan_name(rSkew)); |
| 3016 |
+55
-13
| --- src/xfer.c | ||
| +++ src/xfer.c | ||
| @@ -827,11 +827,11 @@ | ||
| 827 | 827 | int rc = -1; |
| 828 | 828 | char *zLogin = blob_terminate(pLogin); |
| 829 | 829 | defossilize(zLogin); |
| 830 | 830 | |
| 831 | 831 | if( fossil_strcmp(zLogin, "nobody")==0 |
| 832 | - || fossil_strcmp(zLogin,"anonymous")==0 | |
| 832 | + || fossil_strcmp(zLogin, "anonymous")==0 | |
| 833 | 833 | ){ |
| 834 | 834 | return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
| 835 | 835 | } |
| 836 | 836 | if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
| 837 | 837 | && db_get_boolean("remote_user_ok",0) ){ |
| @@ -866,11 +866,11 @@ | ||
| 866 | 866 | const char *zPw = db_column_text(&q, 0); |
| 867 | 867 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
| 868 | 868 | blob_zero(&combined); |
| 869 | 869 | blob_copy(&combined, pNonce); |
| 870 | 870 | blob_append(&combined, zSecret, -1); |
| 871 | - free(zSecret); | |
| 871 | + fossil_free(zSecret); | |
| 872 | 872 | sha1sum_blob(&combined, &hash); |
| 873 | 873 | rc = blob_constant_time_cmp(&hash, pSig); |
| 874 | 874 | blob_reset(&hash); |
| 875 | 875 | blob_reset(&combined); |
| 876 | 876 | } |
| @@ -1241,10 +1241,25 @@ | ||
| 1241 | 1241 | /* |
| 1242 | 1242 | ** If this variable is set, disable login checks. Used for debugging |
| 1243 | 1243 | ** only. |
| 1244 | 1244 | */ |
| 1245 | 1245 | static int disableLogin = 0; |
| 1246 | + | |
| 1247 | +/* | |
| 1248 | +** Must be passed the version info from pragmas | |
| 1249 | +** client-version/server-version cards. If the version info is "new | |
| 1250 | +** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login | |
| 1251 | +** card flag, else this is a no-op. | |
| 1252 | +*/ | |
| 1253 | +static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime, | |
| 1254 | + int fLoginCardMode){ | |
| 1255 | + if( iRemoteVersion>=22700 | |
| 1256 | + && (iDate > 20250727 | |
| 1257 | + || (iDate == 20250727 && iTime >= 110500)) ){ | |
| 1258 | + g.syncInfo.fLoginCardMode |= fLoginCardMode; | |
| 1259 | + } | |
| 1260 | +} | |
| 1246 | 1261 | |
| 1247 | 1262 | /* |
| 1248 | 1263 | ** The CGI/HTTP preprocessor always redirects requests with a content-type |
| 1249 | 1264 | ** of application/x-fossil or application/x-fossil-debug to this page, |
| 1250 | 1265 | ** regardless of what path was specified in the HTTP header. This allows |
| @@ -1273,10 +1288,11 @@ | ||
| 1273 | 1288 | int nUuidList = 0; |
| 1274 | 1289 | char **pzUuidList = 0; |
| 1275 | 1290 | int *pnUuidList = 0; |
| 1276 | 1291 | int uvCatalogSent = 0; |
| 1277 | 1292 | int bSendLinks = 0; |
| 1293 | + int nLogin = 0; | |
| 1278 | 1294 | |
| 1279 | 1295 | if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
| 1280 | 1296 | fossil_redirect_home(); |
| 1281 | 1297 | } |
| 1282 | 1298 | g.zLogin = "anonymous"; |
| @@ -1314,10 +1330,24 @@ | ||
| 1314 | 1330 | } |
| 1315 | 1331 | zScript = xfer_push_code(); |
| 1316 | 1332 | if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
| 1317 | 1333 | pzUuidList = &zUuidList; |
| 1318 | 1334 | pnUuidList = &nUuidList; |
| 1335 | + } | |
| 1336 | + if( g.syncInfo.zLoginCard ){ | |
| 1337 | + /* Login card received via HTTP Cookie header */ | |
| 1338 | + assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" ); | |
| 1339 | + blob_zero(&xfer.line); | |
| 1340 | + blob_append(&xfer.line, g.syncInfo.zLoginCard, -1); | |
| 1341 | + xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, | |
| 1342 | + count(xfer.aToken)); | |
| 1343 | + fossil_free( g.syncInfo.zLoginCard ); | |
| 1344 | + g.syncInfo.zLoginCard = 0; | |
| 1345 | + if( xfer.nToken==4 | |
| 1346 | + && blob_eq(&xfer.aToken[0], "login") ){ | |
| 1347 | + goto handle_login_card; | |
| 1348 | + } | |
| 1319 | 1349 | } |
| 1320 | 1350 | while( blob_line(xfer.pIn, &xfer.line) ){ |
| 1321 | 1351 | if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
| 1322 | 1352 | if( blob_size(&xfer.line)==0 ) continue; |
| 1323 | 1353 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
| @@ -1550,17 +1580,28 @@ | ||
| 1550 | 1580 | |
| 1551 | 1581 | /* login USER NONCE SIGNATURE |
| 1552 | 1582 | ** |
| 1553 | 1583 | ** The client has sent login credentials to the server. |
| 1554 | 1584 | ** Validate the login. This has to happen before anything else. |
| 1555 | - ** The client can send multiple logins. Permissions are cumulative. | |
| 1585 | + ** | |
| 1586 | + ** For many years, Fossil would accept multiple login cards with | |
| 1587 | + ** cumulative permissions. But that feature was never used. Hence | |
| 1588 | + ** it is now prohibited. Any login card after the first generates | |
| 1589 | + ** a fatal error. | |
| 1556 | 1590 | */ |
| 1557 | 1591 | if( blob_eq(&xfer.aToken[0], "login") |
| 1558 | 1592 | && xfer.nToken==4 |
| 1559 | 1593 | ){ |
| 1594 | + handle_login_card: | |
| 1595 | + nLogin++; | |
| 1560 | 1596 | if( disableLogin ){ |
| 1561 | 1597 | g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
| 1598 | + }else if( nLogin > 1 ){ | |
| 1599 | + cgi_reset_content(); | |
| 1600 | + @ error multiple\slogin\cards | |
| 1601 | + nErr++; | |
| 1602 | + break; | |
| 1562 | 1603 | }else{ |
| 1563 | 1604 | if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
| 1564 | 1605 | || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
| 1565 | 1606 | ){ |
| 1566 | 1607 | cgi_reset_content(); |
| @@ -1651,11 +1692,10 @@ | ||
| 1651 | 1692 | }else{ |
| 1652 | 1693 | xfer.nextIsPrivate = 1; |
| 1653 | 1694 | } |
| 1654 | 1695 | }else |
| 1655 | 1696 | |
| 1656 | - | |
| 1657 | 1697 | /* pragma NAME VALUE... |
| 1658 | 1698 | ** |
| 1659 | 1699 | ** The client issues pragmas to try to influence the behavior of the |
| 1660 | 1700 | ** server. These are requests only. Unknown pragmas are silently |
| 1661 | 1701 | ** ignored. |
| @@ -1694,17 +1734,20 @@ | ||
| 1694 | 1734 | ** The client announces to the server what version of Fossil it |
| 1695 | 1735 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 1696 | 1736 | ** for the specific check-in of the client. |
| 1697 | 1737 | */ |
| 1698 | 1738 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
| 1699 | - xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); | |
| 1739 | + xfer.remoteVersion = g.syncInfo.remoteVersion = | |
| 1740 | + atoi(blob_str(&xfer.aToken[2])); | |
| 1700 | 1741 | if( xfer.nToken>=5 ){ |
| 1701 | 1742 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 1702 | 1743 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 1703 | 1744 | @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
| 1704 | 1745 | @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
| 1705 | 1746 | } |
| 1747 | + xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, | |
| 1748 | + xfer.remoteTime, 0x04 ); | |
| 1706 | 1749 | }else |
| 1707 | 1750 | |
| 1708 | 1751 | /* pragma uv-hash HASH |
| 1709 | 1752 | ** |
| 1710 | 1753 | ** The client wants to make sure that unversioned files are all synced. |
| @@ -2341,18 +2384,17 @@ | ||
| 2341 | 2384 | blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
| 2342 | 2385 | zCkinLock = 0; |
| 2343 | 2386 | }else if( zClientId ){ |
| 2344 | 2387 | blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
| 2345 | 2388 | } |
| 2346 | - | |
| 2347 | 2389 | /* Append randomness to the end of the uplink message. This makes all |
| 2348 | 2390 | ** messages unique so that that the login-card nonce will always |
| 2349 | 2391 | ** be unique. |
| 2350 | 2392 | */ |
| 2351 | 2393 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 2352 | 2394 | blob_appendf(&send, "# %s\n", zRandomness); |
| 2353 | - free(zRandomness); | |
| 2395 | + fossil_free(zRandomness); | |
| 2354 | 2396 | |
| 2355 | 2397 | if( (syncFlags & SYNC_VERBOSE)!=0 |
| 2356 | 2398 | && (syncFlags & SYNC_XVERBOSE)==0 |
| 2357 | 2399 | ){ |
| 2358 | 2400 | fossil_print("waiting for server..."); |
| @@ -2725,13 +2767,10 @@ | ||
| 2725 | 2767 | |
| 2726 | 2768 | /* message MESSAGE |
| 2727 | 2769 | ** |
| 2728 | 2770 | ** A message is received from the server. Print it. |
| 2729 | 2771 | ** Similar to "error" but does not stop processing. |
| 2730 | - ** | |
| 2731 | - ** If the "login failed" message is seen, clear the sync password prior | |
| 2732 | - ** to the next cycle. | |
| 2733 | 2772 | */ |
| 2734 | 2773 | if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
| 2735 | 2774 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 2736 | 2775 | defossilize(zMsg); |
| 2737 | 2776 | if( (syncFlags & SYNC_PUSH) && zMsg |
| @@ -2757,15 +2796,18 @@ | ||
| 2757 | 2796 | ** The server announces to the server what version of Fossil it |
| 2758 | 2797 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 2759 | 2798 | ** for the specific check-in of the client. |
| 2760 | 2799 | */ |
| 2761 | 2800 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
| 2762 | - xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); | |
| 2801 | + xfer.remoteVersion = g.syncInfo.remoteVersion = | |
| 2802 | + atoi(blob_str(&xfer.aToken[2])); | |
| 2763 | 2803 | if( xfer.nToken>=5 ){ |
| 2764 | 2804 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 2765 | 2805 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 2766 | 2806 | } |
| 2807 | + xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, | |
| 2808 | + xfer.remoteTime, 0x08 ); | |
| 2767 | 2809 | } |
| 2768 | 2810 | |
| 2769 | 2811 | /* pragma uv-pull-only |
| 2770 | 2812 | ** pragma uv-push-ok |
| 2771 | 2813 | ** |
| @@ -2896,11 +2938,11 @@ | ||
| 2896 | 2938 | &recv |
| 2897 | 2939 | ); |
| 2898 | 2940 | nErr++; |
| 2899 | 2941 | break; |
| 2900 | 2942 | } |
| 2901 | - blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]); | |
| 2943 | + blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line); | |
| 2902 | 2944 | } |
| 2903 | 2945 | |
| 2904 | 2946 | if( blob_size(&xfer.err) ){ |
| 2905 | 2947 | fossil_force_newline(); |
| 2906 | 2948 | fossil_warning("%b", &xfer.err); |
| @@ -2963,11 +3005,11 @@ | ||
| 2963 | 3005 | }else{ |
| 2964 | 3006 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2965 | 3007 | content_enable_dephantomize(1); |
| 2966 | 3008 | } |
| 2967 | 3009 | db_end_transaction(0); |
| 2968 | - }; | |
| 3010 | + }; /* while(go) */ | |
| 2969 | 3011 | transport_stats(&nSent, &nRcvd, 1); |
| 2970 | 3012 | if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
| 2971 | 3013 | if( (rSkew*24.0*3600.0) > 10.0 ){ |
| 2972 | 3014 | fossil_warning("*** time skew *** server is fast by %s", |
| 2973 | 3015 | db_timespan_name(rSkew)); |
| 2974 | 3016 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -827,11 +827,11 @@ | |
| 827 | int rc = -1; |
| 828 | char *zLogin = blob_terminate(pLogin); |
| 829 | defossilize(zLogin); |
| 830 | |
| 831 | if( fossil_strcmp(zLogin, "nobody")==0 |
| 832 | || fossil_strcmp(zLogin,"anonymous")==0 |
| 833 | ){ |
| 834 | return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
| 835 | } |
| 836 | if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
| 837 | && db_get_boolean("remote_user_ok",0) ){ |
| @@ -866,11 +866,11 @@ | |
| 866 | const char *zPw = db_column_text(&q, 0); |
| 867 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
| 868 | blob_zero(&combined); |
| 869 | blob_copy(&combined, pNonce); |
| 870 | blob_append(&combined, zSecret, -1); |
| 871 | free(zSecret); |
| 872 | sha1sum_blob(&combined, &hash); |
| 873 | rc = blob_constant_time_cmp(&hash, pSig); |
| 874 | blob_reset(&hash); |
| 875 | blob_reset(&combined); |
| 876 | } |
| @@ -1241,10 +1241,25 @@ | |
| 1241 | /* |
| 1242 | ** If this variable is set, disable login checks. Used for debugging |
| 1243 | ** only. |
| 1244 | */ |
| 1245 | static int disableLogin = 0; |
| 1246 | |
| 1247 | /* |
| 1248 | ** The CGI/HTTP preprocessor always redirects requests with a content-type |
| 1249 | ** of application/x-fossil or application/x-fossil-debug to this page, |
| 1250 | ** regardless of what path was specified in the HTTP header. This allows |
| @@ -1273,10 +1288,11 @@ | |
| 1273 | int nUuidList = 0; |
| 1274 | char **pzUuidList = 0; |
| 1275 | int *pnUuidList = 0; |
| 1276 | int uvCatalogSent = 0; |
| 1277 | int bSendLinks = 0; |
| 1278 | |
| 1279 | if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
| 1280 | fossil_redirect_home(); |
| 1281 | } |
| 1282 | g.zLogin = "anonymous"; |
| @@ -1314,10 +1330,24 @@ | |
| 1314 | } |
| 1315 | zScript = xfer_push_code(); |
| 1316 | if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
| 1317 | pzUuidList = &zUuidList; |
| 1318 | pnUuidList = &nUuidList; |
| 1319 | } |
| 1320 | while( blob_line(xfer.pIn, &xfer.line) ){ |
| 1321 | if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
| 1322 | if( blob_size(&xfer.line)==0 ) continue; |
| 1323 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
| @@ -1550,17 +1580,28 @@ | |
| 1550 | |
| 1551 | /* login USER NONCE SIGNATURE |
| 1552 | ** |
| 1553 | ** The client has sent login credentials to the server. |
| 1554 | ** Validate the login. This has to happen before anything else. |
| 1555 | ** The client can send multiple logins. Permissions are cumulative. |
| 1556 | */ |
| 1557 | if( blob_eq(&xfer.aToken[0], "login") |
| 1558 | && xfer.nToken==4 |
| 1559 | ){ |
| 1560 | if( disableLogin ){ |
| 1561 | g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
| 1562 | }else{ |
| 1563 | if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
| 1564 | || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
| 1565 | ){ |
| 1566 | cgi_reset_content(); |
| @@ -1651,11 +1692,10 @@ | |
| 1651 | }else{ |
| 1652 | xfer.nextIsPrivate = 1; |
| 1653 | } |
| 1654 | }else |
| 1655 | |
| 1656 | |
| 1657 | /* pragma NAME VALUE... |
| 1658 | ** |
| 1659 | ** The client issues pragmas to try to influence the behavior of the |
| 1660 | ** server. These are requests only. Unknown pragmas are silently |
| 1661 | ** ignored. |
| @@ -1694,17 +1734,20 @@ | |
| 1694 | ** The client announces to the server what version of Fossil it |
| 1695 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 1696 | ** for the specific check-in of the client. |
| 1697 | */ |
| 1698 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
| 1699 | xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); |
| 1700 | if( xfer.nToken>=5 ){ |
| 1701 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 1702 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 1703 | @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
| 1704 | @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
| 1705 | } |
| 1706 | }else |
| 1707 | |
| 1708 | /* pragma uv-hash HASH |
| 1709 | ** |
| 1710 | ** The client wants to make sure that unversioned files are all synced. |
| @@ -2341,18 +2384,17 @@ | |
| 2341 | blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
| 2342 | zCkinLock = 0; |
| 2343 | }else if( zClientId ){ |
| 2344 | blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
| 2345 | } |
| 2346 | |
| 2347 | /* Append randomness to the end of the uplink message. This makes all |
| 2348 | ** messages unique so that that the login-card nonce will always |
| 2349 | ** be unique. |
| 2350 | */ |
| 2351 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 2352 | blob_appendf(&send, "# %s\n", zRandomness); |
| 2353 | free(zRandomness); |
| 2354 | |
| 2355 | if( (syncFlags & SYNC_VERBOSE)!=0 |
| 2356 | && (syncFlags & SYNC_XVERBOSE)==0 |
| 2357 | ){ |
| 2358 | fossil_print("waiting for server..."); |
| @@ -2725,13 +2767,10 @@ | |
| 2725 | |
| 2726 | /* message MESSAGE |
| 2727 | ** |
| 2728 | ** A message is received from the server. Print it. |
| 2729 | ** Similar to "error" but does not stop processing. |
| 2730 | ** |
| 2731 | ** If the "login failed" message is seen, clear the sync password prior |
| 2732 | ** to the next cycle. |
| 2733 | */ |
| 2734 | if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
| 2735 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 2736 | defossilize(zMsg); |
| 2737 | if( (syncFlags & SYNC_PUSH) && zMsg |
| @@ -2757,15 +2796,18 @@ | |
| 2757 | ** The server announces to the server what version of Fossil it |
| 2758 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 2759 | ** for the specific check-in of the client. |
| 2760 | */ |
| 2761 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
| 2762 | xfer.remoteVersion = atoi(blob_str(&xfer.aToken[2])); |
| 2763 | if( xfer.nToken>=5 ){ |
| 2764 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 2765 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 2766 | } |
| 2767 | } |
| 2768 | |
| 2769 | /* pragma uv-pull-only |
| 2770 | ** pragma uv-push-ok |
| 2771 | ** |
| @@ -2896,11 +2938,11 @@ | |
| 2896 | &recv |
| 2897 | ); |
| 2898 | nErr++; |
| 2899 | break; |
| 2900 | } |
| 2901 | blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.aToken[0]); |
| 2902 | } |
| 2903 | |
| 2904 | if( blob_size(&xfer.err) ){ |
| 2905 | fossil_force_newline(); |
| 2906 | fossil_warning("%b", &xfer.err); |
| @@ -2963,11 +3005,11 @@ | |
| 2963 | }else{ |
| 2964 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 2965 | content_enable_dephantomize(1); |
| 2966 | } |
| 2967 | db_end_transaction(0); |
| 2968 | }; |
| 2969 | transport_stats(&nSent, &nRcvd, 1); |
| 2970 | if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
| 2971 | if( (rSkew*24.0*3600.0) > 10.0 ){ |
| 2972 | fossil_warning("*** time skew *** server is fast by %s", |
| 2973 | db_timespan_name(rSkew)); |
| 2974 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -827,11 +827,11 @@ | |
| 827 | int rc = -1; |
| 828 | char *zLogin = blob_terminate(pLogin); |
| 829 | defossilize(zLogin); |
| 830 | |
| 831 | if( fossil_strcmp(zLogin, "nobody")==0 |
| 832 | || fossil_strcmp(zLogin, "anonymous")==0 |
| 833 | ){ |
| 834 | return 0; /* Anybody is allowed to sync as "nobody" or "anonymous" */ |
| 835 | } |
| 836 | if( fossil_strcmp(P("REMOTE_USER"), zLogin)==0 |
| 837 | && db_get_boolean("remote_user_ok",0) ){ |
| @@ -866,11 +866,11 @@ | |
| 866 | const char *zPw = db_column_text(&q, 0); |
| 867 | char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0); |
| 868 | blob_zero(&combined); |
| 869 | blob_copy(&combined, pNonce); |
| 870 | blob_append(&combined, zSecret, -1); |
| 871 | fossil_free(zSecret); |
| 872 | sha1sum_blob(&combined, &hash); |
| 873 | rc = blob_constant_time_cmp(&hash, pSig); |
| 874 | blob_reset(&hash); |
| 875 | blob_reset(&combined); |
| 876 | } |
| @@ -1241,10 +1241,25 @@ | |
| 1241 | /* |
| 1242 | ** If this variable is set, disable login checks. Used for debugging |
| 1243 | ** only. |
| 1244 | */ |
| 1245 | static int disableLogin = 0; |
| 1246 | |
| 1247 | /* |
| 1248 | ** Must be passed the version info from pragmas |
| 1249 | ** client-version/server-version cards. If the version info is "new |
| 1250 | ** enough" then the loginCardMode is ORd into the X-Fossil-Xfer-Login |
| 1251 | ** card flag, else this is a no-op. |
| 1252 | */ |
| 1253 | static void xfer_xflc_check(int iRemoteVersion, int iDate, int iTime, |
| 1254 | int fLoginCardMode){ |
| 1255 | if( iRemoteVersion>=22700 |
| 1256 | && (iDate > 20250727 |
| 1257 | || (iDate == 20250727 && iTime >= 110500)) ){ |
| 1258 | g.syncInfo.fLoginCardMode |= fLoginCardMode; |
| 1259 | } |
| 1260 | } |
| 1261 | |
| 1262 | /* |
| 1263 | ** The CGI/HTTP preprocessor always redirects requests with a content-type |
| 1264 | ** of application/x-fossil or application/x-fossil-debug to this page, |
| 1265 | ** regardless of what path was specified in the HTTP header. This allows |
| @@ -1273,10 +1288,11 @@ | |
| 1288 | int nUuidList = 0; |
| 1289 | char **pzUuidList = 0; |
| 1290 | int *pnUuidList = 0; |
| 1291 | int uvCatalogSent = 0; |
| 1292 | int bSendLinks = 0; |
| 1293 | int nLogin = 0; |
| 1294 | |
| 1295 | if( fossil_strcmp(PD("REQUEST_METHOD","POST"),"POST") ){ |
| 1296 | fossil_redirect_home(); |
| 1297 | } |
| 1298 | g.zLogin = "anonymous"; |
| @@ -1314,10 +1330,24 @@ | |
| 1330 | } |
| 1331 | zScript = xfer_push_code(); |
| 1332 | if( zScript ){ /* NOTE: Are TH1 transfer hooks enabled? */ |
| 1333 | pzUuidList = &zUuidList; |
| 1334 | pnUuidList = &nUuidList; |
| 1335 | } |
| 1336 | if( g.syncInfo.zLoginCard ){ |
| 1337 | /* Login card received via HTTP Cookie header */ |
| 1338 | assert( g.syncInfo.fLoginCardMode && "Set via HTTP cookie" ); |
| 1339 | blob_zero(&xfer.line); |
| 1340 | blob_append(&xfer.line, g.syncInfo.zLoginCard, -1); |
| 1341 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, |
| 1342 | count(xfer.aToken)); |
| 1343 | fossil_free( g.syncInfo.zLoginCard ); |
| 1344 | g.syncInfo.zLoginCard = 0; |
| 1345 | if( xfer.nToken==4 |
| 1346 | && blob_eq(&xfer.aToken[0], "login") ){ |
| 1347 | goto handle_login_card; |
| 1348 | } |
| 1349 | } |
| 1350 | while( blob_line(xfer.pIn, &xfer.line) ){ |
| 1351 | if( blob_buffer(&xfer.line)[0]=='#' ) continue; |
| 1352 | if( blob_size(&xfer.line)==0 ) continue; |
| 1353 | xfer.nToken = blob_tokenize(&xfer.line, xfer.aToken, count(xfer.aToken)); |
| @@ -1550,17 +1580,28 @@ | |
| 1580 | |
| 1581 | /* login USER NONCE SIGNATURE |
| 1582 | ** |
| 1583 | ** The client has sent login credentials to the server. |
| 1584 | ** Validate the login. This has to happen before anything else. |
| 1585 | ** |
| 1586 | ** For many years, Fossil would accept multiple login cards with |
| 1587 | ** cumulative permissions. But that feature was never used. Hence |
| 1588 | ** it is now prohibited. Any login card after the first generates |
| 1589 | ** a fatal error. |
| 1590 | */ |
| 1591 | if( blob_eq(&xfer.aToken[0], "login") |
| 1592 | && xfer.nToken==4 |
| 1593 | ){ |
| 1594 | handle_login_card: |
| 1595 | nLogin++; |
| 1596 | if( disableLogin ){ |
| 1597 | g.perm.Read = g.perm.Write = g.perm.Private = g.perm.Admin = 1; |
| 1598 | }else if( nLogin > 1 ){ |
| 1599 | cgi_reset_content(); |
| 1600 | @ error multiple\slogin\cards |
| 1601 | nErr++; |
| 1602 | break; |
| 1603 | }else{ |
| 1604 | if( check_tail_hash(&xfer.aToken[2], xfer.pIn) |
| 1605 | || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) |
| 1606 | ){ |
| 1607 | cgi_reset_content(); |
| @@ -1651,11 +1692,10 @@ | |
| 1692 | }else{ |
| 1693 | xfer.nextIsPrivate = 1; |
| 1694 | } |
| 1695 | }else |
| 1696 | |
| 1697 | /* pragma NAME VALUE... |
| 1698 | ** |
| 1699 | ** The client issues pragmas to try to influence the behavior of the |
| 1700 | ** server. These are requests only. Unknown pragmas are silently |
| 1701 | ** ignored. |
| @@ -1694,17 +1734,20 @@ | |
| 1734 | ** The client announces to the server what version of Fossil it |
| 1735 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 1736 | ** for the specific check-in of the client. |
| 1737 | */ |
| 1738 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "client-version") ){ |
| 1739 | xfer.remoteVersion = g.syncInfo.remoteVersion = |
| 1740 | atoi(blob_str(&xfer.aToken[2])); |
| 1741 | if( xfer.nToken>=5 ){ |
| 1742 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 1743 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 1744 | @ pragma server-version %d(RELEASE_VERSION_NUMBER) \ |
| 1745 | @ %d(MANIFEST_NUMERIC_DATE) %d(MANIFEST_NUMERIC_TIME) |
| 1746 | } |
| 1747 | xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, |
| 1748 | xfer.remoteTime, 0x04 ); |
| 1749 | }else |
| 1750 | |
| 1751 | /* pragma uv-hash HASH |
| 1752 | ** |
| 1753 | ** The client wants to make sure that unversioned files are all synced. |
| @@ -2341,18 +2384,17 @@ | |
| 2384 | blob_appendf(&send, "pragma ci-lock %s %s\n", zCkinLock, zClientId); |
| 2385 | zCkinLock = 0; |
| 2386 | }else if( zClientId ){ |
| 2387 | blob_appendf(&send, "pragma ci-unlock %s\n", zClientId); |
| 2388 | } |
| 2389 | /* Append randomness to the end of the uplink message. This makes all |
| 2390 | ** messages unique so that that the login-card nonce will always |
| 2391 | ** be unique. |
| 2392 | */ |
| 2393 | zRandomness = db_text(0, "SELECT hex(randomblob(20))"); |
| 2394 | blob_appendf(&send, "# %s\n", zRandomness); |
| 2395 | fossil_free(zRandomness); |
| 2396 | |
| 2397 | if( (syncFlags & SYNC_VERBOSE)!=0 |
| 2398 | && (syncFlags & SYNC_XVERBOSE)==0 |
| 2399 | ){ |
| 2400 | fossil_print("waiting for server..."); |
| @@ -2725,13 +2767,10 @@ | |
| 2767 | |
| 2768 | /* message MESSAGE |
| 2769 | ** |
| 2770 | ** A message is received from the server. Print it. |
| 2771 | ** Similar to "error" but does not stop processing. |
| 2772 | */ |
| 2773 | if( blob_eq(&xfer.aToken[0],"message") && xfer.nToken==2 ){ |
| 2774 | char *zMsg = blob_terminate(&xfer.aToken[1]); |
| 2775 | defossilize(zMsg); |
| 2776 | if( (syncFlags & SYNC_PUSH) && zMsg |
| @@ -2757,15 +2796,18 @@ | |
| 2796 | ** The server announces to the server what version of Fossil it |
| 2797 | ** is running. The DATE and TIME are a pure numeric ISO8601 time |
| 2798 | ** for the specific check-in of the client. |
| 2799 | */ |
| 2800 | if( xfer.nToken>=3 && blob_eq(&xfer.aToken[1], "server-version") ){ |
| 2801 | xfer.remoteVersion = g.syncInfo.remoteVersion = |
| 2802 | atoi(blob_str(&xfer.aToken[2])); |
| 2803 | if( xfer.nToken>=5 ){ |
| 2804 | xfer.remoteDate = atoi(blob_str(&xfer.aToken[3])); |
| 2805 | xfer.remoteTime = atoi(blob_str(&xfer.aToken[4])); |
| 2806 | } |
| 2807 | xfer_xflc_check( xfer.remoteVersion, xfer.remoteDate, |
| 2808 | xfer.remoteTime, 0x08 ); |
| 2809 | } |
| 2810 | |
| 2811 | /* pragma uv-pull-only |
| 2812 | ** pragma uv-push-ok |
| 2813 | ** |
| @@ -2896,11 +2938,11 @@ | |
| 2938 | &recv |
| 2939 | ); |
| 2940 | nErr++; |
| 2941 | break; |
| 2942 | } |
| 2943 | blob_appendf(&xfer.err, "unknown command: [%b]\n", &xfer.line); |
| 2944 | } |
| 2945 | |
| 2946 | if( blob_size(&xfer.err) ){ |
| 2947 | fossil_force_newline(); |
| 2948 | fossil_warning("%b", &xfer.err); |
| @@ -2963,11 +3005,11 @@ | |
| 3005 | }else{ |
| 3006 | manifest_crosslink_end(MC_PERMIT_HOOKS); |
| 3007 | content_enable_dephantomize(1); |
| 3008 | } |
| 3009 | db_end_transaction(0); |
| 3010 | }; /* while(go) */ |
| 3011 | transport_stats(&nSent, &nRcvd, 1); |
| 3012 | if( pnRcvd ) *pnRcvd = nArtifactRcvd; |
| 3013 | if( (rSkew*24.0*3600.0) > 10.0 ){ |
| 3014 | fossil_warning("*** time skew *** server is fast by %s", |
| 3015 | db_timespan_name(rSkew)); |
| 3016 |
+2
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -13,10 +13,12 @@ | ||
| 13 | 13 | <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command. |
| 14 | 14 | <li> Require at least an anonymous login to access the /blame page and similar, |
| 15 | 15 | to help prevent robots from soaking up excess CPU time on such pages. |
| 16 | 16 | <li> When walking the filesystem looking for Fossil repositories, avoid descending |
| 17 | 17 | into directories named "/proc". |
| 18 | + <ll> Reduce memory requirements for sending authenticated sync protocol | |
| 19 | + messages. | |
| 18 | 20 | </ol> |
| 19 | 21 | |
| 20 | 22 | <h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol> |
| 21 | 23 | <li>Enhancements to [/help?cmd=diff|fossil diff] and similar: |
| 22 | 24 | <ol type="a"> |
| 23 | 25 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -13,10 +13,12 @@ | |
| 13 | <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command. |
| 14 | <li> Require at least an anonymous login to access the /blame page and similar, |
| 15 | to help prevent robots from soaking up excess CPU time on such pages. |
| 16 | <li> When walking the filesystem looking for Fossil repositories, avoid descending |
| 17 | into directories named "/proc". |
| 18 | </ol> |
| 19 | |
| 20 | <h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol> |
| 21 | <li>Enhancements to [/help?cmd=diff|fossil diff] and similar: |
| 22 | <ol type="a"> |
| 23 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -13,10 +13,12 @@ | |
| 13 | <li> Enable the --editor option on the [/help?cmd=amend|fossil amend] command. |
| 14 | <li> Require at least an anonymous login to access the /blame page and similar, |
| 15 | to help prevent robots from soaking up excess CPU time on such pages. |
| 16 | <li> When walking the filesystem looking for Fossil repositories, avoid descending |
| 17 | into directories named "/proc". |
| 18 | <ll> Reduce memory requirements for sending authenticated sync protocol |
| 19 | messages. |
| 20 | </ol> |
| 21 | |
| 22 | <h2 id='v2_26'>Changes for version 2.26 (2025-04-30)</h2><ol> |
| 23 | <li>Enhancements to [/help?cmd=diff|fossil diff] and similar: |
| 24 | <ol type="a"> |
| 25 |
+28
-16
| --- www/sync.wiki | ||
| +++ www/sync.wiki | ||
| @@ -220,26 +220,38 @@ | ||
| 220 | 220 | Every message from client to server begins with one or more login |
| 221 | 221 | cards. Each login card has the following format: |
| 222 | 222 | |
| 223 | 223 | <pre><b>login</b> <i>userid nonce signature</i></pre> |
| 224 | 224 | |
| 225 | -The userid is the name of the user that is requesting service | |
| 226 | -from the server. The nonce is the SHA1 hash of the remainder of | |
| 227 | -the message - all text that follows the newline character that | |
| 228 | -terminates the login card. The signature is the SHA1 hash of | |
| 229 | -the concatenation of the nonce and the users password. | |
| 230 | - | |
| 231 | -For each login card, the server looks up the user and verifies | |
| 232 | -that the nonce matches the SHA1 hash of the remainder of the | |
| 233 | -message. It then checks the signature hash to make sure the | |
| 234 | -signature matches. If everything | |
| 235 | -checks out, then the client is granted all privileges of the | |
| 236 | -specified user. | |
| 237 | - | |
| 238 | -Privileges are cumulative. There can be multiple successful | |
| 239 | -login cards. The session privilege is the union of all | |
| 240 | -privileges from all login cards. | |
| 225 | +The userid is the name of the user that is requesting service from the | |
| 226 | +server, encoded in "fossilized" form (exactly as described for <a | |
| 227 | +href="#error">the error card</a>). The nonce is the SHA1 hash of the | |
| 228 | +remainder of the message - all text that follows the newline character | |
| 229 | +that terminates the login card. The signature is the SHA1 hash of the | |
| 230 | +concatenation of the nonce and the users password. | |
| 231 | + | |
| 232 | +When receving a login card, the server looks up the user and verifies | |
| 233 | +that the nonce matches the SHA1 hash of the remainder of the message. | |
| 234 | +It then checks the signature hash to make sure the signature matches. | |
| 235 | +If everything checks out, then the client is granted all privileges of | |
| 236 | +the specified user. | |
| 237 | + | |
| 238 | +Only one login card is permitted. A second login card will trigger | |
| 239 | +a sync error. (Prior to 2025-07-21, the protocol permitted multiple | |
| 240 | +logins, treating the login as the union of all privileges from all | |
| 241 | +login cards. That capability was never used and has been removed.) | |
| 242 | + | |
| 243 | +As of version 2.27, Fossil supports transfering of the login card | |
| 244 | +externally to the request payload via a Cookie HTTP header: | |
| 245 | + | |
| 246 | +<verbatim> | |
| 247 | + Cookie: x-f-x-l=... | |
| 248 | +</verbatim> | |
| 249 | + | |
| 250 | +Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is | |
| 251 | +short for X-Fossil-Xfer-Login. | |
| 252 | + | |
| 241 | 253 | |
| 242 | 254 | <h3 id="file">3.3 File Cards</h3> |
| 243 | 255 | |
| 244 | 256 | Artifacts are transferred using either "file" cards, or "cfile" |
| 245 | 257 | or "uvfile" cards. |
| 246 | 258 |
| --- www/sync.wiki | |
| +++ www/sync.wiki | |
| @@ -220,26 +220,38 @@ | |
| 220 | Every message from client to server begins with one or more login |
| 221 | cards. Each login card has the following format: |
| 222 | |
| 223 | <pre><b>login</b> <i>userid nonce signature</i></pre> |
| 224 | |
| 225 | The userid is the name of the user that is requesting service |
| 226 | from the server. The nonce is the SHA1 hash of the remainder of |
| 227 | the message - all text that follows the newline character that |
| 228 | terminates the login card. The signature is the SHA1 hash of |
| 229 | the concatenation of the nonce and the users password. |
| 230 | |
| 231 | For each login card, the server looks up the user and verifies |
| 232 | that the nonce matches the SHA1 hash of the remainder of the |
| 233 | message. It then checks the signature hash to make sure the |
| 234 | signature matches. If everything |
| 235 | checks out, then the client is granted all privileges of the |
| 236 | specified user. |
| 237 | |
| 238 | Privileges are cumulative. There can be multiple successful |
| 239 | login cards. The session privilege is the union of all |
| 240 | privileges from all login cards. |
| 241 | |
| 242 | <h3 id="file">3.3 File Cards</h3> |
| 243 | |
| 244 | Artifacts are transferred using either "file" cards, or "cfile" |
| 245 | or "uvfile" cards. |
| 246 |
| --- www/sync.wiki | |
| +++ www/sync.wiki | |
| @@ -220,26 +220,38 @@ | |
| 220 | Every message from client to server begins with one or more login |
| 221 | cards. Each login card has the following format: |
| 222 | |
| 223 | <pre><b>login</b> <i>userid nonce signature</i></pre> |
| 224 | |
| 225 | The userid is the name of the user that is requesting service from the |
| 226 | server, encoded in "fossilized" form (exactly as described for <a |
| 227 | href="#error">the error card</a>). The nonce is the SHA1 hash of the |
| 228 | remainder of the message - all text that follows the newline character |
| 229 | that terminates the login card. The signature is the SHA1 hash of the |
| 230 | concatenation of the nonce and the users password. |
| 231 | |
| 232 | When receving a login card, the server looks up the user and verifies |
| 233 | that the nonce matches the SHA1 hash of the remainder of the message. |
| 234 | It then checks the signature hash to make sure the signature matches. |
| 235 | If everything checks out, then the client is granted all privileges of |
| 236 | the specified user. |
| 237 | |
| 238 | Only one login card is permitted. A second login card will trigger |
| 239 | a sync error. (Prior to 2025-07-21, the protocol permitted multiple |
| 240 | logins, treating the login as the union of all privileges from all |
| 241 | login cards. That capability was never used and has been removed.) |
| 242 | |
| 243 | As of version 2.27, Fossil supports transfering of the login card |
| 244 | externally to the request payload via a Cookie HTTP header: |
| 245 | |
| 246 | <verbatim> |
| 247 | Cookie: x-f-x-l=... |
| 248 | </verbatim> |
| 249 | |
| 250 | Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is |
| 251 | short for X-Fossil-Xfer-Login. |
| 252 | |
| 253 | |
| 254 | <h3 id="file">3.3 File Cards</h3> |
| 255 | |
| 256 | Artifacts are transferred using either "file" cards, or "cfile" |
| 257 | or "uvfile" cards. |
| 258 |