diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/http_transport.c | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/http_transport.c')
| -rw-r--r-- | src/http_transport.c | 545 |
1 files changed, 545 insertions, 0 deletions
diff --git a/src/http_transport.c b/src/http_transport.c new file mode 100644 index 0000000..1b3982f --- /dev/null +++ b/src/http_transport.c @@ -0,0 +1,545 @@ +/* +** Copyright (c) 2009 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This module implements the transport layer for the client side HTTP +** connection. The purpose of this layer is to provide a common interface +** for both HTTP and HTTPS and to provide a common "fetch one line" +** interface that is used for parsing the reply. +*/ +#include "config.h" +#include "http_transport.h" + +/* +** State information +*/ +static struct { + int isOpen; /* True when the transport layer is open */ + char *pBuf; /* Buffer used to hold the reply */ + int nAlloc; /* Space allocated for transportBuf[] */ + int nUsed ; /* Space of transportBuf[] used */ + int iCursor; /* Next unread by in transportBuf[] */ + i64 nSent; /* Number of bytes sent */ + i64 nRcvd; /* Number of bytes received */ + FILE *pFile; /* File I/O for FILE: */ + char *zOutFile; /* Name of outbound file for FILE: */ + char *zInFile; /* Name of inbound file for FILE: */ + FILE *pLog; /* Log output here */ + int bTest; /* file:// testing */ +} transport = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +/* +** Information about the connection to the SSH subprocess when +** using the ssh:// sync method. +*/ +static int sshPid; /* Process id of ssh subprocess */ +static int sshIn; /* From ssh subprocess to this process */ +static FILE *sshOut; /* From this to ssh subprocess */ + + +/* +** Return the current transport error message. +*/ +const char *transport_errmsg(UrlData *pUrlData){ + #ifdef FOSSIL_ENABLE_SSL + if( pUrlData->isHttps ){ + return ssl_errmsg(); + } + #endif + return socket_errmsg(); +} + +/* +** Retrieve send/receive counts from the transport layer. If "resetFlag" +** is true, then reset the counts. +*/ +void transport_stats(i64 *pnSent, i64 *pnRcvd, int resetFlag){ + if( pnSent ) *pnSent = transport.nSent; + if( pnRcvd ) *pnRcvd = transport.nRcvd; + if( resetFlag ){ + transport.nSent = 0; + transport.nRcvd = 0; + } +} + +/* +** Check zFossil to see if it is a reasonable "fossil" command to +** run on the server. Do not allow an attacker to substitute something +** like "/bin/rm". +*/ +static int is_safe_fossil_command(const char *zFossil){ + static const char *const azSafe[] = { "*/fossil", "*/fossil.exe", "*/echo" }; + int i; + for(i=0; i<(int)(sizeof(azSafe)/sizeof(azSafe[0])); i++){ + if( sqlite3_strglob(azSafe[i], zFossil)==0 ) return 1; + if( strcmp(azSafe[i]+2, zFossil)==0 ) return 1; + } + return 0; +} + +/* +** Default SSH command +*/ +#if 0 /* was: defined(_WIN32). Windows generally has ssh now. */ +static const char zDefaultSshCmd[] = "plink -ssh"; +#else +static const char zDefaultSshCmd[] = "ssh -e none"; +#endif + +/* +** Initialize a Blob to the name of the configured SSH command. +*/ +void transport_ssh_command(Blob *p){ + char *zSsh; /* The base SSH command */ + zSsh = g.zSshCmd; + if( zSsh==0 || zSsh[0]==0 ){ + zSsh = db_get("ssh-command", zDefaultSshCmd); + } + blob_init(p, zSsh, -1); +} + +/* +** SSH initialization of the transport layer +*/ +int transport_ssh_open(UrlData *pUrlData){ + /* For SSH we need to create and run SSH fossil http + ** to talk to the remote machine. + */ + Blob zCmd; /* The SSH command */ + char *zHost; /* The host name to contact */ + + fossil_free(g.zIpAddr); + g.zIpAddr = fossil_strdup(pUrlData->name); + transport_ssh_command(&zCmd); + if( pUrlData->port!=pUrlData->dfltPort && pUrlData->port ){ + blob_appendf(&zCmd, " -p %d", pUrlData->port); + } + blob_appendf(&zCmd, " -T --"); /* End of switches */ + if( pUrlData->user && pUrlData->user[0] ){ + zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name); + blob_append_escaped_arg(&zCmd, zHost, 0); + fossil_free(zHost); + }else{ + blob_append_escaped_arg(&zCmd, pUrlData->name, 0); + } + if( (pUrlData->flags & URL_SSH_EXE)!=0 + && !is_safe_fossil_command(pUrlData->fossil) + ){ + fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on " + "the server.", pUrlData->fossil); + } + if( (pUrlData->flags & URL_SSH_EXE)==0 + && (pUrlData->flags & URL_SSH_PATH)!=0 + ){ + ssh_add_path_argument(&zCmd); + } + blob_append_escaped_arg(&zCmd, pUrlData->fossil, 1); + blob_append(&zCmd, " test-http", 10); + if( pUrlData->path && pUrlData->path[0] ){ + blob_append_escaped_arg(&zCmd, pUrlData->path, 1); + }else{ + fossil_fatal("ssh:// URI does not specify a path to the repository"); + } + if( g.fSshTrace || g.fHttpTrace ){ + fossil_print("RUN %s\n", blob_str(&zCmd)); /* Show the whole SSH command */ + } + popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid, 0); + if( sshPid==0 ){ + socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd); + } + blob_reset(&zCmd); + return sshPid==0; +} + +/* +** Return true if the filename given matches the pattern that means +** it is a test-reply. +*/ +static int transport_is_test(const char *zFilename){ + return sqlite3_strglob("*.http-test",zFilename)==0; +} + +/* +** Open a connection to the server. The server is defined by the following +** variables: +** +** pUrlData->name Name of the server. Ex: fossil-scm.org +** +** pUrlData->port TCP/IP port. Ex: 80 +** +** pUrlData->isHttps Use TLS for the connection +** +** pUrlData->isSsh Use an SSH connection to the server. name is +** the SSH host name. subpath is the server +** method name (ex: timeline) +** +** pUrlData->isFile The .name is a filename in the local filesystem. +** If that file is a Fossil repo, then launch +** "fossil http" as a subprocess to handle the request. +** Or if filename ends with ".http-test" then the +** content of that file becomes the HTTP reply. +** (The latter is used for testing only.) +** +** pUrlData->subpath The method name on the request for SSH and FILE +** +** Return the number of errors. +*/ +int transport_open(UrlData *pUrlData){ + int rc = 0; + if( transport.isOpen==0 ){ + if( pUrlData->isSsh ){ + rc = transport_ssh_open(pUrlData); + if( rc==0 ) transport.isOpen = 1; + }else if( pUrlData->isHttps ){ +#ifdef FOSSIL_ENABLE_SSL + rc = ssl_open_client(pUrlData); + if( rc==0 ) transport.isOpen = 1; +#else + socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support"); + rc = 1; +#endif + }else if( pUrlData->isFile ){ + if( !file_isfile(pUrlData->name, ExtFILE) ){ + fossil_fatal("no such file: \"%s\"", pUrlData->name); + } + transport.zOutFile = fossil_temp_filename(); + transport.zInFile = fossil_temp_filename(); + if( !db_looks_like_a_repository(pUrlData->name) ){ + if( transport_is_test(pUrlData->name) ){ + /* If the URI filename ends with ".http-test" then the content + ** of that file becomes the HTTP reply and the request is stored + ** in a file with the same name but with ".request" added to the + ** end. Used for testing only */ + transport.bTest = 1; + sqlite3_free(transport.zOutFile); + transport.zOutFile = sqlite3_mprintf("%s.request",pUrlData->name); + sqlite3_free(transport.zInFile); + transport.zInFile = sqlite3_mprintf("%s",pUrlData->name); + }else{ + fossil_fatal("not a fossil repository: \"%s\"", pUrlData->name); + } + } + transport.pFile = fossil_fopen(transport.zOutFile, "wb"); + if( transport.pFile==0 ){ + fossil_fatal("cannot output temporary file: %s", transport.zOutFile); + } + transport.isOpen = 1; + }else{ + rc = socket_open(pUrlData); + if( rc==0 ) transport.isOpen = 1; + } + } + return rc; +} + +/* +** Close the current connection +*/ +void transport_close(UrlData *pUrlData){ + if( transport.isOpen ){ + free(transport.pBuf); + transport.pBuf = 0; + transport.nAlloc = 0; + transport.nUsed = 0; + transport.iCursor = 0; + if( transport.pLog ){ + fclose(transport.pLog); + transport.pLog = 0; + } + if( pUrlData->isSsh ){ + transport_ssh_close(); + }else if( pUrlData->isHttps ){ + #ifdef FOSSIL_ENABLE_SSL + ssl_close_client(); + #endif + }else if( pUrlData->isFile ){ + if( transport.pFile ){ + fclose(transport.pFile); + transport.pFile = 0; + } + if( !transport.bTest ){ + file_delete(transport.zInFile); + file_delete(transport.zOutFile); + } + sqlite3_free(transport.zInFile); + sqlite3_free(transport.zOutFile); + }else{ + socket_close(); + } + transport.isOpen = 0; + } +} + +/* +** Send content over the wire. +*/ +void transport_send(UrlData const *pUrlData, const Blob *toSend){ + char *z = blob_buffer(toSend); + int n = blob_size(toSend); + transport.nSent += n; + if( pUrlData->isSsh ){ + fwrite(z, 1, n, sshOut); + fflush(sshOut); + }else if( pUrlData->isHttps ){ +#ifdef FOSSIL_ENABLE_SSL + int sent; + while( n>0 ){ + sent = ssl_send(0, z, n); + /* printf("Sent %d of %d bytes\n", sent, n); fflush(stdout); */ + if( sent<=0 ) break; + n -= sent; + } +#endif + }else if( pUrlData->isFile ){ + fwrite(z, 1, n, transport.pFile); + }else{ + int sent; + while( n>0 ){ + sent = socket_send(0, z, n); + /* printf("Sent %d of %d bytes\n", sent, n); fflush(stdout); */ + if( sent<=0 ) break; + n -= sent; + } + } +} + +/* +** This routine is called when the outbound message is complete and +** it is time to begin receiving a reply. +** +** This is a no-op for http:, https:, and ssh:. It only does this +** for file:. If the file is a Fossil database, then an instance of +** "fossil http" is invoked in a subprocess to handle the request. +** Or if the filename ends with ".http-test" then the text of that +** file becomes the reply. The ".http-test" hack is for testing purposes. +*/ +void transport_flip(UrlData *pUrlData){ + if( pUrlData->isFile ){ + char *zCmd; + fclose(transport.pFile); + if( !transport.bTest ){ + zCmd = mprintf("%$ http --in %$ --out %$ --ipaddr 127.0.0.1" + " %$ --localauth", + g.nameOfExe, transport.zOutFile, transport.zInFile, pUrlData->name + ); + if( g.fHttpTrace ) fossil_print("RUN %s\n", zCmd); + fossil_system(zCmd); + free(zCmd); + } + transport.pFile = fossil_fopen(transport.zInFile, "rb"); + if( transport.pFile==0 ){ + fossil_fatal("unable to open HTTP reply: \"%s\"",transport.zInFile); + } + } +} + +/* +** Log all input to a file. The transport layer will take responsibility +** for closing the log file when it is done. +*/ +void transport_log(FILE *pLog){ + if( transport.pLog ){ + fclose(transport.pLog); + transport.pLog = 0; + } + transport.pLog = pLog; +} + +/* +** This routine is called when the inbound message has been received +** and it is time to start sending again. +*/ +void transport_rewind(UrlData *pUrlData){ + if( pUrlData->isFile ){ + transport_close(pUrlData); + } +} + +/* +** Read N bytes of content directly from the wire and write into +** the buffer. +*/ +static int transport_fetch(UrlData *pUrlData, char *zBuf, int N){ + int got; + if( pUrlData->isSsh ){ + int x; + int wanted = N; + got = 0; + while( wanted>0 ){ + x = read(sshIn, &zBuf[got], wanted); + if( x<=0 ) break; + got += x; + wanted -= x; + } + }else if( pUrlData->isHttps ){ + #ifdef FOSSIL_ENABLE_SSL + got = ssl_receive(0, zBuf, N); + #else + got = 0; + #endif + }else if( pUrlData->isFile ){ + got = fread(zBuf, 1, N, transport.pFile); + }else{ + got = socket_receive(0, zBuf, N, 0); + } + /* printf("received %d of %d bytes\n", got, N); fflush(stdout); */ + if( transport.pLog ){ + fwrite(zBuf, 1, got, transport.pLog); + fflush(transport.pLog); + } + return got; +} + +/* +** Read N bytes of content from the wire and store in the supplied buffer. +** Return the number of bytes actually received. +*/ +int transport_receive(UrlData *pUrlData, char *zBuf, int N){ + int onHand; /* Bytes current held in the transport buffer */ + int nByte = 0; /* Bytes of content received */ + + onHand = transport.nUsed - transport.iCursor; + if( g.fSshTrace){ + printf("Reading %d bytes with %d on hand... ", N, onHand); + fflush(stdout); + } + if( onHand>0 ){ + int toMove = onHand; + if( toMove>N ) toMove = N; + /* printf("bytes on hand: %d of %d\n", toMove, N); fflush(stdout); */ + memcpy(zBuf, &transport.pBuf[transport.iCursor], toMove); + transport.iCursor += toMove; + if( transport.iCursor>=transport.nUsed ){ + transport.nUsed = 0; + transport.iCursor = 0; + } + N -= toMove; + zBuf += toMove; + nByte += toMove; + } + if( N>0 ){ + int got = transport_fetch(pUrlData, zBuf, N); + if( got>0 ){ + nByte += got; + transport.nRcvd += got; + } + } + if( g.fSshTrace ) printf("Got %d bytes\n", nByte); + return nByte; +} + +/* +** Load up to N new bytes of content into the transport.pBuf buffer. +** The buffer itself might be moved. And the transport.iCursor value +** might be reset to 0. +*/ +static void transport_load_buffer(UrlData *pUrlData, int N){ + int i, j; + if( transport.nAlloc==0 ){ + transport.nAlloc = N; + transport.pBuf = fossil_malloc( N ); + transport.iCursor = 0; + transport.nUsed = 0; + } + if( transport.iCursor>0 ){ + for(i=0, j=transport.iCursor; j<transport.nUsed; i++, j++){ + transport.pBuf[i] = transport.pBuf[j]; + } + transport.nUsed -= transport.iCursor; + transport.iCursor = 0; + } + if( transport.nUsed + N > transport.nAlloc ){ + char *pNew; + transport.nAlloc = transport.nUsed + N; + pNew = fossil_realloc(transport.pBuf, transport.nAlloc); + transport.pBuf = pNew; + } + if( N>0 ){ + i = transport_fetch(pUrlData, &transport.pBuf[transport.nUsed], N); + if( i>0 ){ + transport.nRcvd += i; + transport.nUsed += i; + } + } +} + +/* +** Fetch a single line of input where a line is all text up to the next +** \n character or until the end of input. Remove all trailing whitespace +** from the received line and zero-terminate the result. Return a pointer +** to the line. +** +** Each call to this routine potentially overwrites the returned buffer. +*/ +char *transport_receive_line(UrlData *pUrlData){ + int i; + int iStart; + + i = iStart = transport.iCursor; + while(1){ + if( i >= transport.nUsed ){ + transport_load_buffer(pUrlData, pUrlData->isSsh ? 2 : 1000); + i -= iStart; + iStart = 0; + if( i >= transport.nUsed ){ + transport.pBuf[i] = 0; + transport.iCursor = i; + break; + } + } + if( transport.pBuf[i]=='\n' ){ + transport.iCursor = i+1; + while( i>=iStart && fossil_isspace(transport.pBuf[i]) ){ + transport.pBuf[i] = 0; + i--; + } + break; + } + i++; + } + if( g.fSshTrace ) printf("Got line: [%s]\n", &transport.pBuf[iStart]); + return &transport.pBuf[iStart]; +} + +/* +** Global transport shutdown +*/ +void transport_global_shutdown(UrlData *pUrlData){ + if( pUrlData->isSsh ){ + transport_ssh_close(); + } + if( pUrlData->isHttps ){ + #ifdef FOSSIL_ENABLE_SSL + ssl_global_shutdown(); + #endif + }else{ + socket_global_shutdown(); + } +} + +/* +** Close SSH transport. +*/ +void transport_ssh_close(void){ + if( sshPid ){ + /*printf("Closing SSH tunnel: ");*/ + fflush(stdout); + pclose2(sshIn, sshOut, sshPid); + sshPid = 0; + } +} |
