// very simple http get function #include "cube.h" #include #define CR 13 #define LF 10 #define TCP_TIMEOUT 5678 // only re-use quite fresh connections #define GETERROR(msg) { err = msg; goto geterror; } const char lfcrlf[] = { LF, CR, LF, 0x00 }, *crlf = lfcrlf + 1; void httpget::reset(int keep) // keep == 0: delete all, keep == 1: keep connection, keep == 2: keep connection and url { if(keep < 1) { // reset connection DELSTRING(hostname); ip.host = ENET_HOST_ANY; ip.port = 80; // change manually, if needed maxredirects = 3; maxtransfer = maxsize = 2<<20; // 2 MB (caps transfer size and unzipped size) connecttimeout = 6000; disconnect(); } err = NULL; response = chunked = gzipped = contentlength = offset = elapsedtime = traffic = 0; if(keep < 2) DELSTRING(url); DELSTRING(header); rawsnd.setsize(0); rawrec.setsize(0); datarec.setsize(0); } bool httpget::set_host(const char *newhostname) // set and resolve host { if(!hostname || strcmp(hostname, newhostname)) { DELSTRING(hostname); disconnect(); // new or different host name -> close connection (if still open) ENetAddress newhost; if(enet_address_set_host(&newhost, newhostname) < 0) { err = "failed to resolve hostname"; return false; } ip.host = newhost.host; hostname = newstring(newhostname); } return ip.host != ENET_HOST_ANY; } void httpget::set_port(int port) // set port (disconnect, if different to previous) { port &= 65535; if(port < 80) port = 80; if(port != ip.port) disconnect(); ip.port = port; } bool httpget::execcallback(float progress) { if(callbackfunc && (*callbackfunc)(callbackdata, progress)) { err = "manual abort"; return true; } return false; } void httpget::disconnect() { if(tcp != ENET_SOCKET_NULL) { enet_socket_destroy(tcp); tcp = ENET_SOCKET_NULL; } } struct connectinfo { ENetSocket sock; ENetAddress address; volatile char running; }; int hcont(void *data) { connectinfo ci = *((connectinfo *)data); ((connectinfo *)data)->running = 1; // now the calling thread may as well vanish return enet_socket_connect(ci.sock, &ci.address); } bool httpget::connect(bool force) { if(force || tcp_age.elapsed() > TCP_TIMEOUT) disconnect(); if(tcp == ENET_SOCKET_NULL) { connectinfo ci = { enet_socket_create(ENET_SOCKET_TYPE_STREAM), ip, 0 }; tcp_age.start(); if(ci.sock == ENET_SOCKET_NULL) { err = "could not open socket"; return false; } void *ti = sl_createthread(hcont, &ci); loopk(connecttimeout / 250 + 1) { sl_detachthread(NULL); // cleanup call sl_sleep(250); if(ci.running) // wait for the connect thread to copy its data { if(sl_pollthread(ti)) { if(sl_waitthread(ti) < 0) { err = "could not connect"; ti = NULL; break; } else { tcp = ci.sock; return true; } } if(execcallback(-float(tcp_age.elapsed()) / (connecttimeout| 1))) break; // esc pressed } } sl_detachthread(ti); if(!err) err = "could not connect in time"; enet_socket_destroy(ci.sock); ASSERT(ci.running); return false; } return true; } int httpget::get(const char *url1, uint timeout, uint totaltimeout, int range, bool head) { int redirects = 0, res = 0, transferred = 0; vector *content = NULL; ASSERT(timeout && totaltimeout); // zero is not allowed stopwatch to; to.start(); uint lastresponse = 0; reset(1); // clear old response values and url url = newstring(url1); bool reconnected = false, retry = false; for(;;) { // (in a loop because of possible redirects) if(!connect()) goto geterror; // assemble GET request cvecprintf(rawsnd, "%s %s HTTP/1.1%s", head ? "HEAD" : "GET", url, crlf); cvecprintf(rawsnd, "Host: %s%s", hostname, crlf); cvecprintf(rawsnd, "Accept: */*%s", crlf); if(referrer) cvecprintf(rawsnd, "Referer: %s%s", referrer, crlf); if(useragent) cvecprintf(rawsnd, "User-Agent: %s%s", useragent, crlf); if(range > 0) cvecprintf(rawsnd, "Range: bytes=%d-%sTE: gzip%s", range, crlf, crlf); // content-encoding gzip for range requests is nonsense, so we'll ask for transfer-encoding instead (not that any server will deliver that...) else if(!head) cvecprintf(rawsnd, "Accept-Encoding: gzip%s", crlf); // ask for gzip only for full requests, because we intend to unzip instantly cvecprintf(rawsnd, "%s", crlf); // send request execcallback(0); ENetBuffer buf; buf.data = rawsnd.getbuf(); buf.dataLength = rawsnd.length(); while(buf.dataLength > 0) { enet_uint32 events = ENET_SOCKET_WAIT_SEND; if(enet_socket_wait(tcp, &events, 250) >= 0 && events) { int sent = enet_socket_send(tcp, NULL, &buf, 1); if(sent < 0) { if(!reconnected) { // line went dead, maybe the server closed it - try reconnecting once retry = true; break; } else GETERROR("connection failed"); } buf.data = (char *)buf.data + sent; buf.dataLength -= sent; tcp_age.start(); traffic += sent; lastresponse = to.elapsed(); } elapsedtime = to.elapsed(); if(elapsedtime > totaltimeout || elapsedtime > lastresponse + timeout) GETERROR("timeout 1"); if(execcallback(-float(elapsedtime - lastresponse) / timeout)) goto geterror; // show progress bar, check for user interrupt } // get response bool again = false, haveheader = false; if(!retry) for(;;) { elapsedtime = to.elapsed(); if(elapsedtime > totaltimeout || elapsedtime > lastresponse + timeout) GETERROR(elapsedtime > totaltimeout ? "timeout 2" : "timeout 3"); float progress = -float(elapsedtime - lastresponse) / timeout; enet_uint32 events = ENET_SOCKET_WAIT_RECEIVE; if(rawrec.length() > maxsize || datarec.length() > maxsize) break; if(enet_socket_wait(tcp, &events, 250) >= 0 && events) { if(rawrec.length() >= rawrec.capacity()) rawrec.reserve(4096); buf.data = rawrec.getbuf() + rawrec.length(); buf.dataLength = rawrec.capacity() - rawrec.length(); int recv = enet_socket_receive(tcp, NULL, &buf, 1); if(recv <= 0) { if(!haveheader && !reconnected) retry = true; // line may be half dead - retry once break; } rawrec.advance(recv); tcp_age.start(); traffic += recv; if((transferred += recv) > maxtransfer) GETERROR("transfer size exceeds limit"); lastresponse = to.elapsed(); // parsing received data rawrec.add('\0'); // append '\0' to use string functions safely - will be removed again if(!haveheader) { // still getting header size_t headendlen = strlen(lfcrlf); const char *endhead = strstr((const char *)rawrec.getbuf(), lfcrlf); if(!endhead && --headendlen) endhead = strstr((const char *)rawrec.getbuf(), "\n\n"); // we're tolerant to LF only line endings if(endhead) { size_t headlen = endhead - rawrec.getbuf() + headendlen; header = newstring(rawrec.getbuf(), headlen); rawrec.remove(0, headlen); haveheader = true; // parse header char *header_uc = filtertext(newstring(headlen), header, FTXT_TOUPPER, headlen); char *p = strstr(header, "HTTP/"); if(p) p = strchr(p, ' '); response = p ? atoi(p + 1) : -1; if(p) p = strchr(p, '\n'); if(!p) GETERROR("server error"); // header frame error if(response >= 300 && response < 308) // handle redirects { p = strstr(header_uc + (p - header), "LOCATION: "); if(p) p = header + (p - header_uc); DELSTRING(header_uc); if(++redirects > maxredirects || !p) GETERROR(p ? "too many redirects" : "redirect error"); // read redirect url disconnect(); p = strchr(p, ' ') + 1; char *r = strstr(p, "://"); if(r) p = r + 3; if((r = strchr(p, '/'))) { *r = '\0'; again = set_host(p); *r = '/'; p = r + strcspn(r, " \n\r"); *p = '\0'; const char *nurl = newstring(r); reset(1); url = nurl; } if(!again) GETERROR("bad redirect url"); break; // done receiving } // response was 2xx, check for some extras char *u = header_uc + (p - header), *f, *n; if((f = strstr(u, "TRANSFER-ENCODING: ")) && (n = strchr(f, LF))) { if((f = strstr(f, "CHUNKED")) && f < n) chunked = 1; if((f = strstr(f, "GZIP")) && f < n) gzipped = 1; // untested: I could not find a server who delivers that } if((f = strstr(u, "CONTENT-ENCODING: ")) && (n = strchr(f, LF)) && (f = strstr(f, "GZIP")) && f < n) gzipped = 1; if((f = strstr(u, "CONTENT-LENGTH: ")) && (f = strchr(f, ' '))) contentlength = atoi(f + 1); if((f = strstr(u, "CONTENT-RANGE: ")) && (n = strchr(f, LF)) && (f = strstr(f, "BYTES ")) && (f = strchr(f, ' ')) && f < n) offset = atoi(f + 1); DELSTRING(header_uc); } } if(haveheader) { // check if data is complete const char *d = rawrec.getbuf(), *e = d; if(chunked) { // n * "chunksize\r\nchunkdata\r\n" + "0\r\n\r\n" while(strchr(d, LF)) { int chunklen = (int) strtol(d, (char**) &e, 16); if(*e == CR) e++; if(*e == LF && rawrec.length() >= chunklen + (e - d) + 2 && (e[chunklen + 1] == LF || (e[chunklen + 1] == CR && e[chunklen + 2] == LF))) { // chunk is fully in buffer e++; loopi(chunklen) datarec.add(*e++); if(*e == CR) e++; if(*e++ != LF) GETERROR("chunk frame error"); rawrec.remove(0, int(e - d)); if(chunklen == 0) { contentlength = datarec.length(); content = &datarec; } } else break; } progress = float(datarec.length()) / (maxtransfer | 1); // not accurate, but moving } else { if(contentlength > 0) { progress = float(rawrec.length() - 1) / contentlength; // get a real progress bar if(rawrec.length() - 1 >= contentlength) content = &rawrec; } else progress = float(rawrec.length() - 1) / (maxtransfer | 1); // not accurate, but moving } } rawrec.drop(); } if(execcallback(progress)) goto geterror; // show progress bar, check for user interrupt if(content || (haveheader && head)) break; } if(retry && !reconnected) { // TCP connection showed some error before the response header was fully received: retry the connection once, it may have just been a timeout again = reconnected = true; reset(2); // only clear response values, not url disconnect(); // but close the socket retry = false; } if(again) continue; // redirect else break; } if(!content) content = &rawrec; // end by "server close connection" - this should maybe be flagged as error instead if(gzipped) { z_stream zs; memset((void *)&zs, 0, sizeof(zs)); if(inflateInit2(&zs, 15 + 32) == Z_OK) { zs.next_in = (Bytef*)content->getbuf(); zs.avail_in = content->length(); uchar outbuf[1<<15]; int zstat; do { zs.next_out = (Bytef*)outbuf; zs.avail_out = sizeof(outbuf); zstat = inflate(&zs, Z_NO_FLUSH); if(zstat == Z_OK || zstat == Z_STREAM_END) { int got = sizeof(outbuf) - zs.avail_out; if(got > 0) { res += got; if(outvec) loopi(got) outvec->add(outbuf[i]); if(outstream) outstream->write(outbuf, got); } } else GETERROR("gunzip error"); if(res > maxsize) break; } while(zstat == Z_OK); inflateEnd(&zs); } } else { res = content->length(); if(outvec) outvec->insert(outvec->length(), (const uchar *)content->getbuf(), res); if(outstream) outstream->write(content->getbuf(), res); } geterror: if(res > maxsize) err = "output truncated"; elapsedtime = to.elapsed(); if(err) disconnect(); return err ? -1 : res; } char *urlencode(const char *s, bool strict) { vector d; while(*s) { if(isalnum(*s) || strchr("-_.~", *s) || (!strict && strchr(":/?#[]@!$&'()*+,;=", *s))) d.add(*s++); // see RFC 3986 2.2 else cvecprintf(d, "%%%02X", *s++); } d.add('\0'); return newstring(d.getbuf()); } // parse URLs: scheme:[//[user:password@]domain[:port]][/]path[?query][#fragment] void urlparse::set(const char *newurl) { DELSTRING(buf); char *p, *s = buf = newstring(newurl, strlen(newurl) + 3); scheme = userpassword = port = path = query = fragment = ""; filtertext(s, s, FTXT_NOWHITE); if((p = strstr(s, "://"))) // domain part is mandatory, which makes the double slash a given - if a scheme is specified { // scheme found scheme = s; *p = '\0'; s = p + 3; } if((p = strchr(s, '@'))) { // user:password found userpassword = s; *p = '\0'; s = p + 1; } domain = s; if(*s == '[' && (p = strchr(s, ']'))) s = p + 1; // [....] else while(isalnum(*s) || *s == '.' || *s == '-') s++; // skip domain if(*s == ':') { // port found *s = '\0'; port = ++s; while(isdigit(*s)) s++; } if(*s) { memmove(s + 2, s, strlen(s) + 1); *s++ = '\0'; // end domain or port string *s++ = '/'; // add mandatory slash for path if(*s != '/') s--; path = s; if((p = strchr(s, '?'))) { // query found *p++ = '\0'; query = s = p; } if((p = strchr(s, '#'))) { // fragment found *p++ = '\0'; fragment = s = p; } // fix path, if necessary size_t pl = strlen(path); if(pl < 2) path = ""; // ignore single slash else { p = (char *)path; unixpath(p); p += pl - 1; if(*p == '/') *p = '\0'; // remove trailing slash } } }