/*****************************************************************************/ /* https.c This module is a very basic, single-threaded, HTTP client for TLS services. By default it is TLS, it will also operate on cleartext sockets. All external usage is via httpsGet..() and httpsPost..() calls. Provides a more easily maintained VMS networking than (uacme) using cURL. COPYRIGHT --------- Copyright (C) 2020,2024 Mark G.Daniel This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version. http://www.gnu.org/licenses/gpl.txt VERSION HISTORY --------------- 05-JAN-2021 MGD v1.1.5, HttpsVerifyConnect() less specific, more flexible 06-DEC-2019 MGD initial */ /*****************************************************************************/ #include #include #include #include #include #include #include #include #include #include "wucme.h" #include "read-file.h" extern int dbug; static struct requestStruct { int clength, errline, hlength, hsize, https, portnum, post, rcode, rlength, socket; char *content, *ctype, *error, *header, *hostname, *response, *uri; SSL *ssl; } requestData; static struct requestStruct *request = &requestData; static SSL_CTX *httpsCtx; extern char lets_issuer[], lets_subject[], SoftwareId[]; static int httpsConnect (void); static int httpsMakeRequest (void); static int httpsSetError (int, char*, ...); static void httpsResetRequest (void); static void httpsSetHostName (char*); static void httpsSetPortNum (int); static void httpsSetPost (int); static int httpsSetSSLError (int); static void httpsSetUri (char*); static void httpsSetRequest (char*); static void httpsSetContent (char*, char*, int); static void httpsSetHeader (char*, char*); static void httpsShowConnect (void); static int httpsVerifyConnect (void); /*****************************************************************************/ /* Return the integer response HTTP code. */ int httpsGetResponseCode (void) { return (request->rcode); } /*****************************************************************************/ /* Return a pointer to the full response (header then body). */ char* httpsGetResponseFull (void) { return (request->response); } /*****************************************************************************/ /* Return the integer length of the full response (header then body). */ int httpsGetResponseLength (void) { return (request->rlength); } /*****************************************************************************/ /* Return a pointer to the full response header. */ char* httpsGetHeader (void) { char *cptr; cptr = calloc (1, request->hlength); memcpy (cptr, request->response, request->hlength); return (cptr); } /*****************************************************************************/ /* Return the integer length of the full response (header then body). */ int httpsGetHeaderLength (void) { return (request->hlength); } /*****************************************************************************/ /* Return a pointer to the response content (body). */ char* httpsGetContent (void) { char *cptr; cptr = calloc (1, request->clength); memcpy (cptr, request->content, request->clength); return (cptr); } /*****************************************************************************/ /* Return the integer length of any response body. */ int httpsGetContentLength (void) { return (request->clength); } /*****************************************************************************/ /* Return a pointer to the response content type (e.g. "text/html"). */ char* httpsGetContentType (void) { return (request->ctype); } /*****************************************************************************/ /* Return a pointer to a respresentative error string. Can be NULL if no error. */ char* httpsGetError (void) { return (request->error); } /*****************************************************************************/ /* */ static SSL_CTX* InitCTX (void) { const SSL_METHOD *method; SSL_CTX *ctx; SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); method = TLSv1_2_client_method(); ctx = SSL_CTX_new (method); if (ctx == NULL) ERR_print_errors_fp (stderr); return (ctx); } /*****************************************************************************/ /* Explicitly set the port number. */ static void httpsSetPortNum (int portnum) { request->portnum = portnum; } /*****************************************************************************/ /* Explicitly set if zero then a GET request otherwise POST request. */ static void httpsSetPost (int post) { request->post = post; } /*****************************************************************************/ /* Set the server host name. */ static void httpsSetHostName (char *hostname) { request->hostname = calloc (1, strlen(hostname) + 1); if (!request->hostname) exit (vaxc$errno); strcpy (request->hostname, hostname); } /*****************************************************************************/ /* Set the server resource identifier. */ static void httpsSetUri (char *uri) { request->uri = calloc (1, strlen(uri) + 1); if (!request->uri) exit (vaxc$errno); strcpy (request->uri, uri); } /*****************************************************************************/ /* Using a ://[: string, set all of those request properties. If |post| is zero then a GET request otherwise POST request. */ static void httpsSetRequest (char *url) { char *aptr, *cptr; cptr = url; request->https = 1; if (!strncmp (cptr, "https://", 8)) cptr += 8; else if (!strncmp (cptr, "http://", 7)) { cptr += 7; request->https = 0; } for (aptr = cptr; *cptr && *cptr != ':' && *cptr != '/'; cptr++); request->hostname = calloc (1, cptr - aptr + 1); if (!request->hostname) exit (vaxc$errno); memcpy (request->hostname, aptr, cptr - aptr); if (*cptr == ':') request->portnum = atoi(cptr+1); else if (request->https) request->portnum = 443; else request->portnum = 80; while (*cptr && *cptr != '/') cptr++; if (*cptr) { for (aptr = cptr; *cptr; cptr++); request->uri = calloc (1, cptr - aptr + 1); if (!request->uri) exit (vaxc$errno); memcpy (request->uri, aptr, cptr - aptr); } else { request->uri = calloc (1, cptr - aptr + 1); if (!request->uri) exit (vaxc$errno); *request->uri = '/'; } } /*****************************************************************************/ /* For a POST request add content (body) and content type. */ static void httpsSetContent (char *ctype, char *content, int clength) { if (clength < 0) clength = strlen(content); request->content = calloc (1, clength + 1); if (!request->content) exit (vaxc$errno); strcpy (request->content, content); request->clength = clength; request->ctype = calloc (1, strlen(ctype) + 1); if (!request->ctype) exit (vaxc$errno); strcpy (request->ctype, ctype); } /*****************************************************************************/ /* Add request header field (name and value). If |value| is NULL then the |name| string contains a fully specified field name and value including carriage control. The string can contain multiple such fields. */ static void httpsSetHeader (char *name, char* value) { #define HINC 2048 char *cptr, *sptr, *zptr; for (;;) { if (!name && !value) break; if (request->hlength >= request->hsize) { request->hsize += HINC; request->header = realloc (request->header, request->hsize); if (!request->header) exit (vaxc$errno); /* seems we must zero any additional allocation! */ memset (request->header + request->hlength, 0, HINC); } zptr = (sptr = request->header) + request->hsize - 8; sptr += request->hlength; if (name) while (*name && sptr < zptr) *sptr++ = *name++; request->hlength = sptr - request->header; if (sptr >= zptr) continue; if (name && !*name && value) { *sptr++ = ':'; *sptr++ = ' '; } if (!*name) name = NULL; if (!name && !value) break; if (value) while (*value && sptr < zptr) *sptr++ = *value++; request->hlength = sptr - request->header; if (sptr >= zptr) continue; if (value && !*value) { *sptr++ = '\r'; *sptr++ = '\n'; value = NULL; } request->hlength = sptr - request->header; } } /*****************************************************************************/ /* Reset the request data and connection. */ void httpsResetRequest (void) { if (request->ssl) SSL_free (request->ssl); if (request->socket > 0) close (request->socket); if (request->content) free (request->content); if (request->ctype) free (request->ctype); if (request->error) free (request->error); if (request->header) free (request->header); if (request->hostname) free (request->hostname); if (request->response) free (request->response); if (request->uri) free (request->uri); memset (request, 0, sizeof(struct requestStruct)); } /*****************************************************************************/ /* Establish a network connection to the server host and port. */ static int httpsConnect (void) { int sd; struct hostent *host; struct sockaddr_in addr; if ((host = gethostbyname (request->hostname)) == NULL) { httpsSetError (__LINE__, "%s %s", request->hostname, strerror(errno)); return (errno); } sd = socket(PF_INET, SOCK_STREAM, 0); memset (&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(request->portnum); addr.sin_addr.s_addr = *(long*)(host->h_addr); if (connect (sd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close (sd); httpsSetError (__LINE__, "%s %s", request->hostname, strerror(errno)); return (errno); } request->socket = sd; if (!request->https) return (0); if (!httpsCtx) { /* once established this context is never freed */ httpsCtx = InitCTX(); if (!httpsCtx) { httpsSetSSLError (__LINE__); return (errno); } } request->ssl = SSL_new (httpsCtx); SSL_set_fd (request->ssl, request->socket); if (SSL_connect (request->ssl) < 0) { httpsSetSSLError (__LINE__); SSL_free (request->ssl); request->ssl = NULL; } if (!httpsVerifyConnect ()) { SSL_free (request->ssl); request->ssl = NULL; return (EPERM); } #if 0 httpsShowConnect(); #endif return (0); } /*****************************************************************************/ /* Compare the known Let's Encrypt common name and issuer values (or relevant parts thereof) to the server's certificate detail. Return true if they match and false if not. */ static int httpsVerifyConnect (void) { int oki = 0, oks = 0; char *cptr; X509 *cert; if (!request->ssl) return (0); if (!(cert = SSL_get_peer_certificate (request->ssl))) return (0); cptr = X509_NAME_oneline (X509_get_subject_name(cert), 0, 0); /* e.g. "/CN=acme-v01.api.letsencrypt.org" */ if (!strncmp (cptr, "/CN=acme-", 9) && strstr (cptr+9, ".api.letsencrypt.org")) oks = 1; if (!oks) httpsSetError (__LINE__, "verify subject \"%s\" failed", cptr); free (cptr); cptr = X509_NAME_oneline (X509_get_issuer_name(cert), 0, 0); /* e.g. "/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3" */ if (!strncmp (cptr, "/C=US/O=Let's Encrypt/CN=", 25)) oki = 1; if (!oki) httpsSetError (__LINE__, "verify issuer \"%s\" failed", cptr); free (cptr); X509_free(cert); return (oki && oks); } /*****************************************************************************/ /* Once the request data is set up and the server connected make the request, then receive and process the response. */ static int httpsMakeRequest (void) { #define RINC 4096 int count, length, retval, size; char clength [32], header [4096]; char *bptr, *cptr, *czptr; /****************/ /* make request */ /****************/ /* didn't work (by default) */ request->rcode = -1; if (httpsConnect ()) return (request->rcode); if (request->post) { if (request->content) { httpsSetHeader ("Content-Type", request->ctype); sprintf (clength, "%d", request->clength); httpsSetHeader ("Content-Length", clength); } else httpsSetHeader ("Content-Length", "0"); } count = sprintf (header, "%s %s HTTP/1.0\r\n\ Host: %s\r\n\ User-Agent: %s\r\n\ Connection: close\r\n\ %s\ \r\n", request->post ? "POST" : "GET", request->uri, request->hostname, SoftwareId, request->header ? request->header : ""); if (count >= sizeof(header)) exit (SS$_BUGCHECK); if (dbug) fprintf (stdout, "request %d bytes\n%s\n", count, header); if (request->https) SSL_write (request->ssl, header, count); else write (request->socket, header, count); if (request->post) { if (dbug) fprintf (stdout, "content %d bytes\n%s\n", request->clength, request->content); if (request->https) SSL_write (request->ssl, request->content, request->clength); else write (request->socket, request->content, request->clength); } /****************/ /* get response */ /****************/ length = size = 0; bptr = NULL; for (;;) { if (length >= size) { size += RINC; bptr = realloc (bptr, size); /* seems we must zero any additional allocation! */ memset (bptr + length, 0, RINC); } if (request->https) count = SSL_read (request->ssl, bptr + length, size - length); else count = read (request->socket, bptr + length, size - length); if (count <= 0) break; length += count; if (dbug) fprintf (stdout, "part %d bytes\n%s\n", count, bptr); } if (dbug) fprintf (stdout, "response %d bytes\n%s\n", length, bptr); request->response = bptr; request->rlength = length; /******************/ /* parse response */ /******************/ /* these may be reused for the response */ request->clength = request->hlength = 0; if (request->ctype) free (request->ctype); request->ctype = NULL; czptr = (cptr = bptr) + length; if (memcmp (cptr, "HTTP/1.", 7) || !isdigit(cptr[7])) { request->rcode = 0; return (request->rcode); } for (cptr += 8; cptr < czptr && *cptr == ' '; cptr++); request->rcode = atoi(cptr); while (cptr < czptr && *cptr != '\n') cptr++; if (*cptr == '\n') cptr++; while (cptr < czptr) { bptr = cptr; while (cptr < czptr && *cptr != '\r' && *cptr != '\n') cptr++; if (cptr >= czptr) break; if (!strncasecmp (bptr, "Content-Type:", 13)) { for (bptr += 13; bptr < cptr && *bptr == ' '; bptr++); request->ctype = calloc (1, cptr - bptr + 1); memcpy (request->ctype, bptr, cptr - bptr); } else if (!strncasecmp (bptr, "Content-Length:", 15)) { for (bptr += 15; bptr < cptr && *bptr == ' '; bptr++); request->clength = atoi(bptr); } if (*cptr == '\r') cptr++; if (*cptr == '\n') cptr++; if (*cptr == '\n' || (cptr[0] == '\r' && cptr[1] == '\n')) break; } request->hlength = cptr - request->response; if (request->clength) { if (*cptr == '\r') cptr++; if (*cptr == '\n') cptr++; request->content = cptr; } else request->content = strdup(""); return (request->rcode); } /*****************************************************************************/ /* Set the error string. Only the first is maintained. */ static int httpsSetError (int lnum, char* format, ...) { int count; char *fptr; va_list ap; if (request->error) return (-1); count = asprintf (&fptr, "%s (%d)", format, lnum); if (count <= 0) return (count); va_start (ap, format); count = vasprintf (&request->error, fptr, ap); va_end (ap); free (fptr); return (count); } /*****************************************************************************/ /* */ static int httpsSetSSLError (int lnum) { int count; ulong error; char *sptr = NULL; char errbuf [256]; if (request->error) return (-1); while (error = ERR_get_error()) { ERR_error_string_n (error, errbuf, sizeof(errbuf)); if (sptr) count = asprintf (&request->error, "%s (%d)", errbuf, lnum); else count = asprintf (&request->error, "%s+%s", sptr, errbuf); sptr = request->error; if (count <= 0) break; } return (count); } /*****************************************************************************/ /* */ static void httpsShowConnect (void) { X509 *cert; char *cptr; if (!request->ssl) return; printf ("Encryption: %s\n", SSL_get_cipher (request->ssl)); cert = SSL_get_peer_certificate (request->ssl); if ( cert != NULL ) { printf("Server certificates:\n"); cptr = X509_NAME_oneline (X509_get_subject_name(cert), 0, 0); printf("Subject: %s\n", cptr); free (cptr); cptr = X509_NAME_oneline (X509_get_issuer_name(cert), 0, 0); printf ("Issuer: %s\n", cptr); free (cptr); X509_free(cert); } else printf ("No server certificates configured.\n"); } /*****************************************************************************/ /* A self-contained HTTP GET request. */ int httpsGetRequest ( char *url, char *hdr ) { int rcode; httpsResetRequest(); httpsSetRequest (url); if (hdr) httpsSetHeader (hdr, NULL); rcode = httpsMakeRequest (); return (rcode); } /*****************************************************************************/ /* A self-contained HTTP POST request. */ int httpsPostRequest ( char *url, char *ctype, char *content, int clength, char *hdr ) { int rcode; httpsResetRequest(); httpsSetPost (1); httpsSetRequest (url); httpsSetContent (ctype, content, clength); if (hdr) httpsSetHeader (hdr, NULL); rcode = httpsMakeRequest (); return (rcode); } /*****************************************************************************/ /* If the URL begins with "^ " then it's a POST test, otherwise GET. */ void httpsTest (char *param) { int post = 0; unsigned int clength; char *fname; void *content; printf ("%s\n", param); httpsResetRequest(); if (*param == '^') { httpsSetPost (post = 1); fname = ++param; while (*param && *param != ' ') param++; if (*param) *param++ = '\0'; while (*param == ' ') param++; content = read_file (fname, &clength); if (!content) exit (errno); } httpsSetRequest (param); if (post) httpsSetContent ("application/octet-stream", content, clength); httpsMakeRequest (); httpsShowConnect (); printf ("\n-----\n%d \"%s\" %d bytes %s\n%s\n-----\n", request->rcode, request->ctype, request->rlength, request->error, request->response); httpsResetRequest (); exit (1); } /*****************************************************************************/