#include <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <tls.h>
#include <vis.h>
#include "http.h"
#include "extern.h"
#include "parse.h"
#define RETRY_DELAY 5
#define RETRY_MAX 10
struct buf {
char *buf;
size_t sz;
};
struct conn {
const char *newnonce;
char *kid;
int fd;
int dfd;
struct buf buf;
};
static void
buf_dump(const struct buf *buf)
{
char *nbuf;
if (buf->sz == 0)
return;
if ((nbuf = calloc(buf->sz + 1, 4)) == NULL)
err(EXIT_FAILURE, "calloc");
strvisx(nbuf, buf->buf, buf->sz, VIS_SAFE);
dodbg("transfer buffer: [%s] (%zu bytes)", nbuf, buf->sz);
free(nbuf);
}
static char *
url2host(const char *host, short *port, char **path)
{
char *url, *ep;
if (strncmp(host, "https://", 8) == 0) {
*port = 443;
if ((url = strdup(host + 8)) == NULL) {
warn("strdup");
return NULL;
}
} else {
warnx("%s: RFC 8555 requires https for the API server", host);
return NULL;
}
if ((ep = strchr(url, '/')) != NULL) {
*path = strdup(ep);
*ep = '\0';
} else
*path = strdup("");
if (*path == NULL) {
warn("strdup");
free(url);
return NULL;
}
if ((ep = strchr(url, ':')) != NULL) {
const char *errstr;
*ep = '\0';
*port = strtonum(ep + 1, 1, USHRT_MAX, &errstr);
if (errstr != NULL) {
warn("port is %s: %s", errstr, ep + 1);
free(*path);
*path = NULL;
free(url);
return NULL;
}
}
return url;
}
static ssize_t
urlresolve(int fd, const char *host, struct source *v)
{
char *addr;
size_t i, sz;
long lval;
if (writeop(fd, COMM_DNS, DNS_LOOKUP) <= 0)
return -1;
else if (writestr(fd, COMM_DNSQ, host) <= 0)
return -1;
else if ((lval = readop(fd, COMM_DNSLEN)) < 0)
return -1;
sz = lval;
assert(sz <= MAX_SERVERS_DNS);
for (i = 0; i < sz; i++) {
memset(&v[i], 0, sizeof(struct source));
if ((lval = readop(fd, COMM_DNSF)) < 0)
goto err;
else if (lval != 4 && lval != 6)
goto err;
else if ((addr = readstr(fd, COMM_DNSA)) == NULL)
goto err;
v[i].family = lval;
v[i].ip = addr;
}
return sz;
err:
for (i = 0; i < sz; i++)
free(v[i].ip);
return -1;
}
static long
nreq(struct conn *c, const char *addr)
{
struct httpget *g;
struct source src[MAX_SERVERS_DNS];
struct httphead *st;
char *host, *path;
short port;
size_t srcsz;
ssize_t ssz;
long code;
int redirects = 0;
if ((host = url2host(addr, &port, &path)) == NULL)
return -1;
again:
if ((ssz = urlresolve(c->dfd, host, src)) < 0) {
free(host);
free(path);
return -1;
}
srcsz = ssz;
g = http_get(src, srcsz, host, port, path, 0, NULL, 0);
free(host);
free(path);
if (g == NULL)
return -1;
switch (g->code) {
case 301:
case 302:
case 303:
case 307:
case 308:
redirects++;
if (redirects > 3) {
warnx("too many redirects");
http_get_free(g);
return -1;
}
if ((st = http_head_get("Location", g->head, g->headsz)) ==
NULL) {
warnx("redirect without location header");
http_get_free(g);
return -1;
}
host = url2host(st->val, &port, &path);
http_get_free(g);
if (host == NULL)
return -1;
goto again;
break;
default:
code = g->code;
break;
}
free(c->buf.buf);
c->buf.sz = g->bodypartsz;
c->buf.buf = malloc(c->buf.sz);
if (c->buf.buf == NULL) {
warn("malloc");
code = -1;
} else
memcpy(c->buf.buf, g->bodypart, c->buf.sz);
http_get_free(g);
return code;
}
static long
sreq(struct conn *c, const char *addr, int kid, const char *req, char **loc)
{
struct httpget *g;
struct source src[MAX_SERVERS_DNS];
char *host, *path, *nonce, *reqsn;
short port;
struct httphead *h;
ssize_t ssz;
long code;
int retry = 0;
if ((host = url2host(c->newnonce, &port, &path)) == NULL)
return -1;
if ((ssz = urlresolve(c->dfd, host, src)) < 0) {
free(host);
free(path);
return -1;
}
g = http_get(src, (size_t)ssz, host, port, path, 1, NULL, 0);
free(host);
free(path);
if (g == NULL)
return -1;
h = http_head_get("Replay-Nonce", g->head, g->headsz);
if (h == NULL) {
warnx("%s: no replay nonce", c->newnonce);
http_get_free(g);
return -1;
} else if ((nonce = strdup(h->val)) == NULL) {
warn("strdup");
http_get_free(g);
return -1;
}
http_get_free(g);
again:
if (writeop(c->fd, COMM_ACCT, kid ? ACCT_KID_SIGN : ACCT_SIGN) <= 0) {
free(nonce);
return -1;
} else if (writestr(c->fd, COMM_PAY, req) <= 0) {
free(nonce);
return -1;
} else if (writestr(c->fd, COMM_NONCE, nonce) <= 0) {
free(nonce);
return -1;
} else if (writestr(c->fd, COMM_URL, addr) <= 0) {
free(nonce);
return -1;
}
free(nonce);
if (kid && writestr(c->fd, COMM_KID, c->kid) <= 0)
return -1;
if ((reqsn = readstr(c->fd, COMM_REQ)) == NULL)
return -1;
if ((host = url2host(addr, &port, &path)) == NULL) {
free(reqsn);
return -1;
} else if ((ssz = urlresolve(c->dfd, host, src)) < 0) {
free(host);
free(path);
free(reqsn);
return -1;
}
g = http_get(src, (size_t)ssz, host, port, path, 0, reqsn,
strlen(reqsn));
free(host);
free(path);
free(reqsn);
if (g == NULL)
return -1;
code = g->code;
free(c->buf.buf);
c->buf.sz = g->bodypartsz;
c->buf.buf = malloc(c->buf.sz);
if (c->buf.buf == NULL) {
warn("malloc");
code = -1;
} else
memcpy(c->buf.buf, g->bodypart, c->buf.sz);
if (code == 400) {
struct jsmnn *j;
char *type;
j = json_parse(c->buf.buf, c->buf.sz);
if (j == NULL) {
code = -1;
goto out;
}
type = json_getstr(j, "type");
json_free(j);
if (type == NULL) {
code = -1;
goto out;
}
if (strcmp(type, "urn:ietf:params:acme:error:badNonce") != 0) {
free(type);
goto out;
}
free(type);
if (retry++ < RETRY_MAX) {
h = http_head_get("Replay-Nonce", g->head, g->headsz);
if (h == NULL) {
warnx("no replay nonce");
code = -1;
goto out;
} else if ((nonce = strdup(h->val)) == NULL) {
warn("strdup");
code = -1;
goto out;
}
http_get_free(g);
goto again;
}
}
out:
if (loc != NULL) {
free(*loc);
*loc = NULL;
h = http_head_get("Location", g->head, g->headsz);
if (h != NULL)
*loc = strdup(h->val);
}
http_get_free(g);
return code;
}
static int
donewacc(struct conn *c, const struct capaths *p, const char *contact)
{
struct jsmnn *j = NULL;
int rc = 0;
char *req, *detail, *error = NULL, *accturi = NULL;
long lc;
if ((req = json_fmt_newacc(contact)) == NULL)
warnx("json_fmt_newacc");
else if ((lc = sreq(c, p->newaccount, 0, req, &c->kid)) < 0)
warnx("%s: bad comm", p->newaccount);
else if (lc == 400) {
if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
warnx("%s: bad JSON object", p->newaccount);
else {
detail = json_getstr(j, "detail");
if (detail != NULL && stravis(&error, detail, VIS_SAFE)
!= -1) {
warnx("%s", error);
free(error);
}
}
} else if (lc != 200 && lc != 201)
warnx("%s: bad HTTP: %ld", p->newaccount, lc);
else if (c->buf.buf == NULL || c->buf.sz == 0)
warnx("%s: empty response", p->newaccount);
else
rc = 1;
if (c->kid != NULL) {
if (stravis(&accturi, c->kid, VIS_SAFE) != -1)
printf("account key: %s\n", accturi);
free(accturi);
}
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
free(req);
return rc;
}
static int
dochkacc(struct conn *c, const struct capaths *p, const char *contact)
{
int rc = 0;
char *req, *accturi = NULL;
long lc;
if ((req = json_fmt_chkacc()) == NULL)
warnx("json_fmt_chkacc");
else if ((lc = sreq(c, p->newaccount, 0, req, &c->kid)) < 0)
warnx("%s: bad comm", p->newaccount);
else if (lc != 200 && lc != 400)
warnx("%s: bad HTTP: %ld", p->newaccount, lc);
else if (c->buf.buf == NULL || c->buf.sz == 0)
warnx("%s: empty response", p->newaccount);
else if (lc == 400)
rc = donewacc(c, p, contact);
else
rc = 1;
if (c->kid == NULL)
rc = 0;
else {
if (stravis(&accturi, c->kid, VIS_SAFE) != -1)
dodbg("account key: %s", accturi);
free(accturi);
}
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
free(req);
return rc;
}
static int
doneworder(struct conn *c, struct domain_c *domain, struct order *order,
const struct capaths *p)
{
struct jsmnn *j = NULL;
int rc = 0;
char *req;
long lc;
if ((req = json_fmt_neworder(domain)) == NULL)
warnx("json_fmt_neworder");
else if ((lc = sreq(c, p->neworder, 1, req, &order->uri)) < 0)
warnx("%s: bad comm", p->neworder);
else if (lc != 201)
warnx("%s: bad HTTP: %ld", p->neworder, lc);
else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
warnx("%s: bad JSON object", p->neworder);
else if (!json_parse_order(j, order))
warnx("%s: bad order", p->neworder);
else if (order->status == ORDER_INVALID)
warnx("%s: order invalid", p->neworder);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
free(req);
json_free(j);
return rc;
}
static int
doupdorder(struct conn *c, struct order *order)
{
struct jsmnn *j = NULL;
int rc = 0;
long lc;
if ((lc = sreq(c, order->uri, 1, "", NULL)) < 0)
warnx("%s: bad comm", order->uri);
else if (lc != 200)
warnx("%s: bad HTTP: %ld", order->uri, lc);
else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
warnx("%s: bad JSON object", order->uri);
else if (!json_parse_upd_order(j, order))
warnx("%s: bad order", order->uri);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
json_free(j);
return rc;
}
static int
dochngreq(struct conn *c, const char *auth, struct chng *chng)
{
int rc = 0;
long lc;
struct jsmnn *j = NULL;
dodbg("%s: %s", __func__, auth);
if ((lc = sreq(c, auth, 1, "", NULL)) < 0)
warnx("%s: bad comm", auth);
else if (lc != 200)
warnx("%s: bad HTTP: %ld", auth, lc);
else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
warnx("%s: bad JSON object", auth);
else if (!json_parse_challenge(j, chng))
warnx("%s: bad challenge", auth);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
json_free(j);
return rc;
}
static int
dochngresp(struct conn *c, const struct chng *chng)
{
int rc = 0;
long lc;
dodbg("%s: challenge", chng->uri);
if ((lc = sreq(c, chng->uri, 1, "{}", NULL)) < 0)
warnx("%s: bad comm", chng->uri);
else if (lc != 200 && lc != 201 && lc != 202)
warnx("%s: bad HTTP: %ld", chng->uri, lc);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
return rc;
}
static int
docert(struct conn *c, const char *uri, const char *csr)
{
char *req;
int rc = 0;
long lc;
dodbg("%s: certificate", uri);
if ((req = json_fmt_newcert(csr)) == NULL)
warnx("json_fmt_newcert");
else if ((lc = sreq(c, uri, 1, req, NULL)) < 0)
warnx("%s: bad comm", uri);
else if (lc != 200)
warnx("%s: bad HTTP: %ld", uri, lc);
else if (c->buf.sz == 0 || c->buf.buf == NULL)
warnx("%s: empty response", uri);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
free(req);
return rc;
}
static int
dogetcert(struct conn *c, const char *uri)
{
int rc = 0;
long lc;
dodbg("%s: certificate", uri);
if ((lc = sreq(c, uri, 1, "", NULL)) < 0)
warnx("%s: bad comm", uri);
else if (lc != 200)
warnx("%s: bad HTTP: %ld", uri, lc);
else if (c->buf.sz == 0 || c->buf.buf == NULL)
warnx("%s: empty response", uri);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
return rc;
}
static int
dorevoke(struct conn *c, const char *addr, const char *cert)
{
char *req;
int rc = 0;
long lc = 0;
dodbg("%s: revocation", addr);
if ((req = json_fmt_revokecert(cert)) == NULL)
warnx("json_fmt_revokecert");
else if ((lc = sreq(c, addr, 1, req, NULL)) < 0)
warnx("%s: bad comm", addr);
else if (lc != 200 && lc != 201 && lc != 409)
warnx("%s: bad HTTP: %ld", addr, lc);
else
rc = 1;
if (lc == 409)
warnx("%s: already revoked", addr);
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
free(req);
return rc;
}
static int
dodirs(struct conn *c, const char *addr, struct capaths *paths)
{
struct jsmnn *j = NULL;
long lc;
int rc = 0;
dodbg("%s: directories", addr);
if ((lc = nreq(c, addr)) < 0)
warnx("%s: bad comm", addr);
else if (lc != 200 && lc != 201)
warnx("%s: bad HTTP: %ld", addr, lc);
else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
warnx("json_parse");
else if (!json_parse_capaths(j, paths))
warnx("%s: bad CA paths", addr);
else
rc = 1;
if (rc == 0 || verbose > 1)
buf_dump(&c->buf);
json_free(j);
return rc;
}
int
netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
int revocate, struct authority_c *authority,
struct domain_c *domain)
{
int rc = 0, retries = 0;
size_t i;
char *cert = NULL, *thumb = NULL, *error = NULL;
struct conn c;
struct capaths paths;
struct order order;
struct chng *chngs = NULL;
long lval;
memset(&paths, 0, sizeof(struct capaths));
memset(&c, 0, sizeof(struct conn));
if (unveil(tls_default_ca_cert_file(), "r") == -1) {
warn("unveil %s", tls_default_ca_cert_file());
goto out;
}
if (pledge("stdio inet rpath", NULL) == -1) {
warn("pledge");
goto out;
}
if (http_init(authority->insecure) == -1) {
warn("http_init");
goto out;
}
if (pledge("stdio inet", NULL) == -1) {
warn("pledge");
goto out;
}
if ((lval = readop(afd, COMM_ACCT_STAT)) == 0) {
rc = 1;
goto out;
} else if (lval != ACCT_READY) {
warnx("unknown operation from acctproc");
goto out;
}
if ((lval = readop(kfd, COMM_KEY_STAT)) == 0) {
rc = 1;
goto out;
} else if (lval != KEY_READY) {
warnx("unknown operation from keyproc");
goto out;
}
if ((lval = readop(rfd, COMM_REVOKE_RESP)) == 0) {
rc = 1;
goto out;
} else if (lval != REVOKE_EXP && lval != REVOKE_OK) {
warnx("unknown operation from revokeproc");
goto out;
}
if (lval == REVOKE_OK) {
rc = 1;
goto out;
}
c.dfd = dfd;
c.fd = afd;
if (!dodirs(&c, authority->api, &paths))
goto out;
c.newnonce = paths.newnonce;
if (!dochkacc(&c, &paths, authority->contact))
goto out;
if (revocate) {
if ((cert = readstr(rfd, COMM_CSR)) == NULL)
goto out;
if (!dorevoke(&c, paths.revokecert, cert))
goto out;
else if (writeop(cfd, COMM_CSR_OP, CERT_REVOKE) > 0)
rc = 1;
goto out;
}
memset(&order, 0, sizeof(order));
if (!doneworder(&c, domain, &order, &paths))
goto out;
chngs = calloc(order.authsz, sizeof(struct chng));
if (chngs == NULL) {
warn("calloc");
goto out;
}
if (writeop(afd, COMM_ACCT, ACCT_THUMBPRINT) <= 0)
goto out;
else if ((thumb = readstr(afd, COMM_THUMB)) == NULL)
goto out;
while(order.status != ORDER_VALID && order.status != ORDER_INVALID) {
switch (order.status) {
case ORDER_INVALID:
warnx("order invalid");
goto out;
case ORDER_VALID:
rc = 1;
continue;
case ORDER_PENDING:
if (order.authsz < 1) {
warnx("order is in state pending but no "
"authorizations know");
goto out;
}
for (i = 0; i < order.authsz; i++) {
if (!dochngreq(&c, order.auths[i], &chngs[i]))
goto out;
dodbg("challenge, token: %s, uri: %s, status: "
"%d", chngs[i].token, chngs[i].uri,
chngs[i].status);
if (chngs[i].status == CHNG_VALID ||
chngs[i].status == CHNG_INVALID)
continue;
if (chngs[i].retry++ >= RETRY_MAX) {
warnx("%s: too many tries",
chngs[i].uri);
goto out;
}
if (writeop(Cfd, COMM_CHNG_OP, CHNG_SYN) <= 0)
goto out;
else if (writestr(Cfd, COMM_THUMB, thumb) <= 0)
goto out;
else if (writestr(Cfd, COMM_TOK,
chngs[i].token) <= 0)
goto out;
if (readop(Cfd, COMM_CHNG_ACK) != CHNG_ACK)
goto out;
}
for (i = 0; i < order.authsz; i++) {
if (chngs[i].status == CHNG_VALID ||
chngs[i].status == CHNG_INVALID)
continue;
if (!dochngresp(&c, &chngs[i]))
goto out;
}
break;
case ORDER_READY:
if (writeop(Cfd, COMM_CHNG_OP, CHNG_STOP) <= 0)
goto out;
if ((cert = readstr(kfd, COMM_CERT)) == NULL)
goto out;
if (!docert(&c, order.finalize, cert))
goto out;
break;
case ORDER_PROCESSING:
break;
default:
warnx("unhandled status: %d", order.status);
goto out;
}
if (!doupdorder(&c, &order))
goto out;
dodbg("order.status %d", order.status);
switch (order.status) {
case ORDER_PENDING:
case ORDER_PROCESSING:
if (retries++ > RETRY_MAX) {
warnx("too many retries");
goto out;
}
sleep(RETRY_DELAY);
break;
default:
retries = 0;
break;
}
}
if (order.status != ORDER_VALID) {
for (i = 0; i < order.authsz; i++) {
dochngreq(&c, order.auths[i], &chngs[i]);
if (chngs[i].error != NULL) {
if (stravis(&error, chngs[i].error, VIS_SAFE)
!= -1) {
warnx("%s", error);
free(error);
error = NULL;
}
}
}
goto out;
}
if (order.certificate == NULL) {
warnx("no certificate url received");
goto out;
}
if (!dogetcert(&c, order.certificate))
goto out;
else if (writeop(cfd, COMM_CSR_OP, CERT_UPDATE) <= 0)
goto out;
else if (writebuf(cfd, COMM_CSR, c.buf.buf, c.buf.sz) <= 0)
goto out;
rc = 1;
out:
close(cfd);
close(kfd);
close(afd);
close(Cfd);
close(dfd);
close(rfd);
free(cert);
free(thumb);
free(c.kid);
free(c.buf.buf);
if (chngs != NULL)
for (i = 0; i < order.authsz; i++)
json_free_challenge(&chngs[i]);
free(chngs);
json_free_capaths(&paths);
return rc;
}