#define FetchHasHeader(H) (!!msg.headers[H].a) #define FetchHeaderData(H) (inbuf.p + msg.headers[H].a) #define FetchHeaderLength(H) (msg.headers[H].b - msg.headers[H].a) #define FetchHeaderEqualCase(H, S) \ SlicesEqualCase(S, strlen(S), FetchHeaderData(H), FetchHeaderLength(H)) #define kaNONE 0 #define kaOPEN 1 #define kaKEEP 2 #define kaCLOSE 3 static int LuaFetch(lua_State *L) { #define ssl nope // TODO(jart): make this file less huge ssize_t rc; bool usingssl; uint32_t ip; struct Url url; int t, ret, sock = -1, hdridx; const char *host, *port; char *request; struct TlsBio *bio; struct addrinfo *addr; struct Buffer inbuf; // shadowing intentional struct HttpMessage msg; // shadowing intentional struct HttpUnchunker u; const char *urlarg, *body, *method; char *conlenhdr = ""; char *headers = 0; const char *hosthdr = 0; const char *connhdr = 0; const char *agenthdr = brand; const char *key, *val, *hdr; size_t keylen, vallen; size_t urlarglen, requestlen, paylen, bodylen; size_t i, g, hdrsize; int keepalive = kaNONE; char canmethod[9] = {0}; uint64_t imethod; int numredirects = 0, maxredirects = 5; bool followredirect = true; struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP, .ai_flags = AI_NUMERICSERV}; (void)ret; (void)usingssl; /* * Get args: url [, body | {method = "PUT", body = "..."}] */ urlarg = luaL_checklstring(L, 1, &urlarglen); if (lua_istable(L, 2)) { lua_settop(L, 2); // discard any extra arguments lua_getfield(L, 2, "body"); body = luaL_optlstring(L, -1, "", &bodylen); lua_getfield(L, 2, "method"); // use GET by default if no method is provided method = luaL_optstring(L, -1, "GET"); if ((imethod = ParseHttpMethod(method, -1))) { WRITE64LE(canmethod, imethod); method = canmethod; } else { return LuaNilError(L, "bad method"); } lua_getfield(L, 2, "followredirect"); if (lua_isboolean(L, -1)) followredirect = lua_toboolean(L, -1); lua_getfield(L, 2, "maxredirects"); maxredirects = luaL_optinteger(L, -1, maxredirects); lua_getfield(L, 2, "numredirects"); numredirects = luaL_optinteger(L, -1, numredirects); lua_getfield(L, 2, "keepalive"); if (!lua_isnil(L, -1)) { if (lua_istable(L, -1)) { keepalive = kaOPEN; // will be updated based on host later } else if (lua_isboolean(L, -1)) { keepalive = lua_toboolean(L, -1) ? kaOPEN : kaNONE; if (keepalive) { lua_createtable(L, 0, 1); lua_setfield(L, 2, "keepalive"); } } else { return luaL_argerror(L, 2, "invalid keepalive value;" " boolean or table expected"); } } lua_getfield(L, 2, "headers"); if (!lua_isnil(L, -1)) { if (!lua_istable(L, -1)) return luaL_argerror(L, 2, "invalid headers value; table expected"); lua_pushnil(L); while (lua_next(L, -2)) { if (lua_type(L, -2) == LUA_TSTRING) { // skip any non-string keys key = lua_tolstring(L, -2, &keylen); if (!IsValidHttpToken(key, keylen)) return LuaNilError(L, "invalid header name: %s", key); val = lua_tolstring(L, -1, &vallen); if (!(hdr = gc(EncodeHttpHeaderValue(val, vallen, 0)))) return LuaNilError(L, "invalid header %s value encoding", key); // Content-Length will be overwritten; skip it to avoid duplicates; // also allow unknown headers if ((hdridx = GetHttpHeader(key, keylen)) == -1 || hdridx != kHttpContentLength) { if (hdridx == kHttpUserAgent) { agenthdr = hdr; } else if (hdridx == kHttpHost) { hosthdr = hdr; } else if (hdridx == kHttpConnection) { connhdr = hdr; } else { appendd(&headers, key, keylen); appendw(&headers, READ16LE(": ")); appends(&headers, hdr); appendw(&headers, READ16LE("\r\n")); } } } lua_pop(L, 1); // pop the value, keep the key for the next iteration } } lua_settop(L, 2); // drop all added elements to keep the stack balanced } else if (lua_isnoneornil(L, 2)) { body = ""; bodylen = 0; method = "GET"; } else { body = luaL_checklstring(L, 2, &bodylen); method = "POST"; } // provide Content-Length header unless it's zero and not expected imethod = ParseHttpMethod(method, -1); if (bodylen > 0 || !(imethod == kHttpGet || imethod == kHttpHead || imethod == kHttpTrace || imethod == kHttpDelete || imethod == kHttpConnect)) { conlenhdr = gc(xasprintf("Content-Length: %zu\r\n", bodylen)); } /* * Parse URL. */ gc(ParseUrl(urlarg, urlarglen, &url, true)); gc(url.params.p); DEBUGF("(ftch) client fetching %`'s (host=%`'.*s, port=%.*s, path=%`'.*s)", urlarg, url.host.n, url.host.p, url.port.n, url.port.p, url.path.n, url.path.p); usingssl = false; if (url.scheme.n) { #ifndef UNSECURE if (!unsecure && url.scheme.n == 5 && !memcasecmp(url.scheme.p, "https", 5)) { usingssl = true; } else #endif if (!(url.scheme.n == 4 && !memcasecmp(url.scheme.p, "http", 4))) { return LuaNilError(L, "bad scheme"); } } #ifndef UNSECURE if (usingssl) keepalive = kaNONE; if (usingssl && !sslinitialized) TlsInit(); #endif if (url.host.n) { host = gc(strndup(url.host.p, url.host.n)); if (url.port.n) { port = gc(strndup(url.port.p, url.port.n)); #ifndef UNSECURE } else if (usingssl) { port = "443"; #endif } else { port = "80"; } } else if ((ip = ParseIp(urlarg, -1)) != -1) { host = urlarg; port = "80"; } else { return LuaNilError(L, "invalid host"); } if (!IsAcceptableHost(host, -1)) { return LuaNilError(L, "invalid host"); } if (!IsAcceptablePort(port, -1)) { return LuaNilError(L, "invalid port"); } if (!hosthdr) hosthdr = gc(xasprintf("%s:%s", host, port)); // check if hosthdr is in keepalive table if (keepalive && lua_istable(L, 2)) { lua_getfield(L, 2, "keepalive"); lua_getfield(L, -1, "close"); // aft: -2=tbl, -1=close lua_getfield(L, -2, hosthdr); // aft: -3=tbl, -2=close, -1=hosthdr if (lua_isinteger(L, -1)) { sock = lua_tointeger(L, -1); keepalive = lua_toboolean(L, -2) ? kaCLOSE : kaKEEP; // remove host mapping, as the socket is ether being closed // (so needs to be removed) or will be added after the request is done; // this also helps to keep the mapping clean in case of an error lua_pushnil(L); // aft: -4=tbl, -3=close, -2=hosthdr, -1=nil lua_setfield(L, -4, hosthdr); VERBOSEF("(ftch) reuse socket %d for host %s (and %s)", sock, hosthdr, keepalive == kaCLOSE ? "close" : "keep"); } lua_settop(L, 2); // drop all added elements to keep the stack balanced } url.fragment.p = 0, url.fragment.n = 0; url.scheme.p = 0, url.scheme.n = 0; url.user.p = 0, url.user.n = 0; url.pass.p = 0, url.pass.n = 0; url.host.p = 0, url.host.n = 0; url.port.p = 0, url.port.n = 0; if (!url.path.n || url.path.p[0] != '/') { void *p = gc(xmalloc(1 + url.path.n)); mempcpy(mempcpy(p, "/", 1), url.path.p, url.path.n); url.path.p = p; ++url.path.n; } /* * Create HTTP message. */ request = 0; appendf(&request, "%s %s HTTP/1.1\r\n" "Host: %s\r\n" "Connection: %s\r\n" "User-Agent: %s\r\n" "%s%s" "\r\n", method, gc(EncodeUrl(&url, 0)), hosthdr, (keepalive == kaNONE || keepalive == kaCLOSE) ? "close" : (connhdr ? connhdr : "keep-alive"), agenthdr, conlenhdr, headers ? headers : ""); appendd(&request, body, bodylen); requestlen = appendz(request).i; gc(request); if (keepalive == kaNONE || keepalive == kaOPEN) { /* * Perform DNS lookup. */ DEBUGF("(ftch) client resolving %s", host); if ((rc = getaddrinfo(host, port, &hints, &addr)) != 0) { return LuaNilError(L, "getaddrinfo(%s:%s) error: EAI_%s %s", host, port, gai_strerror(rc), strerror(errno)); } /* * Connect to server. */ ip = ntohl(((struct sockaddr_in *)addr->ai_addr)->sin_addr.s_addr); DEBUGF("(ftch) client connecting %hhu.%hhu.%hhu.%hhu:%d", ip >> 24, ip >> 16, ip >> 8, ip, ntohs(((struct sockaddr_in *)addr->ai_addr)->sin_port)); CHECK_NE(-1, (sock = GoodSocket(addr->ai_family, addr->ai_socktype, addr->ai_protocol, false, &timeout))); rc = connect(sock, addr->ai_addr, addr->ai_addrlen); freeaddrinfo(addr), addr = 0; if (rc == -1) { close(sock); return LuaNilError(L, "connect(%s:%s) error: %s", host, port, strerror(errno)); } } (void)bio; #ifndef UNSECURE if (usingssl) { if (sslcliused) { mbedtls_ssl_session_reset(&sslcli); } else { ReseedRng(&rngcli, "child"); } sslcliused = true; DEBUGF("(ftch) client handshaking %`'s", host); if (!evadedragnetsurveillance) { mbedtls_ssl_set_hostname(&sslcli, host); } bio = gc(malloc(sizeof(struct TlsBio))); bio->fd = sock; bio->a = 0; bio->b = 0; bio->c = -1; mbedtls_ssl_set_bio(&sslcli, bio, TlsSend, 0, TlsRecvImpl); while ((ret = mbedtls_ssl_handshake(&sslcli))) { switch (ret) { case MBEDTLS_ERR_SSL_WANT_READ: break; case MBEDTLS_ERR_X509_CERT_VERIFY_FAILED: goto VerifyFailed; default: close(sock); return LuaNilTlsError(L, "handshake", ret); } } LockInc(&shared->c.sslhandshakes); VERBOSEF("(ftch) shaken %s:%s %s %s", host, port, mbedtls_ssl_get_ciphersuite(&sslcli), mbedtls_ssl_get_version(&sslcli)); } #endif /* UNSECURE */ /* * Send HTTP Message. */ DEBUGF("(ftch) client sending %s request", method); for (i = 0; i < requestlen; i += rc) { #ifndef UNSECURE if (usingssl) { rc = mbedtls_ssl_write(&sslcli, request + i, requestlen - i); if (rc <= 0) { if (rc == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) goto VerifyFailed; close(sock); return LuaNilTlsError(L, "write", rc); } } else #endif if ((rc = WRITE(sock, request + i, requestlen - i)) <= 0) { close(sock); return LuaNilError(L, "write error: %s", strerror(errno)); } } if (logmessages) { LogMessage("sent", request, requestlen); } /* * Handle response. */ bzero(&inbuf, sizeof(inbuf)); InitHttpMessage(&msg, kHttpResponse); for (hdrsize = paylen = t = 0;;) { if (inbuf.n == inbuf.c) { inbuf.c += 1000; inbuf.c += inbuf.c >> 1; inbuf.p = realloc(inbuf.p, inbuf.c); } NOISEF("(ftch) client reading"); #ifndef UNSECURE if (usingssl) { if ((rc = mbedtls_ssl_read(&sslcli, inbuf.p + inbuf.n, inbuf.c - inbuf.n)) < 0) { if (rc == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { rc = 0; } else { close(sock); free(inbuf.p); DestroyHttpMessage(&msg); return LuaNilTlsError(L, "read", rc); } } } else #endif if ((rc = READ(sock, inbuf.p + inbuf.n, inbuf.c - inbuf.n)) == -1) { close(sock); free(inbuf.p); DestroyHttpMessage(&msg); return LuaNilError(L, "read error: %s", strerror(errno)); } g = rc; inbuf.n += g; switch (t) { case kHttpClientStateHeaders: if (!g) { WARNF("(ftch) HTTP client %s error", "EOF headers"); goto TransportError; } rc = ParseHttpMessage(&msg, inbuf.p, inbuf.n, SHRT_MAX); if (rc == -1) { WARNF("(ftch) HTTP client %s error", "ParseHttpMessage"); goto TransportError; } if (rc) { DEBUGF("(ftch) content-length is %`'.*s", FetchHeaderLength(kHttpContentLength), FetchHeaderData(kHttpContentLength)); hdrsize = rc; if (logmessages) { LogMessage("received", inbuf.p, hdrsize); } if (100 <= msg.status && msg.status <= 199) { if ((FetchHasHeader(kHttpContentLength) && !FetchHeaderEqualCase(kHttpContentLength, "0")) || (FetchHasHeader(kHttpTransferEncoding) && !FetchHeaderEqualCase(kHttpTransferEncoding, "identity"))) { WARNF("(ftch) HTTP client %s error", "Content-Length #1"); goto TransportError; } DestroyHttpMessage(&msg); InitHttpMessage(&msg, kHttpResponse); memmove(inbuf.p, inbuf.p + hdrsize, inbuf.n - hdrsize); inbuf.n -= hdrsize; break; } if (msg.status == 204 || msg.status == 304) { goto Finished; } if (FetchHasHeader(kHttpTransferEncoding) && !FetchHeaderEqualCase(kHttpTransferEncoding, "identity")) { if (FetchHeaderEqualCase(kHttpTransferEncoding, "chunked")) { t = kHttpClientStateBodyChunked; bzero(&u, sizeof(u)); goto Chunked; } else { WARNF("(ftch) HTTP client %s error", "Transfer-Encoding"); goto TransportError; } } else if (FetchHasHeader(kHttpContentLength)) { rc = ParseContentLength(FetchHeaderData(kHttpContentLength), FetchHeaderLength(kHttpContentLength)); if (rc == -1) { WARNF("(ftch) ParseContentLength(%`'.*s) failed", FetchHeaderLength(kHttpContentLength), FetchHeaderData(kHttpContentLength)); goto TransportError; } if ((paylen = rc) <= inbuf.n - hdrsize) { goto Finished; } else { t = kHttpClientStateBodyLengthed; } } else { t = kHttpClientStateBody; } } break; case kHttpClientStateBody: if (!g) { paylen = inbuf.n - hdrsize; goto Finished; } break; case kHttpClientStateBodyLengthed: if (!g) { WARNF("(ftch) HTTP client %s error", "EOF body"); goto TransportError; } if (inbuf.n - hdrsize >= paylen) { goto Finished; } break; case kHttpClientStateBodyChunked: Chunked: rc = Unchunk(&u, inbuf.p + hdrsize, inbuf.n - hdrsize, &paylen); if (rc == -1) { WARNF("(ftch) HTTP client %s error", "Unchunk"); goto TransportError; } if (rc) goto Finished; break; default: __builtin_unreachable(); } } Finished: if (paylen && logbodies) LogBody("received", inbuf.p + hdrsize, paylen); VERBOSEF("(ftch) completed %s HTTP%02d %d %s %`'.*s", method, msg.version, msg.status, urlarg, FetchHeaderLength(kHttpServer), FetchHeaderData(kHttpServer)); // check if the server has requested to close the connection // https://www.rfc-editor.org/rfc/rfc2616#section-14.10 if (keepalive && keepalive != kaCLOSE && FetchHasHeader(kHttpConnection) && FetchHeaderEqualCase(kHttpConnection, "close")) { VERBOSEF("(ftch) close keepalive on server request"); keepalive = kaCLOSE; } // need to save updated sock for keepalive if (keepalive && keepalive != kaCLOSE && lua_istable(L, 2)) { lua_getfield(L, 2, "keepalive"); lua_pushinteger(L, sock); lua_setfield(L, -2, hosthdr); lua_pop(L, 1); } if (followredirect && FetchHasHeader(kHttpLocation) && (msg.status == 301 || msg.status == 308 || // permanent redirects msg.status == 302 || msg.status == 307 || // temporary redirects msg.status == 303 /* see other; non-GET changes to GET, body lost */) && numredirects < maxredirects) { // if 303, then remove body and set method to GET if (msg.status == 303) { body = ""; bodylen = 0; method = "GET"; } // create table if needed if (!lua_istable(L, 2)) { lua_settop(L, 1); // pop body if present lua_createtable(L, 0, 3); // body, method, numredirects } lua_pushlstring(L, body, bodylen); lua_setfield(L, -2, "body"); lua_pushstring(L, method); lua_setfield(L, -2, "method"); lua_pushinteger(L, numredirects + 1); lua_setfield(L, -2, "numredirects"); // replace URL with Location header, which // can be a relative or absolute URL: // https://www.rfc-editor.org/rfc/rfc3986#section-4.2 gc(ParseUrl(FetchHeaderData(kHttpLocation), FetchHeaderLength(kHttpLocation), &url, true)); free(url.params.p); VERBOSEF("(ftch) client redirecting %`'.*s " "(scheme=%`'.*s, host=%`'.*s, port=%.*s, path=%`'.*s)", FetchHeaderLength(kHttpLocation), FetchHeaderData(kHttpLocation), url.scheme.n, url.scheme.p, url.host.n, url.host.p, url.port.n, url.port.p, url.path.n, url.path.p); // while it's possible to check for IsAcceptableHost/IsAcceptablePort // it's not clear what to do if they are not; // if they are invalid, redirect returns "invalid host" message if (url.host.n && url.scheme.n) { lua_pushlstring(L, FetchHeaderData(kHttpLocation), FetchHeaderLength(kHttpLocation)); } else { gc(ParseUrl(urlarg, urlarglen, &url, true)); free(url.params.p); // remove user/pass/fragment for the redirect url.fragment.p = 0, url.fragment.n = 0; url.user.p = 0, url.user.n = 0; url.pass.p = 0, url.pass.n = 0; if (FetchHeaderData(kHttpLocation)[0] == '/') { // if the path is absolute, then use it // so `/redir/more` -> `/less` becomes `/less` url.path.n = 0; // replace the path } else { // if the path is relative, then merge it, // so `/redir/more` -> `less` becomes `/redir/less` while (url.path.n > 0 && url.path.p[url.path.n - 1] != '/') { --url.path.n; } } url.path.p = gc(xasprintf("%.*s%.*s", url.path.n, url.path.p, FetchHeaderLength(kHttpLocation), FetchHeaderData(kHttpLocation))); url.path.n = strlen(url.path.p); lua_pushstring(L, gc(EncodeUrl(&url, 0))); } lua_replace(L, -3); DestroyHttpMessage(&msg); free(inbuf.p); if (!keepalive || keepalive == kaCLOSE) close(sock); return LuaFetch(L); } else { lua_pushinteger(L, msg.status); LuaPushHeaders(L, &msg, inbuf.p); lua_pushlstring(L, inbuf.p + hdrsize, paylen); DestroyHttpMessage(&msg); free(inbuf.p); if (!keepalive || keepalive == kaCLOSE) close(sock); return 3; } TransportError: DestroyHttpMessage(&msg); free(inbuf.p); close(sock); return LuaNilError(L, "transport error"); #ifndef UNSECURE VerifyFailed: LockInc(&shared->c.sslverifyfailed); close(sock); return LuaNilTlsError( L, gc(DescribeSslVerifyFailure(sslcli.session_negotiate->verify_result)), ret); #endif #undef ssl }