/* ** Copyright (c) 2007 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 file contains code that implements the client-side HTTP protocol */ #include "config.h" #include "http.h" #include #ifdef _WIN32 #include #ifndef isatty #define isatty(d) _isatty(d) #endif #ifndef fileno #define fileno(s) _fileno(s) #endif #endif #if INTERFACE /* ** Bits of the mHttpFlags parameter to http_exchange() */ #define HTTP_USE_LOGIN 0x00001 /* Add a login card to the sync message */ #define HTTP_GENERIC 0x00002 /* Generic HTTP request */ #define HTTP_VERBOSE 0x00004 /* HTTP status messages */ #define HTTP_QUIET 0x00008 /* No surplus output */ #define HTTP_NOCOMPRESS 0x00010 /* Omit payload compression */ #endif /* Maximum number of HTTP Authorization attempts */ #define MAX_HTTP_AUTH 2 /* Keep track of HTTP Basic Authorization failures */ static int fSeenHttpAuth = 0; /* ** Construct the "login" card with the client credentials. ** ** login LOGIN NONCE SIGNATURE ** ** The LOGIN is the user id of the client. NONCE is the sha1 checksum ** of all payload that follows the login card. SIGNATURE is the sha1 ** checksum of the nonce followed by the user password. ** ** Write the constructed login card into pLogin. pLogin is initialized ** by this routine. */ static void http_build_login_card(Blob *pPayload, Blob *pLogin){ Blob nonce; /* The nonce */ const char *zLogin; /* The user login name */ const char *zPw; /* The user password */ Blob pw; /* The nonce with user password appended */ Blob sig; /* The signature field */ blob_zero(pLogin); if( g.url.user==0 || fossil_strcmp(g.url.user, "anonymous")==0 ){ return; /* If no login card for users "nobody" and "anonymous" */ } if( g.url.isSsh ){ return; /* If no login card for SSH: */ } blob_zero(&nonce); blob_zero(&pw); sha1sum_blob(pPayload, &nonce); blob_copy(&pw, &nonce); zLogin = g.url.user; if( g.url.passwd ){ zPw = g.url.passwd; }else if( g.cgiOutput ){ /* Password failure while doing a sync from the web interface */ cgi_printf("*** incorrect or missing password for user %h\n", zLogin); zPw = 0; }else{ /* Password failure while doing a sync from the command-line interface */ url_prompt_for_password(); zPw = g.url.passwd; } /* The login card wants the SHA1 hash of the password, so convert the ** password to its SHA1 hash if it isn't already a SHA1 hash. */ /* fossil_print("\nzPw=[%s]\n", zPw); // TESTING ONLY */ if( zPw && zPw[0] ) zPw = sha1_shared_secret(zPw, zLogin, 0); blob_append(&pw, zPw, -1); sha1sum_blob(&pw, &sig); blob_appendf(pLogin, "login %F %b %b\n", zLogin, &nonce, &sig); blob_reset(&pw); blob_reset(&sig); blob_reset(&nonce); } /* ** Construct an appropriate HTTP request header. Write the header ** into pHdr. This routine initializes the pHdr blob. pPayload is ** the complete payload (including the login card) already compressed. */ static void http_build_header( Blob *pPayload, /* the payload that will be sent */ Blob *pHdr, /* construct the header here */ const char *zAltMimetype /* Alternative mimetype */ ){ int nPayload = pPayload ? blob_size(pPayload) : 0; blob_zero(pHdr); blob_appendf(pHdr, "%s %s%s HTTP/1.0\r\n", nPayload>0 ? "POST" : "GET", g.url.path, g.url.path[0]==0 ? "/" : ""); if( g.url.proxyAuth ){ blob_appendf(pHdr, "Proxy-Authorization: %s\r\n", g.url.proxyAuth); } if( g.zHttpAuth && g.zHttpAuth[0] ){ const char *zCredentials = g.zHttpAuth; char *zEncoded = encode64(zCredentials, -1); blob_appendf(pHdr, "Authorization: Basic %s\r\n", zEncoded); fossil_free(zEncoded); } blob_appendf(pHdr, "Host: %s\r\n", g.url.hostname); blob_appendf(pHdr, "User-Agent: %s\r\n", get_user_agent()); if( g.url.isSsh ) blob_appendf(pHdr, "X-Fossil-Transport: SSH\r\n"); if( nPayload ){ if( zAltMimetype ){ blob_appendf(pHdr, "Content-Type: %s\r\n", zAltMimetype); }else if( g.fHttpTrace ){ blob_appendf(pHdr, "Content-Type: application/x-fossil-debug\r\n"); }else{ blob_appendf(pHdr, "Content-Type: application/x-fossil\r\n"); } blob_appendf(pHdr, "Content-Length: %d\r\n", blob_size(pPayload)); } blob_append(pHdr, "\r\n", 2); } /* ** Use Fossil credentials for HTTP Basic Authorization prompt */ static int use_fossil_creds_for_httpauth_prompt(void){ Blob x; char c; prompt_user("Use Fossil username and password (y/N)? ", &x); c = blob_str(&x)[0]; blob_reset(&x); return ( c=='y' || c=='Y' ); } /* ** Prompt to save HTTP Basic Authorization information */ static int save_httpauth_prompt(void){ Blob x; char c; if( (g.url.flags & URL_REMEMBER)==0 ) return 0; prompt_user("Remember Basic Authorization credentials (Y/n)? ", &x); c = blob_str(&x)[0]; blob_reset(&x); return ( c!='n' && c!='N' ); } /* ** Get the HTTP Basic Authorization credentials from the user ** when 401 is received. */ char *prompt_for_httpauth_creds(void){ Blob x; char *zUser; char *zPw; char *zPrompt; char *zHttpAuth = 0; if( !isatty(fileno(stdin)) ) return 0; zPrompt = mprintf("\n%s authorization required by\n%s\n", g.url.isHttps==1 ? "Encrypted HTTPS" : "Unencrypted HTTP", g.url.canonical); fossil_print("%s", zPrompt); free(zPrompt); if ( g.url.user && g.url.passwd && use_fossil_creds_for_httpauth_prompt() ){ zHttpAuth = mprintf("%s:%s", g.url.user, g.url.passwd); }else{ prompt_user("Basic Authorization user: ", &x); zUser = mprintf("%b", &x); zPrompt = mprintf("HTTP password for %b: ", &x); blob_reset(&x); prompt_for_password(zPrompt, &x, 0); zPw = mprintf("%b", &x); zHttpAuth = mprintf("%s:%s", zUser, zPw); free(zUser); free(zPw); free(zPrompt); blob_reset(&x); } if( save_httpauth_prompt() ){ set_httpauth(zHttpAuth); } return zHttpAuth; } /* ** Send content pSend to the the server identified by g.url using the ** external program given by g.zHttpCmd. Capture the reply from that ** program and load it into pReply. ** ** This routine implements the --transport-command option for "fossil sync". */ static int http_exchange_external( Blob *pSend, /* Message to be sent */ Blob *pReply, /* Write the reply here */ int mHttpFlags, /* Flags. See above */ const char *zAltMimetype /* Alternative mimetype if not NULL */ ){ char *zUplink; char *zDownlink; char *zCmd; int rc; zUplink = fossil_temp_filename(); zDownlink = fossil_temp_filename(); zCmd = mprintf("%s %$ %$ %$", g.zHttpCmd, g.url.canonical,zUplink,zDownlink); blob_write_to_file(pSend, zUplink); if( g.fHttpTrace ){ fossil_print("RUN: %s\n", zCmd); } rc = fossil_system(zCmd); if( rc ){ fossil_warning("Transport command failed: %s\n", zCmd); } fossil_free(zCmd); file_delete(zUplink); if( file_size(zDownlink, ExtFILE)<0 ){ blob_zero(pReply); }else{ blob_read_from_file(pReply, zDownlink, ExtFILE); file_delete(zDownlink); } return rc; } /* ** Sign the content in pSend, compress it, and send it to the server ** via HTTP or HTTPS. Get a reply, uncompress the reply, and store the reply ** in pRecv. pRecv is assumed to be uninitialized when ** this routine is called - this routine will initialize it. ** ** The server address is contain in the "g" global structure. The ** url_parse() routine should have been called prior to this routine ** in order to fill this structure appropriately. */ int http_exchange( Blob *pSend, /* Message to be sent */ Blob *pReply, /* Write the reply here */ int mHttpFlags, /* Flags. See above */ int maxRedirect, /* Max number of redirects */ const char *zAltMimetype /* Alternative mimetype if not NULL */ ){ Blob login; /* The login card */ Blob payload; /* The complete payload including login card */ Blob hdr; /* The HTTP request header */ int closeConnection; /* True to close the connection when done */ int iLength; /* Expected length of the reply payload */ int iRecvLen; /* Received length of the reply payload */ int rc = 0; /* Result code */ int iHttpVersion; /* Which version of HTTP protocol server uses */ char *zLine; /* A single line of the reply header */ int i; /* Loop counter */ int isError = 0; /* True if the reply is an error message */ int isCompressed = 1; /* True if the reply is compressed */ if( g.zHttpCmd!=0 ){ /* Handle the --transport-command option for "fossil sync" and similar */ return http_exchange_external(pSend,pReply,mHttpFlags,zAltMimetype); } if( transport_open(&g.url) ){ fossil_warning("%s", transport_errmsg(&g.url)); return 1; } /* Construct the login card and prepare the complete payload */ if( blob_size(pSend)==0 ){ blob_zero(&payload); }else{ blob_zero(&login); if( mHttpFlags & HTTP_USE_LOGIN ) http_build_login_card(pSend, &login); if( g.fHttpTrace || (mHttpFlags & HTTP_NOCOMPRESS)!=0 ){ payload = login; blob_append(&payload, blob_buffer(pSend), blob_size(pSend)); }else{ blob_compress2(&login, pSend, &payload); blob_reset(&login); } } /* Construct the HTTP request header */ http_build_header(&payload, &hdr, zAltMimetype); /* When tracing, write the transmitted HTTP message both to standard ** output and into a file. The file can then be used to drive the ** server-side like this: ** ** ./fossil test-http 4 && fossil_strcmp(&zLine[j-4],"/xfer")==0 ){ j -= 4; zLine[j] = 0; } if( (mHttpFlags & HTTP_QUIET)==0 ){ fossil_print("redirect with status %d to %s\n", rc, &zLine[i]); } if( g.url.isFile || g.url.isSsh ){ fossil_warning("cannot redirect from %s to %s", g.url.canonical, &zLine[i]); goto write_err; } wasHttps = g.url.isHttps; url_parse(&zLine[i], 0); if( wasHttps && !g.url.isHttps ){ fossil_warning("cannot redirect from HTTPS to HTTP"); goto write_err; } if( g.url.isSsh || g.url.isFile ){ fossil_warning("cannot redirect to %s", &zLine[i]); goto write_err; } transport_close(&g.url); transport_global_shutdown(&g.url); fSeenHttpAuth = 0; if( g.zHttpAuth ) free(g.zHttpAuth); g.zHttpAuth = get_httpauth(); if( rc==301 || rc==308 ) url_remember(); return http_exchange(pSend, pReply, mHttpFlags, maxRedirect, zAltMimetype); }else if( fossil_strnicmp(zLine, "content-type: ", 14)==0 ){ if( fossil_strnicmp(&zLine[14], "application/x-fossil-debug", -1)==0 ){ isCompressed = 0; }else if( fossil_strnicmp(&zLine[14], "application/x-fossil-uncompressed", -1)==0 ){ isCompressed = 0; }else{ if( (mHttpFlags & HTTP_GENERIC)==0 && fossil_strnicmp(&zLine[14], "application/x-fossil", -1)!=0 ){ isError = 1; } } } } if( iLength<0 ){ fossil_warning("server did not reply"); goto write_err; } if( rc!=200 ){ fossil_warning("\"location:\" missing from %d redirect reply", rc); goto write_err; } /* ** Extract the reply payload that follows the header */ blob_zero(pReply); blob_resize(pReply, iLength); iRecvLen = transport_receive(&g.url, blob_buffer(pReply), iLength); if( iRecvLen != iLength ){ fossil_warning("response truncated: got %d bytes of %d", iRecvLen, iLength); goto write_err; } blob_resize(pReply, iLength); if( isError ){ char *z; int i, j; z = blob_str(pReply); for(i=j=0; z[i]; i++, j++){ if( z[i]=='<' ){ while( z[i] && z[i]!='>' ) i++; if( z[i]==0 ) break; } z[j] = z[i]; } z[j] = 0; fossil_warning("server sends error: %s", z); goto write_err; } if( isCompressed ) blob_uncompress(pReply, pReply); /* ** Close the connection to the server if appropriate. ** ** FIXME: There is some bug in the lower layers that prevents the ** connection from remaining open. The easiest fix for now is to ** simply close and restart the connection for each round-trip. ** ** For SSH we will leave the connection open. */ if( ! g.url.isSsh ) closeConnection = 1; /* FIX ME */ if( closeConnection ){ transport_close(&g.url); }else{ transport_rewind(&g.url); } return 0; /* ** Jump to here if an error is seen. */ write_err: transport_close(&g.url); return 1; } /* ** COMMAND: test-httpmsg ** ** Usage: %fossil test-httpmsg ?OPTIONS? URL ?PAYLOAD? ?OUTPUT? ** ** Send an HTTP message to URL and get the reply. PAYLOAD is a file containing ** the payload, or "-" to read payload from standard input. a POST message ** is sent if PAYLOAD is specified and is non-empty. If PAYLOAD is omitted ** or is an empty file, then a GET message is sent. ** ** If a second filename (OUTPUT) is given after PAYLOAD, then the reply ** is written into that second file instead of being written on standard ** output. Use the "--out OUTPUT" option to specify an output file for ** a GET request where there is no PAYLOAD. ** ** Options: ** ** --compress Use ZLIB compression on the payload ** --mimetype TYPE Mimetype of the payload ** --out FILE Store the reply in FILE ** -v Verbose output ** --xfer PAYLOAD in a Fossil xfer protocol message */ void test_httpmsg_command(void){ const char *zMimetype; const char *zInFile; const char *zOutFile; Blob in, out; unsigned int mHttpFlags = HTTP_GENERIC|HTTP_NOCOMPRESS; zMimetype = find_option("mimetype",0,1); zOutFile = find_option("out","o",1); if( find_option("verbose","v",0)!=0 ) mHttpFlags |= HTTP_VERBOSE; if( find_option("compress",0,0)!=0 ) mHttpFlags &= ~HTTP_NOCOMPRESS; if( find_option("xfer",0,0)!=0 ){ mHttpFlags |= HTTP_USE_LOGIN; mHttpFlags &= ~HTTP_GENERIC; } db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_ANY_SCHEMA, 0); verify_all_options(); if( g.argc<3 || g.argc>5 ){ usage("URL ?PAYLOAD? ?OUTPUT?"); } zInFile = g.argc>=4 ? g.argv[3] : 0; if( g.argc==5 ){ if( zOutFile ){ fossil_fatal("output file specified twice: \"--out %s\" and \"%s\"", zOutFile, g.argv[4]); } zOutFile = g.argv[4]; } url_parse(g.argv[2], 0); if( g.url.protocol[0]!='h' ){ fossil_fatal("the %s command supports only http: and https:", g.argv[1]); } if( zInFile ){ blob_read_from_file(&in, zInFile, ExtFILE); if( zMimetype==0 && (mHttpFlags & HTTP_GENERIC)!=0 ){ if( fossil_strcmp(zInFile,"-")==0 ){ zMimetype = "application/x-unknown"; }else{ zMimetype = mimetype_from_name(zInFile); } } }else{ blob_init(&in, 0, 0); } blob_init(&out, 0, 0); if( (mHttpFlags & HTTP_VERBOSE)==0 && zOutFile==0 ){ zOutFile = "-"; mHttpFlags |= HTTP_QUIET; } http_exchange(&in, &out, mHttpFlags, 4, zMimetype); if( zOutFile ) blob_write_to_file(&out, zOutFile); blob_zero(&in); blob_zero(&out); }