Index: src/blob.c ================================================================== --- src/blob.c +++ src/blob.c @@ -967,10 +967,34 @@ } }else{ blob_resize(pBlob, nToRead); n = fread(blob_buffer(pBlob), 1, nToRead, in); blob_resize(pBlob, n); + } + return blob_size(pBlob); +} + +/* +** Initialize a blob to the data read from HTTP input. Return +** the number of bytes read into the blob. Any prior content +** of the blob is discarded, not freed. +*/ +int blob_read_from_cgi(Blob *pBlob, int nToRead){ + size_t n; + blob_zero(pBlob); + if( nToRead<0 ){ + char zBuf[10000]; + while( !cgi_feof() ){ + n = cgi_fread(zBuf, sizeof(zBuf)); + if( n>0 ){ + blob_append(pBlob, zBuf, n); + } + } + }else{ + blob_resize(pBlob, nToRead); + n = cgi_fread(blob_buffer(pBlob), nToRead); + blob_resize(pBlob, n); } return blob_size(pBlob); } /* Index: src/cgi.c ================================================================== --- src/cgi.c +++ src/cgi.c @@ -84,10 +84,11 @@ #endif #include #include #include #include +#include #include "cgi.h" #include "cygsup.h" #if INTERFACE /* @@ -333,10 +334,95 @@ if( strstr(PD("HTTP_ACCEPT_ENCODING", ""), "gzip")==0 ) return 0; return strncmp(zContentType, "text/", 5)==0 || sqlite3_strglob("application/*xml", zContentType)==0 || sqlite3_strglob("application/*javascript", zContentType)==0; } + + +/* +** The following routines read or write content from/to the wire for +** an HTTP request. Depending on settings the content might be coming +** from or going to a socket, or a file, or it might come from or go +** to an SSL decoder/encoder. +*/ +/* +** Works like fgets(): +** +** Read a single line of input into s[]. Ensure that s[] is zero-terminated. +** The s[] buffer is size bytes and so at most size-1 bytes will be read. +** +** Return a pointer to s[] on success, or NULL at end-of-input. +*/ +static char *cgi_fgets(char *s, int size){ + if( !g.httpUseSSL ){ + return fgets(s, size, g.httpIn); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_gets(g.httpSSLConn, s, size); +#else + fossil_fatal("SSL not available"); +#endif +} + +/* Works like fread(): +** +** Read as many as bytes of content as we can, up to a maximum of nmemb +** bytes. Return the number of bytes read. Return -1 if there is no +** further input or if an I/O error occurs. +*/ +size_t cgi_fread(void *ptr, size_t nmemb){ + if( !g.httpUseSSL ){ + return fread(ptr, 1, nmemb, g.httpIn); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_read_server(g.httpSSLConn, ptr, nmemb); +#else + fossil_fatal("SSL not available"); +#endif +} + +/* Works like feof(): +** +** Return true if end-of-input has been reached. +*/ +int cgi_feof(void){ + if( !g.httpUseSSL ){ + return feof(g.httpIn); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_eof(g.httpSSLConn); +#else + return 1; +#endif +} + +/* Works like fwrite(): +** +** Try to output nmemb bytes of content. Return the number of +** bytes actually written. +*/ +static size_t cgi_fwrite(void *ptr, size_t nmemb){ + if( !g.httpUseSSL ){ + return fwrite(ptr, 1, nmemb, g.httpOut); + } +#ifdef FOSSIL_ENABLE_SSL + return ssl_write_server(g.httpSSLConn, ptr, nmemb); +#else + fossil_fatal("SSL not available"); +#endif +} + +/* Works like fflush(): +** +** Make sure I/O has completed. +*/ +static void cgi_fflush(void){ + if( !g.httpUseSSL ){ + fflush(g.httpOut); + } +} + /* ** Generate the reply to a web request. The output might be an ** full HTTP response, or a CGI response, depending on how things have ** be set up. @@ -436,11 +522,11 @@ blob_appendf(&hdr, "Content-Length: %d\r\n", total_size); }else{ total_size = 0; } blob_appendf(&hdr, "\r\n"); - fwrite(blob_buffer(&hdr), 1, blob_size(&hdr), g.httpOut); + cgi_fwrite(blob_buffer(&hdr), blob_size(&hdr)); blob_reset(&hdr); if( total_size>0 && iReplyStatus!=304 && fossil_strcmp(P("REQUEST_METHOD"),"HEAD")!=0 ){ @@ -452,17 +538,17 @@ }else{ int n = size - rangeStart; if( n>total_size ){ n = total_size; } - fwrite(blob_buffer(&cgiContent[i])+rangeStart, 1, n, g.httpOut); + cgi_fwrite(blob_buffer(&cgiContent[i])+rangeStart, n); rangeStart = 0; total_size -= n; } } } - fflush(g.httpOut); + cgi_fflush(); CGIDEBUG(("-------- END cgi ---------\n")); /* After the webpage has been sent, do any useful background ** processing. */ @@ -1262,18 +1348,19 @@ g.zContentType = zType; } blob_zero(&g.cgiIn); if( len>0 && zType ){ if( fossil_strcmp(zType, "application/x-fossil")==0 ){ - if( blob_read_from_channel(&g.cgiIn, g.httpIn, len)!=len ){ + if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ malformed_request("CGI content-length mismatch"); } blob_uncompress(&g.cgiIn, &g.cgiIn); } #ifdef FOSSIL_ENABLE_JSON - else if( noJson==0 && g.json.isJsonMode!=0 + else if( noJson==0 && g.json.isJsonMode!=0 && json_can_consume_content_type(zType)!=0 ){ + assert( !g.httpUseSSL ); cgi_parse_POST_JSON(g.httpIn, (unsigned int)len); /* Potential TODOs: 1) If parsing fails, immediately return an error response @@ -1281,11 +1368,11 @@ */ cgi_set_content_type(json_guess_content_type()); } #endif /* FOSSIL_ENABLE_JSON */ else{ - blob_read_from_channel(&g.cgiIn, g.httpIn, len); + blob_read_from_cgi(&g.cgiIn, len); } } } /* @@ -1799,11 +1886,11 @@ char *z, *zToken; int i; const char *zScheme = "http"; char zLine[2000]; /* A single line of input. */ g.fullHttpReply = 1; - if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ + if( cgi_fgets(zLine, sizeof(zLine))==0 ){ malformed_request("missing HTTP header"); } blob_append(&g.httpHeader, zLine, -1); cgi_trace(zLine); zToken = extract_token(zLine, &z); @@ -1837,11 +1924,11 @@ } /* Get all the optional fields that follow the first line. */ - while( fgets(zLine,sizeof(zLine),g.httpIn) ){ + while( cgi_fgets(zLine,sizeof(zLine)) ){ char *zFieldName; char *zVal; cgi_trace(zLine); blob_append(&g.httpHeader, zLine, -1); @@ -1916,10 +2003,11 @@ char *z, *zToken; const char *zType = 0; int i, content_length = 0; char zLine[2000]; /* A single line of input. */ + assert( !g.httpUseSSL ); #ifdef FOSSIL_ENABLE_JSON if( nCycles==0 ){ json_bootstrap_early(); } #endif if( zIpAddr ){ if( nCycles==0 ){ @@ -2057,10 +2145,11 @@ /* ** This routine handles the old fossil SSH probes */ char *cgi_handle_ssh_probes(char *zLine, int zSize, char *z, char *zToken){ /* Start looking for probes */ + assert( !g.httpUseSSL ); while( fossil_strcmp(zToken, "echo")==0 ){ zToken = extract_token(z, &z); if( zToken==0 ){ malformed_request("malformed probe"); } @@ -2094,10 +2183,11 @@ */ void cgi_handle_ssh_transport(const char *zCmd){ char *z, *zToken; char zLine[2000]; /* A single line of input. */ + assert( !g.httpUseSSL ); /* look for second newline of transport_flip */ if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){ malformed_request("incorrect transport_flip"); } cgi_trace(zLine); @@ -2143,10 +2233,11 @@ char *zToFree; int nHdr = 0; int nRead; int c, n, m; + assert( !g.httpUseSSL ); while( (c = fgetc(g.httpIn))!=EOF && fossil_isdigit((char)c) ){ nHdr = nHdr*10 + (char)c - '0'; } if( nHdr<16 ) malformed_request("SCGI header too short"); zToFree = zHdr = fossil_malloc(nHdr); @@ -2174,10 +2265,11 @@ #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */ #define HTTP_SERVER_SCGI 0x0002 /* SCGI instead of HTTP */ #define HTTP_SERVER_HAD_REPOSITORY 0x0004 /* Was the repository open? */ #define HTTP_SERVER_HAD_CHECKOUT 0x0008 /* Was a checkout open? */ #define HTTP_SERVER_REPOLIST 0x0010 /* Allow repo listing */ +#define HTTP_SERVER_NOFORK 0x0020 /* Do not call fork() */ #endif /* INTERFACE */ /* ** Maximum number of child processes that we can have running @@ -2258,11 +2350,12 @@ } } if( iPort>mxPort ) return 1; listen(listener,10); fossil_print("Listening for %s requests on TCP port %d\n", - (flags & HTTP_SERVER_SCGI)!=0?"SCGI":"HTTP", iPort); + (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" : + g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort); fflush(stdout); if( zBrowser ){ assert( strstr(zBrowser,"%d")!=0 ); zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort); #if defined(__CYGWIN__) @@ -2293,11 +2386,15 @@ select( listener+1, &readfds, 0, 0, &delay); if( FD_ISSET(listener, &readfds) ){ lenaddr = sizeof(inaddr); connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr); if( connection>=0 ){ - child = fork(); + if( flags & HTTP_SERVER_NOFORK ){ + child = 0; + }else{ + child = fork(); + } if( child!=0 ){ if( child>0 ){ nchildren++; nRequest++; } Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -4263,10 +4263,11 @@ */ /* ** SETTING: ssh-command width=40 sensitive ** The command used to talk to a remote machine with the "ssh://" protocol. */ + /* ** SETTING: ssl-ca-location width=40 sensitive ** The full pathname to a file containing PEM encoded ** CA root certificates, or a directory of certificates ** with filenames formed from the certificate hashes as @@ -4277,10 +4278,24 @@ ** Some platforms may add additional certificates. ** Checking your platform behaviour is required if the ** exact contents of the CA root is critical for your ** application. */ +/* +** SETTING: ssl-cert width=40 block-text sensitive +** The text of SSL server certificate and private key used by commands +** like "fossil server". The text should be in the PEM format. Use +** the "fossil ssl-config load-certs" command to change this setting. +*/ +/* +** SETTING: ssl-cert-file width=40 sensitive +** The name of a file that contains the SSL server certificate, or +** optionally the concatenation of the certificate and private key, +** for use by Fossil when it is acting as a server. If this file +** contains only the certificate, then the ssl-key-file setting must +** contain the name of a file containing the private key. +*/ /* ** SETTING: ssl-identity width=40 sensitive ** The full pathname to a file containing a certificate ** and private key in PEM format. Create by concatenating ** the certificate and private key files. @@ -4287,10 +4302,15 @@ ** ** This identity will be presented to SSL servers to ** authenticate this client, in addition to the normal ** password authentication. */ +/* +** SETTING: ssl-key-file width=40 sensitive +** The name of a file that contains the SSL server certificate private +** key. Used in combination with "ssl-cert-file". +*/ #ifdef FOSSIL_ENABLE_TCL /* ** SETTING: tcl boolean default=off sensitive ** If enabled Tcl integration commands will be added to the TH1 ** interpreter, allowing arbitrary Tcl expressions and Index: src/http_ssl.c ================================================================== --- src/http_ssl.c +++ src/http_ssl.c @@ -16,12 +16,15 @@ ******************************************************************************* ** ** This file manages low-level SSL communications. ** ** This file implements a singleton. A single SSL connection may be active -** at a time. State information is stored in static variables. The identity -** of the server is held in global variables that are set by url_parse(). +** at a time. State information is stored in static variables. +** +** The SSL connections can be either a client or a server. But all +** connections for a single process must be of the same type, either client +** or server. ** ** SSL support is abstracted out into this module because Fossil can ** be compiled without SSL support (which requires OpenSSL library) */ @@ -41,11 +44,11 @@ /* ** There can only be a single OpenSSL IO connection open at a time. ** State information about that IO is stored in the following ** local variables: */ -static int sslIsInit = 0; /* True after global initialization */ +static int sslIsInit = 0; /* 0: uninit 1: init as client 2: init as server */ static BIO *iBio = 0; /* OpenSSL I/O abstraction */ static char *sslErrMsg = 0; /* Text of most recent OpenSSL error */ static SSL_CTX *sslCtx; /* SSL context */ static SSL *ssl; static struct { /* Accept this SSL cert for this session only */ @@ -52,10 +55,120 @@ char *zHost; /* Subject or host name */ char *zHash; /* SHA2-256 hash of the cert */ } sException; static int sslNoCertVerify = 0; /* Do not verify SSL certs */ + +/* This is a self-signed cert in the PEM format that can be used when +** no other certs are available. +*/ +static const char sslSelfCert[] = +"-----BEGIN CERTIFICATE-----\n" +"MIIDMTCCAhkCFGrDmuJkkzWERP/ITBvzwwI2lv0TMA0GCSqGSIb3DQEBCwUAMFQx\n" +"CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzESMBAGA1UEBwwJQ2hhcmxvdHRlMRMw\n" +"EQYDVQQKDApGb3NzaWwtU0NNMQ8wDQYDVQQDDAZGb3NzaWwwIBcNMjExMjI3MTEz\n" +"MTU2WhgPMjEyMTEyMjcxMTMxNTZaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJO\n" +"QzESMBAGA1UEBwwJQ2hhcmxvdHRlMRMwEQYDVQQKDApGb3NzaWwtU0NNMQ8wDQYD\n" +"VQQDDAZGb3NzaWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCCbTU2\n" +"6GRQHQqLq7vyZ0OxpAxmgfAKCxt6eIz+jBi2ZM/CB5vVXWVh2+SkSiWEA3UZiUqX\n" +"xZlzmS/CglZdiwLLDJML8B4OiV72oivFH/vJ7+cbvh1dTxnYiHuww7GfQngPrLfe\n" +"fiIYPDk1GTUJHBQ7Ue477F7F8vKuHdVgwktF/JDM6M60aSqlo2D/oysirrb+dlur\n" +"Tlv0rjsYOfq6bLAajoL3qi/vek6DNssoywbge4PfbTgS9g7Gcgncbcet5pvaS12J\n" +"avhFcd4JU4Ity49Hl9S/C2MfZ1tE53xVggRwKz4FPj65M5uymTdcxtjKXtCxIE1k\n" +"KxJxXQh7rIYjm+RTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFkdtpqcybAzJN8G\n" +"+ONuUm5sXNbWta7JGvm8l0BTSBcCUtJA3hn16iJqXA9KmLnaF2denC4EYk+KlVU1\n" +"QXxskPJ4jB8A5B05jMijYv0nzCxKhviI8CR7GLEEGKzeg9pbW0+O3vaVehoZtdFX\n" +"z3SsCssr9QjCLiApQxMzW1Iv3od2JXeHBwfVMFrWA1VCEUCRs8OSW/VOqDPJLVEi\n" +"G6wxc4kN9dLK+5S29q3nzl24/qzXoF8P9Re5KBCbrwaHgy+OEEceq5jkmfGFxXjw\n" +"pvVCNry5uAhH5NqbXZampUWqiWtM4eTaIPo7Y2mDA1uWhuWtO6F9PsnFJlQHCnwy\n" +"s/TsrXk=\n" +"-----END CERTIFICATE-----\n"; + +/* This is the private-key corresponding to the cert above +*/ +static const char sslSelfPKey[] = +"-----BEGIN PRIVATE KEY-----\n" +"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCCbTU26GRQHQqL\n" +"q7vyZ0OxpAxmgfAKCxt6eIz+jBi2ZM/CB5vVXWVh2+SkSiWEA3UZiUqXxZlzmS/C\n" +"glZdiwLLDJML8B4OiV72oivFH/vJ7+cbvh1dTxnYiHuww7GfQngPrLfefiIYPDk1\n" +"GTUJHBQ7Ue477F7F8vKuHdVgwktF/JDM6M60aSqlo2D/oysirrb+dlurTlv0rjsY\n" +"Ofq6bLAajoL3qi/vek6DNssoywbge4PfbTgS9g7Gcgncbcet5pvaS12JavhFcd4J\n" +"U4Ity49Hl9S/C2MfZ1tE53xVggRwKz4FPj65M5uymTdcxtjKXtCxIE1kKxJxXQh7\n" +"rIYjm+RTAgMBAAECggEANfTH1vc8yIe7HRzmm9lsf8jF+II4s2705y2H5qY+cvYx\n" +"nKtZJGOG1X0KkYy7CGoFv5K0cSUl3lS5FVamM/yWIzoIex/Sz2C1EIL2aI5as6ez\n" +"jB6SN0/J+XI8+Vt7186/rHxfdIPpxuzjHbxX3HTpScETNWcLrghbrPxakbTPPxwt\n" +"+x7QlPmmkFNuMfvkzToFf9NdwL++44TeBPOpvD/Lrw+eyqdth9RJPq9cM96plh9V\n" +"HuRqeD8+QNafaXBdSQs3FJK/cDK/vWGKZWIfFVSDbDhwYljkXGijreFjtXQfkkpF\n" +"rl1J87/H9Ee7z8fTD2YXQHl+0/rghAVtac3u54dpQQKBgQC2XG3OEeMrOp9dNkUd\n" +"F8VffUg0ecwG+9L3LCe7U71K0kPmXjV6xNnuYcNQu84kptc5vI8wD23p29LaxdNc\n" +"9m0lcw06/YYBOPkNphcHkINYZTvVJF10mL3isymzMaTtwDkZUkOjL1B+MTiFT/qp\n" +"ARKrTYGJ4HxY7+tUkI5pUmg4PQKBgQC3GA4d1Rz3Pb/RRpcsZgWknKsKhoN36mSn\n" +"xFJ3wPBvVv2B1ltTMzh/+the0ty6clzMrvoLERzRcheDsNrc/j/TUVG8sVdBYJwX\n" +"tMZyFW4NVMOErT/1ukh6jBqIMBo6NJL3EV/AKj0yniksgKOr0/AAduAccnGST8Jd\n" +"SHOdjwvHzwKBgGZBq/zqgNTDuYseHGE07CMgcDWkumiMGv8ozlq3mSR0hUiPOTPP\n" +"YFjQjyIdPXnF6FfiyPPtIvgIoNK2LVAqiod+XUPf152l4dnqcW13dn9BvOxGyPTR\n" +"lWCikFaAFviOWjY9r9m4dU1dslDmySqthFd0TZgPvgps9ivkJ0cdw30NAoGAMC/E\n" +"h1VvKiK2OP27C5ROJ+STn1GHiCfIFd81VQ8SODtMvL8NifgRBp2eFFaqgOdYRQZI\n" +"CGGYlAbS6XXCJCdF5Peh62dA75PdgN+y2pOJQzjrvB9cle9Q4++7i9wdCvSLOTr5\n" +"WDnFoWy+qVexu6crovOmR9ZWzYrwPFy1EOJ010ECgYBl7Q+jmjOSqsVwhFZ0U7LG\n" +"diN+vXhWfn1wfOWd8u79oaqU/Oy7xyKW2p3H5z2KFrBM/vib53Lh4EwFZjcX+jVG\n" +"krAmbL+M/hP7z3TD2UbESAzR/c6l7FU45xN84Lsz5npkR8H/uAHuqLgb9e430Mjx\n" +"YNMwdb8rChHHChNZu6zuxw==\n" +"-----END PRIVATE KEY-----\n"; + +/* +** Read a PEM certificate from memory and push it into an SSL_CTX. +** Return the number of errors. +*/ +static int sslctx_use_cert_from_mem( + SSL_CTX *ctx, + const char *pData, + int nData +){ + BIO *in; + int rc = 1; + X509 *x = 0; + X509 *cert = 0; + + in = BIO_new_mem_buf(pData, nData); + if( in==0 ) goto end_of_ucfm; + // x = X509_new_ex(ctx->libctx, ctx->propq); + x = X509_new(); + if( x==0 ) goto end_of_ucfm; + cert = PEM_read_bio_X509(in, &x, 0, 0); + if( cert==0 ) goto end_of_ucfm; + rc = SSL_CTX_use_certificate(ctx, x)<=0; +end_of_ucfm: + X509_free(x); + BIO_free(in); + return rc; +} + +/* +** Read a PEM private key from memory and add it to an SSL_CTX. +** Return the number of errors. +*/ +static int sslctx_use_pkey_from_mem( + SSL_CTX *ctx, + const char *pData, + int nData +){ + int rc = 1; + BIO *in; + EVP_PKEY *pkey = 0; + + in = BIO_new_mem_buf(pData, nData); + if( in==0 ) goto end_of_upkfm; + pkey = PEM_read_bio_PrivateKey(in, 0, 0, 0); + if( pkey==0 ) goto end_of_upkfm; + rc = SSL_CTX_use_PrivateKey(ctx, pkey)<=0; + EVP_PKEY_free(pkey); +end_of_upkfm: + BIO_free(in); + return rc; +} + /* ** Clear the SSL error message */ static void ssl_clear_errmsg(void){ free(sslErrMsg); @@ -134,11 +247,11 @@ /* ** Call this routine once before any other use of the SSL interface. ** This routine does initial configuration of the SSL module. */ -void ssl_global_init(void){ +static void ssl_global_init_client(void){ const char *zCaSetting = 0, *zCaFile = 0, *zCaDirectory = 0; const char *identityFile; if( sslIsInit==0 ){ SSL_library_init(); @@ -193,10 +306,12 @@ /* Register a callback to tell the user what to do when the server asks ** for a cert */ SSL_CTX_set_client_cert_cb(sslCtx, ssl_client_cert_callback); sslIsInit = 1; + }else{ + assert( sslIsInit==1 ); } } /* ** Call this routine to shutdown the SSL module prior to program exit. @@ -208,14 +323,14 @@ sslIsInit = 0; } } /* -** Close the currently open SSL connection. If no connection is open, +** Close the currently open client SSL connection. If no connection is open, ** this routine is a no-op. */ -void ssl_close(void){ +void ssl_close_client(void){ if( iBio!=NULL ){ (void)BIO_reset(iBio); BIO_free_all(iBio); iBio = NULL; } @@ -280,34 +395,36 @@ void ssl_disable_cert_verification(void){ sslNoCertVerify = 1; } /* -** Open an SSL connection. The identify of the server is determined -** as follows: +** Open an SSL connection as a client that is to connect to the server +** identified by pUrlData. +** +* The identify of the server is determined as follows: ** ** pUrlData->name Name of the server. Ex: fossil-scm.org ** g.url.name Name of the proxy server, if proxying. ** pUrlData->port TCP/IP port to use. Ex: 80 ** ** Return the number of errors. */ -int ssl_open(UrlData *pUrlData){ +int ssl_open_client(UrlData *pUrlData){ X509 *cert; const char *zRemoteHost; - ssl_global_init(); + ssl_global_init_client(); if( pUrlData->useProxy ){ int rc; char *connStr = mprintf("%s:%d", g.url.name, pUrlData->port); BIO *sBio = BIO_new_connect(connStr); free(connStr); if( BIO_do_connect(sBio)<=0 ){ ssl_set_errmsg("SSL: cannot connect to proxy %s:%d (%s)", pUrlData->name, pUrlData->port, ERR_reason_error_string(ERR_get_error())); - ssl_close(); + ssl_close_client(); return 1; } rc = establish_proxy_tunnel(pUrlData, sBio); if( rc<200||rc>299 ){ ssl_set_errmsg("SSL: proxy connect failed with HTTP status code %d", rc); @@ -355,29 +472,29 @@ free(connStr); if( BIO_do_connect(iBio)<=0 ){ ssl_set_errmsg("SSL: cannot connect to host %s:%d (%s)", pUrlData->name, pUrlData->port, ERR_reason_error_string(ERR_get_error())); - ssl_close(); + ssl_close_client(); return 1; } } if( BIO_do_handshake(iBio)<=0 ) { ssl_set_errmsg("Error establishing SSL connection %s:%d (%s)", pUrlData->useProxy?pUrlData->hostname:pUrlData->name, pUrlData->useProxy?pUrlData->proxyOrigPort:pUrlData->port, ERR_reason_error_string(ERR_get_error())); - ssl_close(); + ssl_close_client(); return 1; } /* Check if certificate is valid */ cert = SSL_get_peer_certificate(ssl); if ( cert==NULL ){ ssl_set_errmsg("No SSL certificate was presented by the peer"); - ssl_close(); + ssl_close_client(); return 1; } /* Debugging hint: On unix-like system, run something like: ** @@ -441,11 +558,11 @@ if( cReply!='y' && cReply!='Y' && fossil_stricmp(blob_str(&ans),zHash)!=0 ){ X509_free(cert); ssl_set_errmsg("SSL cert declined"); - ssl_close(); + ssl_close_client(); blob_reset(&ans); return 1; } blob_reset(&ans); ssl_one_time_exception(pUrlData, zHash); @@ -528,11 +645,12 @@ fossil_free(sException.zHash); sException.zHash = fossil_strdup(zHash); } /* -** Send content out over the SSL connection. +** Send content out over the SSL connection from the client to +** the server. */ size_t ssl_send(void *NotUsed, void *pContent, size_t N){ size_t total = 0; while( N>0 ){ int sent = BIO_write(iBio, pContent, N); @@ -548,11 +666,12 @@ } return total; } /* -** Receive content back from the SSL connection. +** Receive content back from the client SSL connection. In other +** words read the reply back from the server. */ size_t ssl_receive(void *NotUsed, void *pContent, size_t N){ size_t total = 0; while( N>0 ){ int got = BIO_read(iBio, pContent, N); @@ -566,50 +685,366 @@ N -= got; pContent = (void*)&((char*)pContent)[got]; } return total; } + +/* +** Initialize the SSL library so that it is able to handle +** server-side connections. Invoke fossil_fatal() if there are +** any problems. +** +** If zKeyFile and zCertFile are not NULL, then they are the names +** of disk files that hold the certificate and private-key for the +** server. If zCertFile is not NULL but zKeyFile is NULL, then +** zCertFile is assumed to be a concatenation of the certificate and +** the private-key in the PEM format. +** +** If zCertFile is NULL, then "ssl-cert" setting is consulted +** to get the certificate and private-key (concatenated together, in +** the PEM format). If there is no ssl-cert setting, then +** a built-in self-signed cert is used. +*/ +void ssl_init_server(const char *zCertFile, const char *zKeyFile){ + if( sslIsInit==0 ){ + const char *zTlsCert; + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + sslCtx = SSL_CTX_new(SSLv23_server_method()); + if( sslCtx==0 ){ + ERR_print_errors_fp(stderr); + fossil_fatal("Error initializing the SSL server"); + } + if( zCertFile && zCertFile[0] ){ + if( SSL_CTX_use_certificate_file(sslCtx,zCertFile,SSL_FILETYPE_PEM)<=0 ){ + ERR_print_errors_fp(stderr); + fossil_fatal("Error loading CERT file \"%s\"", zCertFile); + } + if( zKeyFile==0 ) zKeyFile = zCertFile; + if( SSL_CTX_use_PrivateKey_file(sslCtx, zKeyFile, SSL_FILETYPE_PEM)<=0 ){ + ERR_print_errors_fp(stderr); + fossil_fatal("Error loading PRIVATE KEY from file \"%s\"", zKeyFile); + } + }else + if( (zTlsCert = db_get("ssl-cert",0))!=0 ){ + if( sslctx_use_cert_from_mem(sslCtx, zTlsCert, -1) + || sslctx_use_pkey_from_mem(sslCtx, zTlsCert, -1) + ){ + fossil_fatal("Error loading the CERT from the" + " 'ssl-cert' setting"); + } + }else if( sslctx_use_cert_from_mem(sslCtx, sslSelfCert, -1) + || sslctx_use_pkey_from_mem(sslCtx, sslSelfPKey, -1) ){ + fossil_fatal("Error loading self-signed CERT"); + } + if( !SSL_CTX_check_private_key(sslCtx) ){ + fossil_fatal("PRIVATE KEY \"%s\" does not match CERT \"%s\"", + zKeyFile, zCertFile); + } + sslIsInit = 2; + }else{ + assert( sslIsInit==2 ); + } +} + +typedef struct SslServerConn { + SSL *ssl; /* The SSL codec */ + int atEof; /* True when EOF reached. */ + int fd0; /* Read channel, or socket */ + int fd1; /* Write channel */ +} SslServerConn; + +/* +** Create a new server-side codec. The arguments are the file +** descriptors from which teh codec reads and writes, respectively. +** +** If the writeFd is negative, then use then the readFd is a socket +** over which we both read and write. +*/ +void *ssl_new_server(int readFd, int writeFd){ + SslServerConn *pServer = fossil_malloc_zero(sizeof(*pServer)); + pServer->ssl = SSL_new(sslCtx); + pServer->fd0 = readFd; + pServer->fd1 = writeFd; + if( writeFd<0 ){ + SSL_set_fd(pServer->ssl, readFd); + }else{ + SSL_set_rfd(pServer->ssl, readFd); + SSL_set_wfd(pServer->ssl, writeFd); + } + SSL_accept(pServer->ssl); + return (void*)pServer; +} + +/* +** Close a server-side code previously returned from ssl_new_server(). +*/ +void ssl_close_server(void *pServerArg){ + SslServerConn *pServer = (SslServerConn*)pServerArg; + SSL_free(pServer->ssl); + close(pServer->fd0); + if( pServer->fd1>=0 ) close(pServer->fd0); + fossil_free(pServer); +} + +/* +** Return TRUE if there are no more bytes available to be read from +** the client. +*/ +int ssl_eof(void *pServerArg){ + SslServerConn *pServer = (SslServerConn*)pServerArg; + return pServer->atEof; +} + +/* +** Read cleartext bytes that have been received from the client and +** decrypted by the SSL server codec. +*/ +size_t ssl_read_server(void *pServerArg, char *zBuf, size_t nBuf){ + int n; + SslServerConn *pServer = (SslServerConn*)pServerArg; + if( pServer->atEof ) return 0; + if( nBuf>0x7fffffff ){ fossil_fatal("SSL read too big"); } + n = SSL_read(pServer->ssl, zBuf, (int)nBuf); + if( natEof = 1; + return n; +} + +/* +** Read a single line of text from the client. +*/ +char *ssl_gets(void *pServerArg, char *zBuf, int nBuf){ + int n = 0; + int i; + SslServerConn *pServer = (SslServerConn*)pServerArg; + + if( pServer->atEof ) return 0; + for(i=0; issl, &zBuf[i], 1); + if( n<=0 ){ + return 0; + } + if( zBuf[i]=='\n' ) break; + } + zBuf[i+1] = 0; + return zBuf; +} + + +/* +** Write cleartext bytes into the SSL server codec so that they can +** be encrypted and sent back to the client. +*/ +size_t ssl_write_server(void *pServerArg, char *zBuf, size_t nBuf){ + int n; + SslServerConn *pServer = (SslServerConn*)pServerArg; + if( pServer->atEof ) return 0; + if( nBuf>0x7fffffff ){ fossil_fatal("SSL write too big"); } + n = SSL_write(pServer->ssl, zBuf, (int)nBuf); + return n; +} #endif /* FOSSIL_ENABLE_SSL */ /* ** COMMAND: tls-config* +** COMMAND: ssl-config ** -** Usage: %fossil tls-config [SUBCOMMAND] [OPTIONS...] [ARGS...] +** Usage: %fossil ssl-config [SUBCOMMAND] [OPTIONS...] [ARGS...] ** ** This command is used to view or modify the TLS (Transport Layer ** Security) configuration for Fossil. TLS (formerly SSL) is the ** encryption technology used for secure HTTPS transport. ** ** Sub-commands: ** -** show Show the TLS configuration +** clear-cert Remove information about server certificates. +** This is a subset of the "scrub" command. +** +** load-cert PEM-FILES... Identify server certificate files. These +** should be in the PEM format. There are +** normally two files, the certificate and the +** private-key. By default, the text of both +** files is concatenated and added to the +** "ssl-cert" setting. Use --filename to store +** just the filenames. +** +** remove-exception DOMAINS Remove TLS cert exceptions for the domains +** listed. Or remove them all if the --all +** option is specified. +** +** scrub ?--force? Remove all SSL configuration data from the +** repository. Use --force to omit the +** confirmation. ** -** remove-exception DOMAIN... Remove TLS cert exceptions -** for the domains listed. Or if -** the --all option is specified, -** remove all TLS cert exceptions. +** show ?-v? Show the TLS configuration. Add -v to see +** additional explaination */ void test_tlsconfig_info(void){ -#if !defined(FOSSIL_ENABLE_SSL) - fossil_print("TLS disabled in this build\n"); -#else const char *zCmd; size_t nCmd; int nHit = 0; db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); db_open_config(1,0); - zCmd = g.argc>=3 ? g.argv[2] : "show"; - nCmd = strlen(zCmd); + if( g.argc==2 || (g.argc>=3 && g.argv[2][0]=='-') ){ + zCmd = "show"; + nCmd = 4; + }else{ + zCmd = g.argv[2]; + nCmd = strlen(zCmd); + } + if( strncmp("clear-cert",zCmd,nCmd)==0 && nCmd>=4 ){ + int bForce = find_option("force","f",0)!=0; + verify_all_options(); + if( !bForce ){ + Blob ans; + char cReply; + prompt_user( + "Confirm removing of the SSL server certificate from this repository.\n" + "The removal cannot be undone. Continue (y/N)? ", &ans); + cReply = blob_str(&ans)[0]; + if( cReply!='y' && cReply!='Y' ){ + fossil_exit(1); + } + } + db_unprotect(PROTECT_ALL); + db_multi_exec( + "PRAGMA secure_delete=ON;" + "DELETE FROM config " + " WHERE name IN ('ssl-cert','ssl-cert-file','ssl-cert-key');" + ); + db_protect_pop(); + }else + if( strncmp("load-cert",zCmd,nCmd)==0 && nCmd>=4 ){ + int bFN = find_option("filename",0,0)!=0; + int i; + Blob allText = BLOB_INITIALIZER; + int haveCert = 0; + int haveKey = 0; + verify_all_options(); + db_begin_transaction(); + db_unprotect(PROTECT_ALL); + db_multi_exec( + "PRAGMA secure_delete=ON;" + "DELETE FROM config " + " WHERE name IN ('ssl-cert','ssl-cert-file','ssl-cert-key');" + ); + nHit = 0; + for(i=3; i4 ){ + int bForce = find_option("force","f",0)!=0; + verify_all_options(); + if( !bForce ){ + Blob ans; + char cReply; + prompt_user( + "Scrubbing the SSL configuration will permanently delete information.\n" + "Changes cannot be undone. Continue (y/N)? ", &ans); + cReply = blob_str(&ans)[0]; + if( cReply!='y' && cReply!='Y' ){ + fossil_exit(1); + } + } + db_unprotect(PROTECT_ALL); + db_multi_exec( + "PRAGMA secure_delete=ON;" + "DELETE FROM config WHERE name GLOB 'ssl-*';" + ); + db_protect_pop(); + }else if( strncmp("show",zCmd,nCmd)==0 ){ const char *zName, *zValue; size_t nName; Stmt q; + int verbose = find_option("verbose","v",0)!=0; + verify_all_options(); + +#if !defined(FOSSIL_ENABLE_SSL) + fossil_print("OpenSSL-version: (none)\n"); + if( verbose ){ + fossil_print("\n" + " The OpenSSL library is not used by this build of Fossil\n\n" + ); + } +#else fossil_print("OpenSSL-version: %s (0x%09x)\n", SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER); + if( verbose ){ + fossil_print("\n" + " The version of the OpenSSL library being used\n" + " by this instance of Fossil. Version 3.0.0 or\n" + " later is recommended.\n\n" + ); + } + fossil_print("OpenSSL-cert-file: %s\n", X509_get_default_cert_file()); fossil_print("OpenSSL-cert-dir: %s\n", X509_get_default_cert_dir()); + if( verbose ){ + fossil_print("\n" + " The default locations for the set of root certificates\n" + " used by the \"fossil sync\" and similar commands to verify\n" + " the identity of servers for \"https:\" URLs. These values\n" + " come into play when Fossil is used as a TLS client. These\n" + " values are built into your OpenSSL library.\n\n" + ); + } + zName = X509_get_default_cert_file_env(); zValue = fossil_getenv(zName); if( zValue==0 ) zValue = ""; nName = strlen(zName); fossil_print("%s:%*s%s\n", zName, 18-nName, "", zValue); @@ -616,25 +1051,86 @@ zName = X509_get_default_cert_dir_env(); zValue = fossil_getenv(zName); if( zValue==0 ) zValue = ""; nName = strlen(zName); fossil_print("%s:%*s%s\n", zName, 18-nName, "", zValue); - nHit++; + if( verbose ){ + fossil_print("\n" + " Alternative locations for the root certificates used by Fossil\n" + " when it is acting as a SSL client in order to verify the identity\n" + " of servers. If specified, these alternative locations override\n" + " the built-in locations.\n\n" + ); + } +#endif /* FOSSIL_ENABLE_SSL */ + fossil_print("ssl-ca-location: %s\n", db_get("ssl-ca-location","")); + if( verbose ){ + fossil_print("\n" + " This setting is the name of a file or directory that contains\n" + " the complete set of root certificates to used by Fossil when it\n" + " is acting as a SSL client. If defined, this setting takes\n" + " priority over built-in paths and environment variables\n\n" + ); + } + fossil_print("ssl-identity: %s\n", db_get("ssl-identity","")); + if( verbose ){ + fossil_print("\n" + " This setting is the name of a file that contains the PEM-format\n" + " certificate and private-key used by Fossil clients to authentice\n" + " with servers. Few servers actually require this, so this setting\n" + " is usually blank.\n\n" + ); + } + + zValue = db_get("ssl-cert",0); + if( zValue ){ + fossil_print("ssl-cert: (%d-byte PEM)\n", (int)strlen(zValue)); + }else{ + fossil_print("ssl-cert:\n"); + } + if( verbose ){ + fossil_print("\n" + " This setting is the PEM-formatted value of the SSL server\n" + " certificate and private-key, used by Fossil when it is acting\n" + " as a server via the \"fossil server\" command or similar.\n\n" + ); + } + + fossil_print("ssl-cert-file: %s\n", db_get("ssl-cert-file","")); + fossil_print("ssl-key-file: %s\n", db_get("ssl-key-file","")); + if( verbose ){ + fossil_print("\n" + " This settings are the names of files that contin the certificate\n" + " private-key used by Fossil when it is acting as a server.\n\n" + ); + } + db_prepare(&q, - "SELECT name FROM global_config" + "SELECT name, '' FROM global_config" " WHERE name GLOB 'cert:*'" "UNION ALL " - "SELECT name FROM config" + "SELECT name, date(mtime,'unixepoch') FROM config" " WHERE name GLOB 'cert:*'" " ORDER BY name" ); + nHit = 0; while( db_step(&q)==SQLITE_ROW ){ - fossil_print("exception: %s\n", db_column_text(&q,0)+5); + fossil_print("exception: %-40s %s\n", + db_column_text(&q,0)+5, db_column_text(&q,1)); + nHit++; } db_finalize(&q); + if( nHit && verbose ){ + fossil_print("\n" + " The exceptions are server certificates that the Fossil client\n" + " is unable to verify using root certificates, but which should be\n" + " accepted anyhow.\n\n" + ); + } + }else if( strncmp("remove-exception",zCmd,nCmd)==0 ){ int i; Blob sql; char *zSep = "("; @@ -673,10 +1169,54 @@ db_commit_transaction(); blob_reset(&sql); }else /*default*/{ fossil_fatal("unknown sub-command \"%s\".\nshould be one of:" - " remove-exception show", + " clear-certs load-certs remove-exception scrub show", zCmd); } -#endif +} + +/* +** WEBPAGE: .well-known +** +** If the "--acme" option was supplied to "fossil server" or "fossil http" or +** similar, then this page returns the content of files found in the +** ".well-known" subdirectory of the same directory that contains the +** repository file. This facilitates Automated Certificate +** Management using tools like "certbot". +** +** The content is returned directly, without any interpretation, using +** a generic mimetype. +*/ +void wellknown_page(void){ + char *zPath = 0; + const char *zTail = P("name"); + Blob content; + int i; + char c; + if( !g.fAllowACME ) goto wellknown_notfound; + if( g.zRepositoryName==0 ) goto wellknown_notfound; + if( zTail==0 ) goto wellknown_notfound; + zPath = mprintf("%z/.well-known/%s", file_dirname(g.zRepositoryName), zTail); + for(i=0; (c = zTail[i])!=0; i++){ + if( fossil_isalnum(c) ) continue; + if( c=='.' ){ + if( i==0 || zTail[i-1]=='/' || zTail[i-1]=='.' ) goto wellknown_notfound; + continue; + } + if( c==',' || c!='-' || c=='/' || c==':' || c=='_' || c=='~' ) continue; + goto wellknown_notfound; + } + if( strstr("/..", zPath)!=0 ) goto wellknown_notfound; + if( !file_isfile(zPath, ExtFILE) ) goto wellknown_notfound; + blob_read_from_file(&content, zPath, ExtFILE); + cgi_set_content(&content); + cgi_set_content_type(mimetype_from_name(zPath)); + cgi_reply(); + return; + +wellknown_notfound: + fossil_free(zPath); + webpage_notfound_error(0); + return; } Index: src/http_transport.c ================================================================== --- src/http_transport.c +++ src/http_transport.c @@ -170,11 +170,11 @@ 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(pUrlData); + 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 @@ -213,11 +213,11 @@ } if( pUrlData->isSsh ){ transport_ssh_close(); }else if( pUrlData->isHttps ){ #ifdef FOSSIL_ENABLE_SSL - ssl_close(); + ssl_close_client(); #endif }else if( pUrlData->isFile ){ if( transport.pFile ){ fclose(transport.pFile); transport.pFile = 0; Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -166,10 +166,11 @@ int fCgiTrace; /* True if --cgitrace is enabled */ int fQuiet; /* True if -quiet flag is present */ int fJail; /* True if running with a chroot jail */ int fHttpTrace; /* Trace outbound HTTP requests */ int fAnyTrace; /* Any kind of tracing */ + int fAllowACME; /* Deliver files from .well-known */ char *zHttpAuth; /* HTTP Authorization user:pass information */ int fSystemTrace; /* Trace calls to fossil_system(), --systemtrace */ int fSshTrace; /* Trace the SSH setup traffic */ int fSshClient; /* HTTP client flags for SSH client */ int fNoHttpCompress; /* Do not compress HTTP traffic (for debugging) */ @@ -195,10 +196,12 @@ Th_Interp *interp; /* The TH1 interpreter */ char *th1Setup; /* The TH1 post-creation setup script, if any */ int th1Flags; /* The TH1 integration state flags */ FILE *httpIn; /* Accept HTTP input from here */ FILE *httpOut; /* Send HTTP output here */ + int httpUseSSL; /* True to use an SSL codec for HTTP traffic */ + void *httpSSLConn; /* The SSL connection */ int xlinkClusterOnly; /* Set when cloning. Only process clusters */ int fTimeFormat; /* 1 for UTC. 2 for localtime. 0 not yet selected */ int *aCommitFile; /* Array of files to be committed */ int markPrivate; /* All new artifacts are private if true */ char *ckinLockFail; /* Check-in lock failure received from server */ @@ -1662,10 +1665,11 @@ #if defined(_WIN32) || defined(__CYGWIN__) if( sqlite3_strglob("/[a-zA-Z]:/*", zPathInfo)==0 ) i = 4; #endif } while( 1 ){ + size_t nBase = strlen(zBase); while( zPathInfo[i] && zPathInfo[i]!='/' ){ i++; } /* The candidate repository name is some prefix of the PATH_INFO ** with ".fossil" appended */ zRepo = zToFree = mprintf("%s%.*s.fossil",zBase,i,zPathInfo); @@ -1682,11 +1686,11 @@ ** that "-" never occurs immediately after a "/" and that "." is always ** surrounded by two alphanumerics. Any character that does not ** satisfy these constraints is converted into "_". */ szFile = 0; - for(j=strlen(zBase)+1, k=0; zRepo[j] && k0 ){ /* If REPOSITORY arg is the root of a checkout, ** chdir to that checkout so that the current version ** gets highlighted in the timeline by default. */ const char * zDir = g.argv[2]; @@ -3086,18 +3183,19 @@ iPort = db_get_int("http-port", 8080); mxPort = iPort+100; } if( isUiCmd && !fNoBrowser ){ char *zBrowserArg; + const char *zProtocol = g.httpUseSSL ? "https" : "http"; if( zRemote ) db_open_config(0,0); zBrowser = fossil_web_browser(); if( zIpAddr==0 ){ - zBrowserArg = mprintf("http://localhost:%%d/%s", zInitPage); + zBrowserArg = mprintf("%s://localhost:%%d/%s", zProtocol, zInitPage); }else if( strchr(zIpAddr,':') ){ - zBrowserArg = mprintf("http://[%s]:%%d/%s", zIpAddr, zInitPage); + zBrowserArg = mprintf("%s://[%s]:%%d/%s", zProtocol, zIpAddr, zInitPage); }else{ - zBrowserArg = mprintf("http://%s:%%d/%s", zIpAddr, zInitPage); + zBrowserArg = mprintf("%s://%s:%%d/%s", zProtocol, zIpAddr, zInitPage); } zBrowserCmd = mprintf("%s %!$ &", zBrowser, zBrowserArg); fossil_free(zBrowserArg); } if( zRemote ){ @@ -3186,20 +3284,35 @@ g.zRepositoryName = enter_chroot_jail(g.zRepositoryName, noJail); } } if( flags & HTTP_SERVER_SCGI ){ cgi_handle_scgi_request(); + }else if( g.httpUseSSL ){ +#if FOSSIL_ENABLE_SSL + g.httpSSLConn = ssl_new_server(0,-1); +#endif + cgi_handle_http_request(0); }else{ cgi_handle_http_request(0); } process_one_web_page(zNotFound, glob_create(zFileGlob), allowRepoList); if( g.fAnyTrace ){ fprintf(stderr, "/***** Webpage finished in subprocess %d *****/\n", getpid()); } -#else +#if FOSSIL_ENABLE_SSL + if( g.httpUseSSL && g.httpSSLConn ){ + ssl_close_server(g.httpSSLConn); + g.httpSSLConn = 0; + } +#endif /* FOSSIL_ENABLE_SSL */ + +#else /* WIN32 */ /* Win32 implementation */ + if( g.httpUseSSL ){ + fossil_fatal("TLS-encrypted server is not (yet) supported on Windows"); + } if( allowRepoList ){ flags |= HTTP_SERVER_REPOLIST; } if( win32_http_service(iPort, zAltBase, zNotFound, zFileGlob, flags) ){ win32_http_server(iPort, mxPort, zBrowserCmd, zStopperFile, Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -916,10 +916,11 @@ delete_private_content(); } if( !privateOnly ){ db_unprotect(PROTECT_ALL); db_multi_exec( + "PRAGMA secure_delete=ON;" "UPDATE user SET pw='';" "DELETE FROM config WHERE name IN" "(WITH pattern(x) AS (VALUES" " ('baseurl:*')," " ('cert:*')," @@ -933,11 +934,12 @@ " ('peer-*')," " ('skin:*')," " ('subrepo:*')," " ('sync-*')," " ('syncfrom:*')," - " ('syncwith:*')" + " ('syncwith:*')," + " ('ssl-*')" ") SELECT name FROM config, pattern WHERE name GLOB x);" ); if( bVerily ){ db_multi_exec( "DELETE FROM concealed;\n"