From: Art Cancro Date: Tue, 28 Dec 2021 21:31:53 +0000 (-0500) Subject: WebCit-NG: X-Git-Tag: v943~7 X-Git-Url: https://code.citadel.org/?a=commitdiff_plain;h=02d9ac29a60e4d76d4f87a8fb155fbeb7f72e3f5;p=citadel.git WebCit-NG: * Force webcit to be on the same host as citserver * Remove local keys directory, use the ones from citserver directory * Auto re-bind key and cert if either one changes * Support .well-known directory for static content, supporting HTTP-01 --- diff --git a/citadel/modules/crypto/serv_crypto.h b/citadel/modules/crypto/serv_crypto.h index 90c8d2604..00a94b13d 100644 --- a/citadel/modules/crypto/serv_crypto.h +++ b/citadel/modules/crypto/serv_crypto.h @@ -2,7 +2,7 @@ /* * Number of days for which self-signed certs are valid. */ -#define SIGN_DAYS 3650 /* Ten years */ +#define SIGN_DAYS 1100 // Just over three years /* Shared Diffie-Hellman parameters */ #define DH_P "1A74527AEE4EE2568E85D4FB2E65E18C9394B9C80C42507D7A6A0DBE9A9A54B05A9A96800C34C7AA5297095B69C88901EEFD127F969DCA26A54C0E0B5C5473EBAEB00957D2633ECAE3835775425DE66C0DE6D024DBB17445E06E6B0C78415E589B8814F08531D02FD43778451E7685541079CFFB79EF0D26EFEEBBB69D1E80383" diff --git a/webcit-ng/ctdlclient.c b/webcit-ng/ctdlclient.c index a5a25f9e5..389fd9838 100644 --- a/webcit-ng/ctdlclient.c +++ b/webcit-ng/ctdlclient.c @@ -132,96 +132,6 @@ int uds_connectsock(char *sockpath) { } -// TCP client - connect to a host/port -int tcp_connectsock(char *host, char *service) { - struct in6_addr serveraddr; - struct addrinfo hints; - struct addrinfo *res = NULL; - struct addrinfo *ai = NULL; - int rc = (-1); - int s = (-1); - - if ((host == NULL) || IsEmptyStr(host)) - return (-1); - if ((service == NULL) || IsEmptyStr(service)) - return (-1); - - syslog(LOG_DEBUG, "tcp_connectsock(%s,%s)", host, service); - - memset(&hints, 0x00, sizeof(hints)); - hints.ai_flags = AI_NUMERICSERV; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - // Handle numeric IPv4 and IPv6 addresses - rc = inet_pton(AF_INET, host, &serveraddr); - if (rc == 1) { // dotted quad - hints.ai_family = AF_INET; - hints.ai_flags |= AI_NUMERICHOST; - } - else { - rc = inet_pton(AF_INET6, host, &serveraddr); - if (rc == 1) { // IPv6 address - hints.ai_family = AF_INET6; - hints.ai_flags |= AI_NUMERICHOST; - } - } - - // Begin the connection process - - rc = getaddrinfo(host, service, &hints, &res); - if (rc != 0) { - syslog(LOG_DEBUG, "%s: %s", host, gai_strerror(rc)); - freeaddrinfo(res); - return (-1); - } - - // Try all available addresses until we connect to one or until we run out. - for (ai = res; ai != NULL; ai = ai->ai_next) { - - if (ai->ai_family == AF_INET) - syslog(LOG_DEBUG, "Trying IPv4"); - else if (ai->ai_family == AF_INET6) - syslog(LOG_DEBUG, "Trying IPv6"); - else - syslog(LOG_WARNING, "This is going to fail."); - - s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); - if (s < 0) { - syslog(LOG_WARNING, "socket() failed: %s", strerror(errno)); - freeaddrinfo(res); - return (-1); - } - rc = connect(s, ai->ai_addr, ai->ai_addrlen); - if (rc >= 0) { - int fdflags; - freeaddrinfo(res); - - fdflags = fcntl(rc, F_GETFL); - if (fdflags < 0) { - syslog(LOG_ERR, "unable to get socket %d flags! %s", rc, strerror(errno)); - close(rc); - return -1; - } - fdflags = fdflags | O_NONBLOCK; - if (fcntl(rc, F_SETFL, fdflags) < 0) { - syslog(LOG_ERR, "unable to set socket %d nonblocking flags! %s", rc, strerror(errno)); - close(s); - return -1; - } - - return (s); - } - else { - syslog(LOG_WARNING, "connect() failed: %s", strerror(errno)); - close(s); - } - } - freeaddrinfo(res); - return (-1); -} - - // Extract from the headers, the username and password the client is attempting to use. // This could be HTTP AUTH or it could be in the cookies. void extract_auth(struct http_transaction *h, char *authbuf, int authbuflen) { @@ -366,7 +276,12 @@ struct ctdlsession *connect_to_citadel(struct http_transaction *h) { if (is_new_session) { strcpy(my_session->room, ""); - my_session->sock = tcp_connectsock(ctdlhost, ctdlport); + static char *ctdl_sock_path = NULL; + if (!ctdl_sock_path) { + ctdl_sock_path = malloc(PATH_MAX); + snprintf(ctdl_sock_path, PATH_MAX, "%s/citadel.socket", ctdl_dir); + } + my_session->sock = uds_connectsock(ctdl_sock_path); ctdl_readline(my_session, buf, sizeof(buf)); // skip past the server greeting banner if (!IsEmptyStr(auth)) { // do we need to log in to Citadel? diff --git a/webcit-ng/http.c b/webcit-ng/http.c index 47a5bf66b..8a478f638 100644 --- a/webcit-ng/http.c +++ b/webcit-ng/http.c @@ -159,7 +159,7 @@ void perform_one_http_transaction(struct client_handle *ch) { } // If the URL had any query parameters in it, parse them out now. - char *p = strchr(h.url, '?'); + char *p = (h.url ? strchr(h.url, '?') : NULL); if (p) { *p++ = 0; // insert a null to remove parameters from the URL char *tok, *saveptr = NULL; diff --git a/webcit-ng/main.c b/webcit-ng/main.c index 958b4ea40..d1089f6d0 100644 --- a/webcit-ng/main.c +++ b/webcit-ng/main.c @@ -14,8 +14,7 @@ #include "webcit.h" // All other headers are included from this header. -char *ctdlhost = CTDLHOST; -char *ctdlport = CTDLPORT; +char *ctdl_dir = CTDL_DIR; // Main entry point for the web server. int main(int argc, char **argv) { @@ -86,14 +85,15 @@ int main(int argc, char **argv) { "[-d] [-Z] [-G i18ndumpfile] " "[-u uid] [-h homedirectory] " "[-D daemonizepid] [-v] " - "[-g defaultlandingpage] [-B basesize] " "[-s] [-S cipher_suites]" "[remotehost [remoteport]]\n"); + "[-g defaultlandingpage] [-B basesize] " + "[-s] [-S cipher_suites]" + "[citadel_directory]\n" + ); return 1; } - if (optind < argc) { - ctdlhost = argv[optind]; - if (++optind < argc) - ctdlport = argv[optind]; + while (optind < argc) { + ctdl_dir = strdup(argv[optind++]); } // Start the logger diff --git a/webcit-ng/request.c b/webcit-ng/request.c index 8ea0893c9..a27a65ac2 100644 --- a/webcit-ng/request.c +++ b/webcit-ng/request.c @@ -4,7 +4,7 @@ // and pass control back down to the HTTP layer to output the response back to // the client. // -// Copyright (c) 1996-2021 by the citadel.org team +// Copyright (c) 1996-2022 by the citadel.org team // // This program is open source software. It runs great on the // Linux operating system (and probably elsewhere). You can use, @@ -91,14 +91,20 @@ void perform_request(struct http_transaction *h) { // Legacy URL patterns (/readnew?gotoroom=xxx&start_reading_at=yyy) ... // Direct room name (/my%20blog) ... + // HTTP-01 challenge [RFC5785 section 3, RFC8555 section 9.2] + if (!strncasecmp(h->url, HKEY("/.well-known"))) { // Static content + output_static(h); + return; + } + // Everything below this line is strictly REST URL patterns. - if (strncasecmp(h->url, HKEY("/ctdl/"))) { // Reject non-REST + if (strncasecmp(h->url, HKEY("/ctdl/"))) { // Reject non-REST do_404(h); return; } - if (!strncasecmp(h->url, HKEY("/ctdl/s/"))) { // Static content + if (!strncasecmp(h->url, HKEY("/ctdl/s/"))) { // Static content output_static(h); return; } diff --git a/webcit-ng/ssl.c b/webcit-ng/ssl.c index 9de67d1cd..ee970d475 100644 --- a/webcit-ng/ssl.c +++ b/webcit-ng/ssl.c @@ -2,7 +2,7 @@ // Functions in this module handle SSL encryption when WebCit is running // as an HTTPS server. // -// Copyright (c) 1996-2021 by the citadel.org team +// Copyright (c) 1996-2022 by the citadel.org team // // This program is open source software. It runs great on the // Linux operating system (and probably elsewhere). You can use, @@ -16,58 +16,30 @@ #include "webcit.h" -SSL_CTX *ssl_ctx; // SSL context +SSL_CTX *ssl_ctx; // global SSL context +char key_file[PATH_MAX] = ""; +char cert_file[PATH_MAX] = ""; char *ssl_cipher_list = DEFAULT_SSL_CIPHER_LIST; -// Generate a private key for SSL -void generate_key(char *keyfilename) { - int ret = 0; - RSA *rsa = NULL; - BIGNUM *bne = NULL; - int bits = 2048; - unsigned long e = RSA_F4; - FILE *fp; - - if (access(keyfilename, R_OK) == 0) { - return; +// Set the private key and certificate chain for the global SSL Context. +// This is called during initialization, and can be called again later if the certificate changes. +void bind_to_key_and_certificate(void) { + if (IsEmptyStr(key_file)) { + snprintf(key_file, sizeof key_file, "%s/keys/citadel.key", ctdl_dir); + } + if (IsEmptyStr(cert_file)) { + snprintf(cert_file, sizeof key_file, "%s/keys/citadel.cer", ctdl_dir); } - syslog(LOG_INFO, "crypto: generating RSA key pair"); + syslog(LOG_DEBUG, "crypto: [re]installing key \"%s\" and certificate \"%s\"", key_file, cert_file); - // generate rsa key - bne = BN_new(); - ret = BN_set_word(bne, e); - if (ret != 1) { - goto free_all; - } + SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_file); + SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM); - rsa = RSA_new(); - ret = RSA_generate_key_ex(rsa, bits, bne, NULL); - if (ret != 1) { - goto free_all; + if ( !SSL_CTX_check_private_key(ssl_ctx) ) { + syslog(LOG_WARNING, "crypto: cannot install certificate: %s", ERR_reason_error_string(ERR_get_error())); } - // write the key file - fp = fopen(keyfilename, "w"); - if (fp != NULL) { - chmod(keyfilename, 0600); - if (PEM_write_RSAPrivateKey(fp, // the file */ - rsa, // the key */ - NULL, // no enc */ - NULL, // no passphrase - 0, // no passphrase - NULL, // no callback - NULL // no callbk - ) != 1) { - syslog(LOG_ERR, "crypto: cannot write key: %s", ERR_reason_error_string(ERR_get_error())); - unlink(keyfilename); - } - fclose(fp); - } - // 4. free - free_all: - RSA_free(rsa); - BN_free(bne); } @@ -99,141 +71,30 @@ void init_ssl(void) { return; } - // Get our certificates in order. - // First, create the key/cert directory if it's not there already... - mkdir(CTDL_CRYPTO_DIR, 0700); - - // If we still don't have a private key, generate one. - generate_key(CTDL_KEY_PATH); - - // If there is no certificate file on disk, we will be generating a self-signed certificate - // in the next step. Therefore, if we have neither a CSR nor a certificate, generate - // the CSR in this step so that the next step may commence. - if ((access(CTDL_CER_PATH, R_OK) != 0) && (access(CTDL_CSR_PATH, R_OK) != 0)) { - syslog(LOG_INFO, "Generating a certificate signing request."); - - // Read our key from the file. No, we don't just keep this - // in memory from the above key-generation function, because - // there is the possibility that the key was already on disk - // and we didn't just generate it now. - fp = fopen(CTDL_KEY_PATH, "r"); - if (fp) { - rsa = PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL); - fclose(fp); - } - - if (rsa) { - // Create a public key from the private key - if (pk = EVP_PKEY_new(), pk != NULL) { - EVP_PKEY_assign_RSA(pk, rsa); - if (req = X509_REQ_new(), req != NULL) { - const char *env; - // Set the public key - X509_REQ_set_pubkey(req, pk); - X509_REQ_set_version(req, 0L); - name = X509_REQ_get_subject_name(req); - X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *) "Citadel Server", -1, -1, 0); - X509_NAME_add_entry_by_txt(name, "OU", MBSTRING_ASC, (unsigned char *) "Default Certificate PLEASE CHANGE", -1, -1, 0); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *) "*", -1, -1, 0); - X509_REQ_set_subject_name(req, name); - - // Sign the CSR - if (!X509_REQ_sign(req, pk, EVP_md5())) { - syslog(LOG_WARNING, "X509_REQ_sign(): error"); - } - else { - // Write it to disk - fp = fopen(CTDL_CSR_PATH, "w"); - if (fp != NULL) { - chmod(CTDL_CSR_PATH, 0600); - PEM_write_X509_REQ(fp, req); - fclose(fp); - } - else { - syslog(LOG_WARNING, "Cannot write key: %s", CTDL_CSR_PATH); - exit(1); - } - } - X509_REQ_free(req); - } - } - RSA_free(rsa); - } - else { - syslog(LOG_WARNING, "Unable to read private key."); - } - } - - // Generate a self-signed certificate if we don't have one. - if (access(CTDL_CER_PATH, R_OK) != 0) { - syslog(LOG_INFO, "Generating a self-signed certificate."); - - // Same deal as before: always read the key from disk because - // it may or may not have just been generated. - fp = fopen(CTDL_KEY_PATH, "r"); - if (fp) { - rsa = PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL); - fclose(fp); - } + // Now try to bind to the key and certificate. + bind_to_key_and_certificate(); +} - // This also holds true for the CSR - req = NULL; - cer = NULL; - pk = NULL; - if (rsa) { - if (pk = EVP_PKEY_new(), pk != NULL) { - EVP_PKEY_assign_RSA(pk, rsa); - } - fp = fopen(CTDL_CSR_PATH, "r"); - if (fp) { - req = PEM_read_X509_REQ(fp, NULL, NULL, NULL); - fclose(fp); - } +// Check the modification time of the key and certificate -- reload if they changed +void update_key_and_cert_if_needed(void) { + static time_t cert_mtime = 0; + struct stat keystat; + struct stat certstat; - if (req) { - if (cer = X509_new(), cer != NULL) { - ASN1_INTEGER_set(X509_get_serialNumber(cer), 0); - X509_set_issuer_name(cer, X509_REQ_get_subject_name(req)); - X509_set_subject_name(cer, X509_REQ_get_subject_name(req)); - X509_gmtime_adj(X509_get_notBefore(cer), 0); - X509_gmtime_adj(X509_get_notAfter(cer), (long) 60 * 60 * 24 * SIGN_DAYS); - req_pkey = X509_REQ_get_pubkey(req); - X509_set_pubkey(cer, req_pkey); - EVP_PKEY_free(req_pkey); - - // Sign the cert - if (!X509_sign(cer, pk, EVP_md5())) { - syslog(LOG_WARNING, "X509_sign(): error"); - } - else { // Write it to disk - fp = fopen(CTDL_CER_PATH, "w"); - if (fp != NULL) { - chmod(CTDL_CER_PATH, 0600); - PEM_write_X509(fp, cer); - fclose(fp); - } - else { - syslog(LOG_WARNING, "Cannot write key: %s", CTDL_CER_PATH); - exit(1); - } - } - X509_free(cer); - } - } - RSA_free(rsa); - } + if (stat(key_file, &keystat) != 0) { + syslog(LOG_ERR, "%s: %s", key_file, strerror(errno)); + return; } - - // Now try to bind to the key and certificate. - // Note that we use SSL_CTX_use_certificate_chain_file() which allows - // the certificate file to contain intermediate certificates. - SSL_CTX_use_certificate_chain_file(ssl_ctx, CTDL_CER_PATH); - SSL_CTX_use_PrivateKey_file(ssl_ctx, CTDL_KEY_PATH, SSL_FILETYPE_PEM); - if (!SSL_CTX_check_private_key(ssl_ctx)) { - syslog(LOG_WARNING, "Cannot install certificate: %s", ERR_reason_error_string(ERR_get_error())); + if (stat(cert_file, &certstat) != 0) { + syslog(LOG_ERR, "%s: %s", cert_file, strerror(errno)); + return; } + if ((keystat.st_mtime > cert_mtime) || (certstat.st_mtime > cert_mtime)) { + bind_to_key_and_certificate(); + cert_mtime = certstat.st_mtime; + } } @@ -244,6 +105,10 @@ void starttls(struct client_handle *ch) { if (!ssl_ctx) { return; } + + // Check the modification time of the key and certificate -- reload if they changed + update_key_and_cert_if_needed(); + if (!(ch->ssl_handle = SSL_new(ssl_ctx))) { syslog(LOG_WARNING, "SSL_new failed: %s", ERR_reason_error_string(ERR_get_error())); return; @@ -263,28 +128,28 @@ void starttls(struct client_handle *ch) { if (ssl_error_reason == NULL) { syslog(LOG_WARNING, "SSL_accept failed: errval=%ld, retval=%d %s", errval, retval, strerror(errval)); } - else { - syslog(LOG_WARNING, "SSL_accept failed: %s\n", ssl_error_reason); - } - sleep(1); - retval = SSL_accept(ch->ssl_handle); - } - if (retval < 1) { - long errval; - const char *ssl_error_reason = NULL; - - errval = SSL_get_error(ch->ssl_handle, retval); - ssl_error_reason = ERR_reason_error_string(ERR_get_error()); - if (ssl_error_reason == NULL) { - syslog(LOG_WARNING, "SSL_accept failed: errval=%ld, retval=%d (%s)", errval, retval, strerror(errval)); - } else { syslog(LOG_WARNING, "SSL_accept failed: %s", ssl_error_reason); } - SSL_free(ch->ssl_handle); - ch->ssl_handle = NULL; - return; + //sleep(1); + //retval = SSL_accept(ch->ssl_handle); } + //if (retval < 1) { + //long errval; + //const char *ssl_error_reason = NULL; +// + //errval = SSL_get_error(ch->ssl_handle, retval); + //ssl_error_reason = ERR_reason_error_string(ERR_get_error()); + //if (ssl_error_reason == NULL) { + //syslog(LOG_WARNING, "SSL_accept failed: errval=%ld, retval=%d (%s)", errval, retval, strerror(errval)); + //} + //else { + //syslog(LOG_WARNING, "SSL_accept failed: %s", ssl_error_reason); + //} + //SSL_free(ch->ssl_handle); + //ch->ssl_handle = NULL; + //return; + //} else { syslog(LOG_INFO, "SSL_accept success"); } @@ -292,7 +157,6 @@ void starttls(struct client_handle *ch) { syslog(LOG_INFO, "SSL/TLS using %s on %s (%d of %d bits)", SSL_CIPHER_get_name(SSL_get_current_cipher(ch->ssl_handle)), SSL_CIPHER_get_version(SSL_get_current_cipher(ch->ssl_handle)), bits, alg_bits); - syslog(LOG_INFO, "SSL started"); } @@ -334,7 +198,7 @@ int client_write_ssl(struct client_handle *ch, char *buf, int nbytes) { sleep(1); continue; } - syslog(LOG_WARNING, "SSL_write got error %ld, ret %d", errval, retval); + syslog(LOG_WARNING, "SSL_write: %s", ERR_reason_error_string(ERR_get_error())); if (retval == -1) { syslog(LOG_WARNING, "errno is %d", errno); endtls(ch); diff --git a/webcit-ng/static.c b/webcit-ng/static.c index a523b4928..dbc846524 100644 --- a/webcit-ng/static.c +++ b/webcit-ng/static.c @@ -15,12 +15,21 @@ #include "webcit.h" -// Called from perform_request() to handle the /ctdl/s/ prefix -- always static content. +// Called from perform_request() to handle static content. void output_static(struct http_transaction *h) { char filename[PATH_MAX]; struct stat statbuf; - snprintf(filename, sizeof filename, "static/%s", &h->url[8]); + if (!strncasecmp(h->url, "/ctdl/s/", 8)) { + snprintf(filename, sizeof filename, "static/%s", &h->url[8]); + } + else if (!strncasecmp(h->url, "/.well-known/", 13)) { + snprintf(filename, sizeof filename, "static/.well-known/%s", &h->url[13]); + } + else { + do_404(h); + return; + } if (strstr(filename, "../")) { // 100% guaranteed attacker. do_404(h); // Die in a car fire. @@ -29,6 +38,7 @@ void output_static(struct http_transaction *h) { FILE *fp = fopen(filename, "r"); // Try to open the requested file. if (fp == NULL) { + syslog(LOG_DEBUG, "%s: %s", filename, strerror(errno)); do_404(h); return; } diff --git a/webcit-ng/webcit.h b/webcit-ng/webcit.h index c3e326dfb..53984d63e 100644 --- a/webcit-ng/webcit.h +++ b/webcit-ng/webcit.h @@ -92,8 +92,7 @@ struct ctdlsession { extern char *ssl_cipher_list; extern int is_https; // nonzero if we are an HTTPS server today -extern char *ctdlhost; -extern char *ctdlport; +extern char *ctdl_dir; // directory where Citadel Server is running void init_ssl(void); void starttls(struct client_handle *); void endtls(struct client_handle *); @@ -109,19 +108,13 @@ enum { #define TRACE syslog(LOG_DEBUG, "\033[3%dmCHECKPOINT: %s:%d\033[0m", ((__LINE__%6)+1), __FILE__, __LINE__) #define SLEEPING 180 // TCP connection timeout #define MAX_WORKER_THREADS 32 // Maximum number of worker threads permitted to exist -#define CTDL_CRYPTO_DIR "keys" -#define CTDL_KEY_PATH CTDL_CRYPTO_DIR "/webcit.key" -#define CTDL_CSR_PATH CTDL_CRYPTO_DIR "/webcit.csr" -#define CTDL_CER_PATH CTDL_CRYPTO_DIR "/webcit.cer" -#define SIGN_DAYS 3650 // how long our certificate should live #define DEFAULT_SSL_CIPHER_LIST "DEFAULT" // See http://openssl.org/docs/apps/ciphers.html #define WEBSERVER_PORT 80 #define WEBSERVER_INTERFACE "*" -#define CTDLHOST "dev.citadel.org" -#define CTDLPORT "504" +#define CTDL_DIR "/usr/local/citadel" #define DEVELOPER_ID 0 #define CLIENT_ID 4 -#define TARGET "webcit01" /* Window target for inline URL's */ +#define TARGET "webcit02" /* Window target for inline URL's */ void worker_entry(int *pointer_to_master_socket); void perform_one_http_transaction(struct client_handle *ch); @@ -130,7 +123,6 @@ void perform_request(struct http_transaction *); void do_404(struct http_transaction *); void output_static(struct http_transaction *); int uds_connectsock(char *sockpath); -int tcp_connectsock(char *host, char *service); void ctdl_a(struct http_transaction *, struct ctdlsession *); void ctdl_r(struct http_transaction *, struct ctdlsession *); void ctdl_u(struct http_transaction *, struct ctdlsession *);