/*****************************************************************************/ /* (wu)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-2020 MGD adapted for wuCME 20-MAR-2019 MGD UtilSetPrn() detect set process name failure 12-JUL-2017 MGD initial (for wCME) */ /*****************************************************************************/ /* standard C header files */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wucme.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 ""; char* doasprintf (char*, ...); /*****************************************************************************/ /* Spawn and manage a subprocess providing a standalone http-01 challenge server. When |what| is 1 then spawn the http-01 listener, 0 then forcex() the image to exit, -1 then report the subprocess completion status, negative but *not* -1 (e.g. -999) 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, "wuCME-http01"); static $DESCRIPTOR (CmdDsc, cmd); int check01, index, status; char *cptr, *sptr, *zptr; char ThisHostAddr [64]; /*********/ /* begin */ /*********/ if (check01 = (what < 0)) { if (what == -1) { warnx ("spawn() http-01 exit %s%08.08X %s", "%X", SubPrcStatus, UtilGetMsg(SubPrcStatus)); return; } /* when check/http01 after 2 minutes forcex() the process */ status = sys$setimr (EFN$C_ENF, &Delta120, &Http01Spawn, 0, 0); if (!(status & 1)) { warnx ("sys$setimr() %s:%d %s%08.08X %s", FI_LI, "%X", status, UtilGetMsg(status)); exit (status); } /* continue thru to perform the spawn... */ what = 1; gethostaddr (ThisHostAddr); warnx ("http://%s/.well-known/acme-challenge/", ThisHostAddr); } if (what == 0) { /* Q&D flush */ fsync (STDOUT_FILENO); fsync (STDERR_FILENO); /* harmless if not hibernating */ sys$wake (0, 0); if (pid) { status = sys$delprc (&pid, 0, 0); if (status & 1) warnx ("delprc() http-01 OK"); else warnx ("delprc() http-01 failed %s%08.08X %s", "%X", status, UtilGetMsg(status)); pid = 0; } /* give the spawn end AST (-1) a chance to deliver */ sleep (1); sys$wake (0, 0); return; } if (what > 0) { zptr = (sptr = cmd) + sizeof(cmd)-1; for (cptr = "pipe set process /privilege=sysprv ; wucme=\"$"; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = UtilImageName(); *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = "\" ; wucme "; *cptr && sptr < zptr; *sptr++ = *cptr++); if (wucmeMode (IS_PROCTOR)) /* see Http01Begin() verb */ cptr = "http01p"; else if (wucmeMode (IS_SCRIPT)) cptr = "http01s"; else cptr = "http01"; while (*cptr && sptr < zptr) *sptr++ = *cptr++; *sptr = '\0'; CmdDsc.dsc$w_length = sptr - cmd; /* spawn() does not wait */ status = lib$spawn (&CmdDsc, 0, 0, &flags, &SubPrcNamDsc, &pid, &SubPrcStatus, 0, &Http01Spawn, -1, 0, 0, 0); warnx ("spawn() http-01 %08.08X %s%08.08X %s", pid, "%X", status, UtilGetMsg(status)); /* if check/http01 then wait for ^Y or timer */ if (wake01 = check01) sys$hiber (); } } /*****************************************************************************/ /* Set up a socket listening to port 80. */ int Http01Begin (char *verb) { 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 */ /*********/ /* proctored subprocess should use the proctor log */ if (!strcasecmp (verb, "http01p")) wucmeLog (1); if (UtilSysTrnLnm (WUCME_VERBOSE)) verboserer(1); warnx ("begin", FI_LI); if (!UtilSetPrn ("wuCME-http01")) 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 ("WUCME_HTTP01", "LNM$SYSTEM", 0)) laddr.sin_port = htons(atoi(cptr)); if (!laddr.sin_port) laddr.sin_port = htons(80); UtilAdjustPriv(); if ((lsock = socket (AF_INET, SOCK_STREAM, 0)) < 0) { warnx ("socket() %s:%d %s", FI_LI, strerror(errno)); return (-1); } if (setsockopt (lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) { warnx ("setsockopt() %s:%d %s", FI_LI, strerror(errno)); close (lsock); return (-1); } if (bind (lsock, (struct sockaddr*)&laddr, sizeof(laddr)) < 0) { warnx ("bind() %s:%d %s", FI_LI, strerror(errno)); close (lsock); return (-1); } if (listen (lsock, 5) < 0) { warnx ("listen() %s:%d %s", FI_LI, strerror(errno)); close (lsock); return (-1); } 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)) { warnx ("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; warnx ("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 */ /*********/ warnx ("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 Delta60 [2] = { -600000000, -1 }; int bcnt, retval, status; char *cptr, *czptr, *token, *uptr; char buf [4096]; FILE *fp; /*********/ /* begin */ /*********/ buf[bcnt = 0] = '\0'; uptr = NULL; /* wait maximum of this many seconds for a request */ status = sys$setimr (EFN$C_ENF, &Delta60, &Http01CancelRequest, csock, 0); if (!(status & 1)) { warnx ("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) { warnx ("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) { warnx ("bad request %s:%d 400", FI_LI); send (csock, Response400, strlen(Response400), 0); return (400); } warnx ("URI %s", uptr); if (strstr (uptr, "/admin/check/challenge")) { cptr = doasprintf ( "HTTP/1.0 200 OK\r\n\ %s\ Challenge check succeeded!", MOREHEADER); send (csock, cptr, strlen(cptr), 0); free (cptr); return (200); } if (strncmp (uptr, "/.well-known/acme-challenge/", 28)) { warnx ("not challenge %s:%d 404", FI_LI); send (csock, Response404, strlen(Response404), 0); return (403); } token = uptr + 28; cptr = wucmeChallenge (token, NULL); if (cptr) { warnx ("challenge %s 200", token); cptr = doasprintf ( "HTTP/1.0 200 OK\r\n\ Content-Length: %d\r\n\ %s\ %s", strlen(token), MOREHEADER, token); status = 200; } else { cptr = doasprintf ("%sChallenge received but no such token.\n", Response403); status = 403; } send (csock, cptr, strlen(cptr), 0); free (cptr); return (status); } /*****************************************************************************/ /* Timer target. */ void Http01CancelRequest (int csock) { /*********/ /* begin */ /*********/ warnx ("request timeout %s:%d", FI_LI); close (csock); } /*****************************************************************************/