diff --git a/misc/http_client.c b/misc/http_client.c new file mode 100644 index 0000000..2669213 --- /dev/null +++ b/misc/http_client.c @@ -0,0 +1,456 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "subr.h" +#include "http_client.h" + +struct http_url * +http_url_parse(const char *url) +{ + size_t ulen = strlen(url); + if (strncmp(url, "http://", 7) != 0) + return NULL; + const char *cur, *at, *uri = NULL, *uri_e = NULL; + const char *host = NULL, *host_e = NULL; + const char *port = NULL, *port_e = NULL; + at = strchr(url + 7, '@'); + uri = strchr(url + 7, '/'); + cur = strchr(url + 7, '?'); + if (cur != NULL && (uri == NULL || cur < uri)) + uri = cur; + if (uri == NULL) + uri = url + ulen; + if (at != NULL && at < uri) + host = at + 1; + else + host = url + 7; + cur = host; + while (cur < uri && *cur != ':') + cur++; + host_e = cur; + if (host_e == host) + return NULL; + if (*cur == ':') { + cur++; + port = cur; + while (cur < uri && *cur >= '0' && *cur <= '9') + cur++; + if (cur == port || cur != uri) + return NULL; + port_e = cur; + } + while (*cur != '\0') + cur++; + uri_e = cur; + struct http_url *res = + malloc(sizeof(*res) + host_e - host + 1 + uri_e - uri + 2); + if (res == NULL) + return NULL; + if (port != NULL) + sscanf(port, "%hu", &res->port); + else + res->port = 80; + res->host = (char *)(res + 1); + res->uri = res->host + (host_e - host + 1); + bcopy(host, res->host, host_e - host); + res->host[host_e - host] = '\0'; + if (*uri != '/') { + res->uri[0] = '/'; + bcopy(uri, res->uri + 1, uri_e - uri); + res->uri[uri_e - uri + 1] = '\0'; + } else { + bcopy(uri, res->uri, uri_e - uri); + res->uri[uri_e - uri] = '\0'; + } + return res; +} + +void +http_url_free(struct http_url *url) +{ + free(url); +} + +struct http_req { + enum { + HTTP_RESOLVE, HTTP_CONNECT, HTTP_WRITE, HTTP_RECEIVE, HTTP_PARSE + } state; + struct http_url *url; + int sd; + struct event ev; + http_cb cb; + void *arg; + int cancel; + + int pstate, chunked; + long length; + + struct evbuffer *buf; +}; + +static void +http_free(struct http_req *req) +{ + if (req->url != NULL) + http_url_free(req->url); + if (req->buf != NULL) + evbuffer_free(req->buf); + if (req->sd > 0) { + event_del(&req->ev); + close(req->sd); + } + free(req); +} + +static void +http_error(struct http_req *req) +{ + struct http_response res; + res.type = HTTP_T_ERR; + res.v.error = 1; + req->cb(req, &res, req->arg); + http_free(req); +} + +#define PS_HEAD 0 +#define PS_CHUNK_SIZE 1 +#define PS_CHUNK_DATA 2 +#define PS_CHUNK_CRLF 3 +#define PS_ID_DATA 4 + +static int +headers_parse(struct http_req *req, char *buf, char *end) +{ + int code; + char *cur, *crlf; + char name[128], value[872]; + struct http_response res; + + req->chunked = 0; + req->length = -1; + + if (sscanf(buf, "HTTP/1.1 %d", &code) == 0) + return 0; + res.type = HTTP_T_CODE; + res.v.code = code; + req->cb(req, &res, req->arg); + if (req->cancel) + return 1; + + cur = strstr(buf, "\r\n") + 2; + crlf = strstr(cur, "\r\n"); + while (cur < end) { + int i; + char *colon = strchr(cur, ':'); + if (colon == NULL || colon > crlf) + return 0; + snprintf(name, sizeof(name), "%.*s", (int)(colon - cur), cur); + + cur = colon + 1; + i = 0; + val_loop: + while (isblank(*cur)) + cur++; + while (cur < crlf) { + if (i < sizeof(value) - 1) { + value[i] = *cur; + i++; + } + cur++; + } + cur += 2; + crlf = strstr(cur, "\r\n"); + if (isblank(*cur)) { + if (i < sizeof(value) - 1) { + value[i] = ' '; + i++; + } + cur++; + goto val_loop; + } + value[i] = '\0'; + for (i--; i >= 0 && isblank(value[i]); i--) + value[i] = '\0'; + + res.type = HTTP_T_HEADER; + res.v.header.n = name; + res.v.header.v = value; + req->cb(req, &res, req->arg); + if (req->cancel) + return 1; + if ((!req->chunked + && strcasecmp("Transfer-Encoding", name) == 0 + && strcasecmp("chunked", value) == 0)) + req->chunked = 1; + if ((!req->chunked && req->length == -1 + && strcasecmp("Content-Length", name) == 0)) { + errno = 0; + req->length = strtol(value, NULL, 10); + if (errno) + req->length = -1; + } + } + if (req->chunked) + req->pstate = PS_CHUNK_SIZE; + else + req->pstate = PS_ID_DATA; + return 1; +} + +static int +http_parse(struct http_req *req, int len) +{ + char *end, *numend; + size_t dlen; + struct http_response res; +again: + switch (req->pstate) { + case PS_HEAD: + if (len == 0) + goto error; + if ((end = evbuffer_find(req->buf, "\r\n\r\n", 4)) == NULL) { + if (req->buf->off < (1 << 15)) + return 1; + else + goto error; + } + if (evbuffer_add(req->buf, "", 1) != 0) + goto error; + req->buf->off--; + if (!headers_parse(req, req->buf->buffer, end)) + goto error; + if (req->cancel) + goto cancel; + evbuffer_drain(req->buf, end - (char *)req->buf->buffer + 4); + goto again; + case PS_CHUNK_SIZE: + assert(req->chunked); + if (len == 0) + goto error; + if ((end = evbuffer_find(req->buf, "\r\n", 2)) == NULL) { + if (req->buf->off < 20) + return 1; + else + goto error; + } + errno = 0; + req->length = strtol(req->buf->buffer, &numend, 16); + if (req->length < 0 || numend == (char *)req->buf->buffer || errno) + goto error; + if (req->length == 0) + goto done; + evbuffer_drain(req->buf, end - (char *)req->buf->buffer + 2); + req->pstate = PS_CHUNK_DATA; + goto again; + case PS_CHUNK_DATA: + if (len == 0) + goto error; + assert(req->length > 0); + dlen = min(req->buf->off, req->length); + if (dlen > 0) { + res.type = HTTP_T_DATA; + res.v.data.l = dlen; + res.v.data.p = req->buf->buffer; + req->cb(req, &res, req->arg); + if (req->cancel) + goto cancel; + evbuffer_drain(req->buf, dlen); + req->length -= dlen; + if (req->length == 0) { + req->pstate = PS_CHUNK_CRLF; + goto again; + } + } + return 1; + case PS_CHUNK_CRLF: + if (len == 0) + goto error; + assert(req->length == 0); + if (req->buf->off < 2) + return 1; + if (bcmp(req->buf->buffer, "\r\n", 2) != 0) + goto error; + evbuffer_drain(req->buf, 2); + req->pstate = PS_CHUNK_SIZE; + goto again; + case PS_ID_DATA: + if (len == 0 && req->length < 0) + goto done; + else if (len == 0) + goto error; + if (req->length < 0) + dlen = req->buf->off; + else + dlen = min(req->buf->off, req->length); + if (dlen > 0) { + res.type = HTTP_T_DATA; + res.v.data.p = req->buf->buffer; + res.v.data.l = dlen; + req->cb(req, &res, req->arg); + if (req->cancel) + goto cancel; + evbuffer_drain(req->buf, dlen); + if (req->length > 0) { + req->length -= dlen; + if (req->length == 0) + goto done; + } + } + return 1; + default: + abort(); + } +error: + http_error(req); + return 0; +done: + res.type = HTTP_T_DONE; + req->cb(req, &res, req->arg); +cancel: + http_free(req); + return 0; +} + +static void +http_read_cb(int sd, short type, void *arg) +{ + struct http_req *req = arg; + if (type == EV_TIMEOUT) { + http_error(req); + return; + } + int nr = evbuffer_read(req->buf, sd, 1 << 14); + if (nr < 0) { + if (nr == EAGAIN) + goto more; + else { + printf("read err\n"); + http_error(req); + return; + } + } + req->state = HTTP_PARSE; + if (!http_parse(req, nr)) + return; + req->state = HTTP_RECEIVE; +more: + if (event_add(&req->ev, NULL) != 0) + http_error(req); +} + +static void +http_write_cb(int sd, short type, void *arg) +{ + struct http_req *req = arg; + if (type == EV_TIMEOUT) { + http_error(req); + return; + } + int nw = evbuffer_write(req->buf, sd); + if (nw == -1) { + if (errno == EAGAIN) + goto out; + else + goto error; + } +out: + if (req->buf->off != 0) { + if (event_add(&req->ev, NULL) != 0) + goto error; + } else { + req->state = HTTP_RECEIVE; + event_set(&req->ev, req->sd, EV_READ, http_read_cb, req); + if (event_add(&req->ev, NULL) != 0) + goto error; + } + return; +error: + printf("http write err\n"); + http_error(req); +} + +static void +http_dnscb(int result, char type, int count, int ttl, void *addrs, void *arg) +{ + struct http_req *req = arg; + if (req->cancel) + http_free(req); + else if (result == 0 && type == 1 && count > 0) { + int addri = rand_between(0, count - 1); + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(req->url->port); + bcopy(addrs + addri * 4, &addr.sin_addr.s_addr, 4); + req->state = HTTP_CONNECT; + if ((req->sd = socket(PF_INET, SOCK_STREAM, 0)) == -1) + goto error; + if (set_nonblocking(req->sd) != 0) + goto error; + if ((connect(req->sd, (struct sockaddr *)&addr, sizeof(addr)) != 0 + && errno != EINPROGRESS)) + goto error; + event_set(&req->ev, req->sd, EV_WRITE, http_write_cb, req); + if (event_add(&req->ev, NULL) != 0) + goto error; + } else + goto error; + return; + +error: + http_error(req); +} + +int +http_get(struct http_req **out, const char *url, const char *hdrs, http_cb cb, + void *arg) +{ + struct http_req *req = calloc(1, sizeof(*req)); + if (req == NULL) + return 0; + req->sd = -1; + req->cb = cb; + req->arg = arg; + req->url = http_url_parse(url); + if (req->url == NULL) + goto error; + if ((req->buf = evbuffer_new()) == NULL) + goto error; + if (evbuffer_add_printf(req->buf, "GET %s HTTP/1.1\r\n" + "Accept-Encoding:\r\n" + "Connection: close\r\n" + "Host: %s\r\n" + "%s" + "\r\n", req->url->uri, req->url->host, hdrs) == -1) + goto error; + if (evdns_resolve_ipv4(req->url->host, 0, http_dnscb, req) != 0) + goto error; + if (out != NULL) + *out = req; + return 1; +error: + http_free(req); + return 0; +} + +void +http_cancel(struct http_req *req) +{ + if (req->state == HTTP_RESOLVE || req->state == HTTP_PARSE) + req->cancel = 1; + else + http_free(req); +} diff --git a/misc/http_client.h b/misc/http_client.h new file mode 100644 index 0000000..2d0c24a --- /dev/null +++ b/misc/http_client.h @@ -0,0 +1,38 @@ +#ifndef BTPD_HTTP_CLIENT_H +#define BTPD_HTTP_CLIENT_H + +struct http_response { + enum { + HTTP_T_ERR, HTTP_T_CODE, HTTP_T_HEADER, HTTP_T_DATA, HTTP_T_DONE + } type; + union { + int error; + int code; + struct { + char *n; + char *v; + } header; + struct { + size_t l; + char *p; + } data; + } v; +}; + +struct http_url { + char *host; + char *uri; + uint16_t port; +}; + +struct http_req; + +typedef void (*http_cb)(struct http_req *, struct http_response *, void *); + +struct http_url *http_url_parse(const char *url); +void http_url_free(struct http_url *url); +int http_get(struct http_req **out, const char *url, const char *hdrs, + http_cb cb, void *arg); +void http_cancel(struct http_req *req); + +#endif