/* ** 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 */ } transport = { 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(void){ #ifdef FOSSIL_ENABLE_SSL if( g.urlIsHttps ){ 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; } } /* ** Read text from sshIn. Zero-terminate and remove trailing ** whitespace. */ static void sshin_read(char *zBuf, int szBuf){ int got; zBuf[0] = 0; got = read(sshIn, zBuf, szBuf-1); while( got>=0 ){ zBuf[got] = 0; if( got==0 || !fossil_isspace(zBuf[got-1]) ) break; got--; } } /* ** Default SSH command */ #ifdef __MINGW32__ static char zDefaultSshCmd[] = "ssh -T"; #else static char zDefaultSshCmd[] = "ssh -e none -T"; #endif /* ** Global initialization of the transport layer */ void transport_global_startup(void){ if( g.urlIsSsh ){ /* Only SSH requires a global initialization. For SSH we need to create ** and run an SSH command to talk to the remote machine. */ const char *zSsh; /* The base SSH command */ Blob zCmd; /* The SSH command */ char *zHost; /* The host name to contact */ char *zIn; /* An input line received back from remote */ zSsh = db_get("ssh-command", zDefaultSshCmd); blob_init(&zCmd, zSsh, -1); if( g.urlPort!=g.urlDfltPort ){ #ifdef __MINGW32__ blob_appendf(&zCmd, " -P %d", g.urlPort); #else blob_appendf(&zCmd, " -p %d", g.urlPort); #endif } printf("%s", blob_str(&zCmd)); /* Show the base of the SSH command */ if( g.urlUser && g.urlUser[0] ){ zHost = mprintf("%s@%s", g.urlUser, g.urlName); #ifdef __MINGW32__ /* Only win32 (and specifically PLINK.EXE) support the -pw option */ if( g.urlPasswd && g.urlPasswd[0] ){ Blob pw; blob_zero(&pw); if( g.urlPasswd[0]=='*' ){ char *zPrompt; zPrompt = mprintf("Password for [%s]: ", zHost); prompt_for_password(zPrompt, &pw, 0); free(zPrompt); }else{ blob_init(&pw, g.urlPasswd, -1); } blob_append(&zCmd, " -pw ", -1); shell_escape(&zCmd, blob_str(&pw)); blob_reset(&pw); printf(" -pw ********"); /* Do not show the password text */ } #endif }else{ zHost = mprintf("%s", g.urlName); } blob_append(&zCmd, " ", 1); shell_escape(&zCmd, zHost); printf(" %s\n", zHost); /* Show the conclusion of the SSH command */ free(zHost); popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid); if( sshPid==0 ){ fossil_fatal("cannot start ssh tunnel using [%b]", &zCmd); } blob_reset(&zCmd); /* Send an "echo" command to the other side to make sure that the ** connection is up and working. */ fprintf(sshOut, "echo test\n"); fflush(sshOut); zIn = fossil_malloc(16000); sshin_read(zIn, 16000); if( memcmp(zIn, "test", 4)!=0 ){ pclose2(sshIn, sshOut, sshPid); fossil_fatal("ssh connection failed: [%s]", zIn); } fossil_free(zIn); } } /* ** Open a connection to the server. The server is defined by the following ** global variables: ** ** g.urlName Name of the server. Ex: www.fossil-scm.org ** g.urlPort TCP/IP port. Ex: 80 ** g.urlIsHttps Use TLS for the connection ** ** Return the number of errors. */ int transport_open(void){ int rc = 0; if( transport.isOpen==0 ){ if( g.urlIsSsh ){ Blob cmd; blob_zero(&cmd); shell_escape(&cmd, g.urlFossil); blob_append(&cmd, " test-http ", -1); shell_escape(&cmd, g.urlPath); /* printf("%s\n", blob_str(&cmd)); fflush(stdout); */ fprintf(sshOut, "%s\n", blob_str(&cmd)); fflush(sshOut); blob_reset(&cmd); }else if( g.urlIsHttps ){ #ifdef FOSSIL_ENABLE_SSL rc = ssl_open(); if( rc==0 ) transport.isOpen = 1; #else socket_set_errmsg("HTTPS: Fossil has been compiled without SSL support"); rc = 1; #endif }else if( g.urlIsFile ){ sqlite3_uint64 iRandId; sqlite3_randomness(sizeof(iRandId), &iRandId); transport.zOutFile = mprintf("%s-%llu-out.http", g.zRepositoryName, iRandId); transport.zInFile = mprintf("%s-%llu-in.http", g.zRepositoryName, iRandId); transport.pFile = fopen(transport.zOutFile, "wb"); if( transport.pFile==0 ){ fossil_fatal("cannot output temporary file: %s", transport.zOutFile); } transport.isOpen = 1; }else{ rc = socket_open(); if( rc==0 ) transport.isOpen = 1; } } return rc; } /* ** Close the current connection */ void transport_close(void){ 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( g.urlIsSsh ){ /* No-op */ }else if( g.urlIsHttps ){ #ifdef FOSSIL_ENABLE_SSL ssl_close(); #endif }else if( g.urlIsFile ){ if( transport.pFile ){ fclose(transport.pFile); transport.pFile = 0; } unlink(transport.zInFile); unlink(transport.zOutFile); free(transport.zInFile); free(transport.zOutFile); }else{ socket_close(); } transport.isOpen = 0; } } /* ** Send content over the wire. */ void transport_send(Blob *toSend){ char *z = blob_buffer(toSend); int n = blob_size(toSend); transport.nSent += n; if( g.urlIsSsh ){ int sent; sent = fwrite(z, 1, n, sshOut); fflush(sshOut); /* printf("sent %d of %d bytes\n", sent, n); fflush(stdout); */ }else if( g.urlIsHttps ){ #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( g.urlIsFile ){ 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 being recieving a reply. */ void transport_flip(void){ if( g.urlIsSsh ){ fprintf(sshOut, "\n\n"); }else if( g.urlIsFile ){ char *zCmd; fclose(transport.pFile); zCmd = mprintf("\"%s\" http \"%s\" \"%s\" \"%s\" 127.0.0.1 --localauth", fossil_nameofexe(), g.urlName, transport.zOutFile, transport.zInFile ); fossil_system(zCmd); free(zCmd); transport.pFile = fopen(transport.zInFile, "rb"); } } /* ** 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(void){ if( g.urlIsFile ){ transport_close(); } } /* ** Read N bytes of content directly from the wire and write into ** the buffer. */ static int transport_fetch(char *zBuf, int N){ int got; if( sshIn ){ int x; int wanted = N; got = 0; /* printf("want %d bytes...\n", wanted); fflush(stdout); */ while( wanted>0 ){ x = read(sshIn, &zBuf[got], wanted); if( x<=0 ) break; got += x; wanted -= x; } }else if( g.urlIsHttps ){ #ifdef FOSSIL_ENABLE_SSL got = ssl_receive(0, zBuf, N); #else got = 0; #endif }else if( g.urlIsFile ){ got = fread(zBuf, 1, N, transport.pFile); }else{ got = socket_receive(0, zBuf, N); } /* 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(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; /* printf("request %d with %d on hand\n", 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(zBuf, N); if( got>0 ){ nByte += got; transport.nRcvd += got; } } 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(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.nAlloc ){ char *pNew; transport.nAlloc = transport.nUsed + N; pNew = fossil_realloc(transport.pBuf, transport.nAlloc); transport.pBuf = pNew; } if( N>0 ){ i = transport_fetch(&transport.pBuf[transport.nUsed], N); if( i>0 ){ 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(void){ int i; int iStart; i = iStart = transport.iCursor; while(1){ if( i >= transport.nUsed ){ transport_load_buffer(g.urlIsSsh ? 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++; } /* printf("Got line: [%s]\n", &transport.pBuf[iStart]); */ return &transport.pBuf[iStart]; } void transport_global_shutdown(void){ if( g.urlIsSsh && sshPid ){ printf("Closing SSH tunnel: "); fflush(stdout); pclose2(sshIn, sshOut, sshPid); sshPid = 0; } if( g.urlIsHttps ){ #ifdef FOSSIL_ENABLE_SSL ssl_global_shutdown(); #endif }else{ socket_global_shutdown(); } }