/*****************************************************************************/ /* http01.c Implements a cleartext HTTP server listening on port 80 to respond to an ACME http-01 challenge. The server is single threaded (and only services one client at a time). This model works fine for the stand-alone http-01 challenge. It binds to INADDR_ANY port 80 and so can only (and is only intended to) be used on a system where there is no cleartext HTTP service already operating. The server operates in a spawned subprocess. Each connection must take no longer than 60 seconds to be established and each request no longer han 30 seconds to be completely received. VERSION HISTORY --------------- 20-MAR-2019 MGD UtilSetPrn() detect set process name failure 12-JUL-2017 MGD initial */ /*****************************************************************************/ /* standard C header files */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "http01.h" #include "util.h" #include "acme-client/wcme_config.h" #define FI_LI "HTTP01", __LINE__ static int ListenSocket; #define MOREHEADER "Content-Type: text/plain\r\n\ Script-Control: X-stream-mode\r\n\ Cache-Control: no-cache, no-store, must-revalidate\r\n\ Pragma: no-cache\r\n\ Expires: 0\r\n\ \r\n" static char Response200 [] = "HTTP/1.0 200 OK\r\n" MOREHEADER ""; static char Response400 [] = "HTTP/1.0 400 Huh?\r\n" MOREHEADER "Couldn't understand the request.\n"; static char Response404 [] = "HTTP/1.0 404 ZERO2C\r\n" MOREHEADER "Nothing to see here ... move along.\n"; static char Response403 [] = "HTTP/1.0 403 Oops\r\n" MOREHEADER ""; extern char* WcmeLogFileName; char* doasprintf (char*, ...); /*****************************************************************************/ /* Spawn and manage a subprocess providing a standalone http-01 challenge server. When |what| is 1 then forcex() the image to exit, 0 then spawn the http-01 listener, -1 then report the subprocess completion status, negative but *not* -1 for checking purposes. */ void Http01Spawn (int what) { static ulong flags = 0x03; /* NOCLISYM | NOWAIT */ static ulong Delta120 [2] = { -1200000000, -1 }; static int pid, wake01, SubPrcStatus; static char cmd [256]; static $DESCRIPTOR (SubPrcNamDsc, "WCME-http-01"); static $DESCRIPTOR (CmdDsc, cmd); int check01, index, status; char *cptr, *sptr, *zptr; char ThisHostAddr [64]; /*********/ /* begin */ /*********/ if (check01 = (what < 0)) { if (what == -1) { vmsdbg ("spawn() http-01 exit %s%08.08X %s", "%X", SubPrcStatus, UtilGetMsg(SubPrcStatus)); return; } /* when /wcme=check=http01 after 2 minutes forcex() the process */ status = sys$setimr (EFN$C_ENF, &Delta120, &Http01Spawn, 1, 0); if (!(status & 1)) { vmsdbg ("sys$setimr() %s:%d %s%08.08X %s", FI_LI, "%X", status, UtilGetMsg(status)); exit (status); } /* continue thru to perform the spawn... */ what = 0; gethostaddr (ThisHostAddr); vmsdbg ("NOTE: http://%s/.well-known/acme-challenge/", ThisHostAddr); } if (what > 0) { if (pid) { status = sys$forcex (&pid, 0, SS$_NORMAL); if (status & 1) vmsdbg ("forcex() http-01 OK"); else vmsdbg ("forcex() http-01 failed %s%08.08X %s", "%X", status, UtilGetMsg(status)); } else vmsdbg ("forcex() pid 0!"); /* if /wcme=check=http01 */ if (wake01) sys$wake (0, 0); return; } if (what == 0) { zptr = (sptr = cmd) + sizeof(cmd)-1; for (cptr = "pipe set process /privilege=sysprv"; *cptr && sptr < zptr; *sptr++ = *cptr++); if (WcmeLogFileName) { for (cptr = " ; define/process WCME_LOG_FILE \""; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = WcmeLogFileName; *cptr && sptr < zptr; *sptr++ = *cptr++); if (sptr < zptr) *sptr++ = '\"'; } for (cptr = " ; wcme=\"$"; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = UtilImageName(); *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = "\" ; wcme /wcme=http01"; *cptr && sptr < zptr; *sptr++ = *cptr++); *sptr = '\0'; CmdDsc.dsc$w_length = sptr - cmd; UtilSysPrv(); /* spawn() does not wait */ status = lib$spawn (&CmdDsc, 0, 0, &flags, &SubPrcNamDsc, &pid, &SubPrcStatus, 0, &Http01Spawn, -1, 0, 0, 0); UtilMereMortal(); vmsdbg ("spawn() http-01 %08.08X %s%08.08X %s", pid, "%X", status, UtilGetMsg(status)); /* if /wcme=check=http01 then wait for ^Y or timer */ if (wake01 = check01) sys$hiber (); } } /*****************************************************************************/ /* Set up a socket listening to port 80. */ int Http01Begin () { static int one = 1; static ulong Delta60 [2] = { -600000000, -1 }; int csock, lsock, status; uint clen; char *cptr; struct sockaddr_in caddr; struct sockaddr_in laddr; /*********/ /* begin */ /*********/ vmsdbg ("begin", FI_LI); if (!UtilSetPrn ("http-01")) return (-1); memset (&laddr, 0, sizeof(laddr)); laddr.sin_family = AF_INET; laddr.sin_addr.s_addr = INADDR_ANY; /* default port is 80 but for test/development can be run on alternate */ if (cptr = UtilTrnLnm ("WCME_HTTP01", "LNM$SYSTEM", 0)) laddr.sin_port = htons(atoi(cptr)); if (!laddr.sin_port) laddr.sin_port = htons(80); UtilSysPrv(); if ((lsock = socket (AF_INET, SOCK_STREAM, 0)) < 0) { vmsdbg ("socket() %s:%d %s", FI_LI, strerror(errno)); UtilMereMortal(); return (-1); } if (setsockopt (lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { vmsdbg ("setsockopt() %s:%d %s", FI_LI, strerror(errno)); close (lsock); UtilMereMortal(); return (-1); } if (bind (lsock, (struct sockaddr*)&laddr, sizeof(laddr)) < 0) { vmsdbg ("bind() %s:%d %s", FI_LI, strerror(errno)); close (lsock); UtilMereMortal(); return (-1); } if (listen (lsock, 5) < 0) { vmsdbg ("listen() %s:%d %s", FI_LI, strerror(errno)); close (lsock); UtilMereMortal(); return (-1); } UtilMereMortal(); ListenSocket = lsock; for (;;) { /* wait maximum of this many seconds for a connection */ ListenSocket = lsock; status = sys$setimr (EFN$C_ENF, &Delta60, &Http01CancelListen, lsock, 0); if (!(status & 1)) { vmsdbg ("sys$setimr() %s:%d %s%08.08X %s", FI_LI, "%X", status, UtilGetMsg(status)); ListenSocket = 0; break; } clen = sizeof(caddr); csock = accept (lsock, (struct sockaddr*)&caddr, &clen); if (csock < 0) { status = vaxc$errno; vmsdbg ("accept() %s:%d %s", FI_LI, strerror(errno)); if (status == SS$_CANCEL || SS$_IVCHAN) break; continue; } if (ListenSocket) { sys$cantim (ListenSocket, 0); ListenSocket = 0; } Http01Request (csock); close (csock); } close (lsock); return (0); } /*****************************************************************************/ /* Timer target. */ void Http01CancelListen (int lsock) { /*********/ /* begin */ /*********/ vmsdbg ("listen timeout %s:%d", FI_LI); if (ListenSocket) { close (ListenSocket); ListenSocket = 0; } } /*****************************************************************************/ /* Read the request header from the client. If the request fails, is not understood, or is anything other than an acme-challenge then return an error response. Otherwise attempt to read the challenge file and return the contents to the CA. Error responses can be returned at this stage too. Return -1 for an underlying error, or the HTTP status of the response. */ int Http01Request (int csock) { static ulong Delta30 [2] = { -300000000, -1 }; int bcnt, retval, status; char *cptr, *czptr, *uptr; char buf [4096], token [256]; FILE *fp; /*********/ /* begin */ /*********/ buf[bcnt = 0] = '\0'; uptr = NULL; /* wait maximum of this many seconds for a request */ status = sys$setimr (EFN$C_ENF, &Delta30, &Http01CancelRequest, csock, 0); if (!(status & 1)) { vmsdbg ("sys$setimr() %s:%d %s%08.08X %s", FI_LI, "%X", status, UtilGetMsg(status)); return (-1); } for (;;) { if ((retval = recv (csock, buf+bcnt, sizeof(buf)-bcnt, 0)) <= 0) { vmsdbg ("recv() %s:%d %s", FI_LI, strerror(errno)); return (-1); } bcnt += retval; /* look for the end of the request header */ czptr = buf + bcnt; for (cptr = buf; cptr < czptr; cptr++) { if (*cptr == '\r' && *(cptr+1) == '\n' && *(cptr+2) == '\r' && *(cptr+3) == '\n') break; else if (*cptr == '\n' && *(cptr+1) == '\n') break; } /* request header incomplete need more from the client */ if (cptr >= czptr) continue; /* parse the first request line from the header */ cptr = buf; if (!strncmp (cptr, "GET ", 4)) { for (cptr += 4; cptr < czptr && *cptr == ' '; cptr++); if (cptr < czptr && *cptr == '/') { uptr = cptr; while (cptr < czptr && *cptr != ' ' && *cptr != '\r' && *cptr != '\n') cptr++; if (cptr < czptr && *cptr == ' ') { *cptr++ = '\0'; while (cptr < czptr && *cptr == ' ') cptr++; if (cptr >= czptr) uptr = NULL; else if (strncmp (cptr, "HTTP/1.", 7)) uptr = NULL; } } } break; } sys$cantim (csock, 0); if (!uptr) { vmsdbg ("bad request %s:%d 400", FI_LI); send (csock, Response400, strlen(Response400), 0); return (400); } vmsdbg ("URI %s", uptr); if (strncmp (uptr, "/.well-known/acme-challenge/", 28)) { vmsdbg ("not challenge %s:%d 404", FI_LI); send (csock, Response404, strlen(Response404), 0); return (403); } UtilSysPrv(); cptr = NULL; fp = fopen (UtilOds2FileName(WWW_DIR,uptr+28,NULL), "r"); if (fp) cptr = fgets (token, sizeof(token), fp); if (!cptr) { vmsdbg ("fopen() %s:%d %d", FI_LI, 403); cptr = doasprintf ("%sChallenge received but %s.\n", Response403, strerror(errno)); send (csock, cptr, strlen(cptr), 0); free (cptr); fclose (fp); UtilMereMortal(); return (500); } fclose (fp); UtilMereMortal(); vmsdbg ("challenge %s 200", token); cptr = doasprintf ( "HTTP/1.0 200 OK\r\n\ Content-Length: %d\r\n\ %s\ %s", strlen(token), MOREHEADER, token); send (csock, cptr, strlen(cptr), 0); free (cptr); return (200); } /*****************************************************************************/ /* Timer target. */ void Http01CancelRequest (int csock) { /*********/ /* begin */ /*********/ vmsdbg ("request timeout %s:%d", FI_LI); close (csock); } /*****************************************************************************/