Various features added
[pazpar2-moved-to-github.git] / http.c
diff --git a/http.c b/http.c
index 9de5c05..c806c40 100644 (file)
--- a/http.c
+++ b/http.c
@@ -1,5 +1,5 @@
 /*
- * $Id: http.c,v 1.1 2006-11-21 18:46:43 quinn Exp $
+ * $Id: http.c,v 1.4 2006-11-27 14:35:15 quinn Exp $
  */
 
 #include <stdio.h>
@@ -11,6 +11,8 @@
 #include <strings.h>
 #include <ctype.h>
 #include <fcntl.h>
+#include <netdb.h>
+#include <errno.h>
 
 #include <yaz/yaz-util.h>
 #include <yaz/comstack.h>
 #include "http.h"
 #include "http_command.h"
 
+static void proxy_io(IOCHAN i, int event);
+
 extern IOCHAN channel_list;
 
+static struct sockaddr_in *proxy_addr = 0; // If this is set, we proxy normal HTTP requests
+static char proxy_url[256] = "";
+static struct http_buf *http_buf_freelist = 0;
+
+static struct http_buf *http_buf_create()
+{
+    struct http_buf *r;
+
+    if (http_buf_freelist)
+    {
+        r = http_buf_freelist;
+        http_buf_freelist = http_buf_freelist->next;
+    }
+    else
+        r = xmalloc(sizeof(struct http_buf));
+    r->offset = 0;
+    r->len = 0;
+    r->next = 0;
+    return r;
+}
+
+static void http_buf_destroy(struct http_buf *b)
+{
+    b->next = http_buf_freelist;
+    http_buf_freelist = b;
+}
+
+static void http_buf_destroy_queue(struct http_buf *b)
+{
+    struct http_buf *p;
+    while (b)
+    {
+        p = b->next;
+        http_buf_destroy(b);
+        b = p;
+    }
+}
+
+#ifdef GAGA
+// Calculate length of chain
+static int http_buf_len(struct http_buf *b)
+{
+    int sum = 0;
+    for (; b; b = b->next)
+        sum += b->len;
+    return sum;
+}
+#endif
+
+static struct http_buf *http_buf_bybuf(char *b, int len)
+{
+    struct http_buf *res = 0;
+    struct http_buf **p = &res;
+
+    while (len)
+    {
+        *p = http_buf_create();
+        int tocopy = len;
+        if (tocopy > HTTP_BUF_SIZE)
+            tocopy = HTTP_BUF_SIZE;
+        memcpy((*p)->buf, b, tocopy);
+        (*p)->len = tocopy;
+        len -= tocopy;
+        b += tocopy;
+        p = &(*p)->next;
+    }
+    return res;
+}
+
+// Add a (chain of) buffers to the end of an existing queue.
+static void http_buf_enqueue(struct http_buf **queue, struct http_buf *b)
+{
+    while (*queue)
+        queue = &(*queue)->next;
+    *queue = b;
+}
+
+static struct http_buf *http_buf_bywrbuf(WRBUF wrbuf)
+{
+    return http_buf_bybuf(wrbuf_buf(wrbuf), wrbuf_len(wrbuf));
+}
+
+// Non-destructively collapse chain of buffers into a string (max *len)
+// Return
+static int http_buf_peek(struct http_buf *b, char *buf, int len)
+{
+    int rd = 0;
+    while (b && rd < len)
+    {
+        int toread = len - rd;
+        if (toread > b->len)
+            toread = b->len;
+        memcpy(buf + rd, b->buf + b->offset, toread);
+        rd += toread;
+        b = b->next;
+    }
+    buf[rd] = '\0';
+    return rd;
+}
+
+// Ddestructively munch up to len  from head of queue.
+static int http_buf_read(struct http_buf **b, char *buf, int len)
+{
+    int rd = 0;
+    while ((*b) && rd < len)
+    {
+        int toread = len - rd;
+        if (toread > (*b)->len)
+            toread = (*b)->len;
+        memcpy(buf + rd, (*b)->buf + (*b)->offset, toread);
+        rd += toread;
+        if (toread < (*b)->len)
+        {
+            (*b)->len -= toread;
+            (*b)->offset += toread;
+            break;
+        }
+        else
+        {
+            struct http_buf *n = (*b)->next;
+            http_buf_destroy(*b);
+            *b = n;
+        }
+    }
+    buf[rd] = '\0';
+    return rd;
+}
+
+void static urldecode(char *i, char *o)
+{
+    while (*i)
+    {
+        if (*i == '+')
+        {
+            *(o++) = ' ';
+            i++;
+        }
+        else if (*i == '%')
+        {
+            i++;
+            sscanf(i, "%2hhx", o);
+            i += 2;
+            o++;
+        }
+        else
+            *(o++) = *(i++);
+    }
+    *o = '\0';
+}
+
 void http_addheader(struct http_response *r, const char *name, const char *value)
 {
     struct http_channel *c = r->channel;
@@ -35,16 +189,18 @@ void http_addheader(struct http_response *r, const char *name, const char *value
     r->headers = h;
 }
 
-char *argbyname(struct http_request *r, char *name)
+char *http_argbyname(struct http_request *r, char *name)
 {
     struct http_argument *p;
+    if (!name)
+        return 0;
     for (p = r->arguments; p; p = p->next)
         if (!strcmp(p->name, name))
             return p->value;
     return 0;
 }
 
-char *headerbyname(struct http_request *r, char *name)
+char *http_headerbyname(struct http_request *r, char *name)
 {
     struct http_header *p;
     for (p = r->headers; p; p = p->next)
@@ -67,10 +223,13 @@ struct http_response *http_create_response(struct http_channel *c)
 // Check if we have a complete request. Return 0 or length (including trailing newline)
 // FIXME: Does not deal gracefully with requests carrying payload
 // but this is kind of OK since we will reject anything other than an empty GET
-static int request_check(const char *buf)
+static int request_check(struct http_buf *queue)
 {
+    char tmp[4096];
     int len = 0;
+    char *buf = tmp;
 
+    http_buf_peek(queue, tmp, 4096);
     while (*buf) // Check if we have a sequence of lines terminated by an empty line
     {
         char *b = strstr(buf, "\r\n");
@@ -86,20 +245,32 @@ static int request_check(const char *buf)
     return 0;
 }
 
-struct http_request *http_parse_request(struct http_channel *c, char *buf)
+struct http_request *http_parse_request(struct http_channel *c, struct http_buf **queue,
+        int len)
 {
     struct http_request *r = nmem_malloc(c->nmem, sizeof(*r));
     char *p, *p2;
+    char tmp[4096];
+    char *buf = tmp;
+
+    if (len > 4096)
+        return 0;
+    if (http_buf_read(queue, buf, len) < len)
+        return 0;
 
     r->channel = c;
+    r->arguments = 0;
+    r->headers = 0;
     // Parse first line
-    if (!strncmp(buf, "GET ", 4))
-        r->method = Method_GET;
-    else
+    for (p = buf, p2 = r->method; *p && *p != ' ' && p - buf < 19; p++)
+        *(p2++) = *p;
+    if (*p != ' ')
     {
         yaz_log(YLOG_WARN, "Unexpected HTTP method in request");
         return 0;
     }
+    *p2 = '\0';
+
     if (!(buf = strchr(buf, ' ')))
     {
         yaz_log(YLOG_WARN, "Syntax error in request (1)");
@@ -135,6 +306,7 @@ struct http_request *http_parse_request(struct http_channel *c, char *buf)
             a = nmem_malloc(c->nmem, sizeof(struct http_argument));
             *(equal++) = '\0';
             a->name = nmem_strdup(c->nmem, p2);
+            urldecode(equal, equal);
             a->value = nmem_strdup(c->nmem, equal);
             a->next = r->arguments;
             r->arguments = a;
@@ -151,18 +323,47 @@ struct http_request *http_parse_request(struct http_channel *c, char *buf)
         if (!(p = strstr(buf, "\r\n")))
             return 0;
         *(p++) = '\0';
+        p++;
         strcpy(r->http_version, buf);
         buf = p;
     }
     strcpy(c->version, r->http_version);
 
-    r->headers = 0; // We might want to parse these someday
+    r->headers = 0;
+    while (*buf)
+    {
+        if (!(p = strstr(buf, "\r\n")))
+            return 0;
+        if (p == buf)
+            break;
+        else
+        {
+            struct http_header *h = nmem_malloc(c->nmem, sizeof(*h));
+            if (!(p2 = strchr(buf, ':')))
+                return 0;
+            *(p2++) = '\0';
+            h->name = nmem_strdup(c->nmem, buf);
+            while (isspace(*p2))
+                p2++;
+            if (p2 >= p) // Empty header?
+            {
+                buf = p + 2;
+                continue;
+            }
+            *p = '\0';
+            h->value = nmem_strdup(c->nmem, p2);
+            h->next = r->headers;
+            r->headers = h;
+            buf = p + 2;
+        }
+    }
 
     return r;
 }
 
 
-static char *http_serialize_response(struct http_channel *c, struct http_response *r)
+static struct http_buf *http_serialize_response(struct http_channel *c,
+        struct http_response *r)
 {
     wrbuf_rewind(c->wrbuf);
     struct http_header *h;
@@ -177,16 +378,57 @@ static char *http_serialize_response(struct http_channel *c, struct http_respons
     if (r->payload)
         wrbuf_puts(c->wrbuf, r->payload);
 
-    wrbuf_putc(c->wrbuf, '\0');
-    return wrbuf_buf(c->wrbuf);
+    return http_buf_bywrbuf(c->wrbuf);
+}
+
+// Serialize a HTTP request
+static struct http_buf *http_serialize_request(struct http_request *r)
+{
+    struct http_channel *c = r->channel;
+    wrbuf_rewind(c->wrbuf);
+    struct http_header *h;
+    struct http_argument *a;
+
+    wrbuf_printf(c->wrbuf, "%s %s", r->method, r->path);
+
+    if (r->arguments)
+    {
+        wrbuf_putc(c->wrbuf, '?');
+        for (a = r->arguments; a; a = a->next) {
+            if (a != r->arguments)
+                wrbuf_putc(c->wrbuf, '&');
+            wrbuf_printf(c->wrbuf, "%s=%s", a->name, a->value);
+        }
+    }
+
+    wrbuf_printf(c->wrbuf, " HTTP/%s\r\n", r->http_version);
+
+    for (h = r->headers; h; h = h->next)
+        wrbuf_printf(c->wrbuf, "%s: %s\r\n", h->name, h->value);
+
+    wrbuf_puts(c->wrbuf, "\r\n");
+    
+    return http_buf_bywrbuf(c->wrbuf);
 }
 
+
 // Cleanup
 static void http_destroy(IOCHAN i)
 {
     struct http_channel *s = iochan_getdata(i);
 
-    yaz_log(YLOG_DEBUG, "Destroying http channel");
+    if (s->proxy)
+    {
+        if (s->proxy->iochan)
+        {
+            close(iochan_getfd(s->proxy->iochan));
+            iochan_destroy(s->proxy->iochan);
+        }
+        http_buf_destroy_queue(s->proxy->oqueue);
+        xfree(s->proxy);
+    }
+    http_buf_destroy_queue(s->iqueue);
+    http_buf_destroy_queue(s->oqueue);
     nmem_destroy(s->nmem);
     wrbuf_free(s->wrbuf, 1);
     xfree(s);
@@ -194,6 +436,76 @@ static void http_destroy(IOCHAN i)
     iochan_destroy(i);
 }
 
+static int http_weshouldproxy(struct http_request *rq)
+{
+    if (proxy_addr && !strstr(rq->path, "search.pz2"))
+        return 1;
+    return 0;
+}
+
+static int http_proxy(struct http_request *rq)
+{
+    struct http_channel *c = rq->channel;
+    struct http_proxy *p = c->proxy;
+    struct http_header *hp;
+    struct http_buf *requestbuf;
+
+    if (!p) // This is a new connection. Create a proxy channel
+    {
+        int sock;
+        struct protoent *pe;
+        int one = 1;
+        int flags;
+
+        if (!(pe = getprotobyname("tcp"))) {
+            abort();
+        }
+        if ((sock = socket(PF_INET, SOCK_STREAM, pe->p_proto)) < 0)
+        {
+            yaz_log(YLOG_WARN|YLOG_ERRNO, "socket");
+            return -1;
+        }
+        if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)
+                        &one, sizeof(one)) < 0)
+            abort();
+        if ((flags = fcntl(sock, F_GETFL, 0)) < 0) 
+            yaz_log(YLOG_FATAL|YLOG_ERRNO, "fcntl");
+        if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0)
+            yaz_log(YLOG_FATAL|YLOG_ERRNO, "fcntl2");
+        if (connect(sock, (struct sockaddr *) proxy_addr, sizeof(*proxy_addr)) < 0)
+            if (errno != EINPROGRESS)
+            {
+                yaz_log(YLOG_WARN|YLOG_ERRNO, "Proxy connect");
+                return -1;
+            }
+
+        p = xmalloc(sizeof(struct http_proxy));
+        p->oqueue = 0;
+        p->channel = c;
+        c->proxy = p;
+        // We will add EVENT_OUTPUT below
+        p->iochan = iochan_create(sock, proxy_io, EVENT_INPUT);
+        iochan_setdata(p->iochan, p);
+        p->iochan->next = channel_list;
+        channel_list = p->iochan;
+    }
+
+    // Modify Host: header
+    for (hp = rq->headers; hp; hp = hp->next)
+        if (!strcmp(hp->name, "Host"))
+            break;
+    if (!hp)
+    {
+        yaz_log(YLOG_WARN, "Failed to find Host header in proxy");
+        return -1;
+    }
+    hp->value = nmem_strdup(c->nmem, proxy_url);
+    requestbuf = http_serialize_request(rq);
+    http_buf_enqueue(&p->oqueue, requestbuf);
+    iochan_setflag(p->iochan, EVENT_OUTPUT);
+    return 0;
+}
+
 static void http_io(IOCHAN i, int event)
 {
     struct http_channel *hc = iochan_getdata(i);
@@ -202,74 +514,99 @@ static void http_io(IOCHAN i, int event)
 
     switch (event)
     {
-        int res;
+        int res, reqlen;
+        struct http_buf *htbuf;
 
         case EVENT_INPUT:
-            yaz_log(YLOG_DEBUG, "HTTP Input event");
-
-            res = read(iochan_getfd(i), hc->ibuf + hc->read, IBUF_SIZE - (hc->read + 1));
-            if (res <= 0)
+            htbuf = http_buf_create();
+            res = read(iochan_getfd(i), htbuf->buf, HTTP_BUF_SIZE -1);
+            if (res <= 0 && errno != EAGAIN)
             {
-                yaz_log(YLOG_WARN|YLOG_ERRNO, "HTTP read");
+                http_buf_destroy(htbuf);
                 http_destroy(i);
                 return;
             }
-            yaz_log(YLOG_DEBUG, "HTTP read %d octets", res);
-            hc->read += res;
-            hc->ibuf[hc->read] = '\0';
-
-            if ((res = request_check(hc->ibuf)) <= 2)
+            if (res > 0)
             {
-                yaz_log(YLOG_DEBUG, "We don't have a complete HTTP request yet");
-                return;
+                htbuf->buf[res] = '\0';
+                htbuf->len = res;
+                http_buf_enqueue(&hc->iqueue, htbuf);
             }
-            yaz_log(YLOG_DEBUG, "We think we have a complete HTTP request (len %d): \n%s", res,  hc->ibuf);
+
+            if ((reqlen = request_check(hc->iqueue)) <= 2)
+                return;
+
             nmem_reset(hc->nmem);
-            if (!(request = http_parse_request(hc, hc->ibuf)))
+            if (!(request = http_parse_request(hc, &hc->iqueue, reqlen)))
             {
                 yaz_log(YLOG_WARN, "Failed to parse request");
                 http_destroy(i);
                 return;
             }
-            response = http_command(request);
-            if (!response)
+            yaz_log(YLOG_LOG, "Request: %s %s v %s", request->method,  request->path,
+                    request->http_version);
+            if (http_weshouldproxy(request))
+                http_proxy(request);
+            else
             {
-                http_destroy(i);
-                return;
+                struct http_buf *hb;
+                // Execute our business logic!
+                response = http_command(request);
+                if (!response)
+                {
+                    http_destroy(i);
+                    return;
+                }
+                if (!(hb =  http_serialize_response(hc, response)))
+                {
+                    http_destroy(i);
+                    return;
+                }
+                http_buf_enqueue(&hc->oqueue, hb);
+                iochan_setflags(i, EVENT_OUTPUT); // Turns off input selecting
             }
-            // FIXME -- do something to cause the response to be sent to the client
-            if (!(hc->obuf = http_serialize_response(hc, response)))
+            if (hc->iqueue)
             {
-                http_destroy(i);
-                return;
+                yaz_log(YLOG_DEBUG, "We think we have more input to read. Forcing event");
+                iochan_setevent(i, EVENT_INPUT);
             }
-            yaz_log(YLOG_DEBUG, "Response ready:\n%s", hc->obuf);
-            hc->writ = 0;
-            hc->read = 0;
-            iochan_setflags(i, EVENT_OUTPUT); // Turns off input selecting
+
             break;
 
         case EVENT_OUTPUT:
-            yaz_log(YLOG_DEBUG, "HTTP output event");
-            res = write(iochan_getfd(hc->iochan), hc->obuf + hc->writ,
-                        strlen(hc->obuf + hc->writ));
-            if (res <= 0)
+            if (hc->oqueue)
             {
-                yaz_log(YLOG_WARN|YLOG_ERRNO, "write");
-                http_destroy(i);
-                return;
-            }
-            hc->writ += res;
-            if (!hc->obuf[hc->writ]) {
-                yaz_log(YLOG_DEBUG, "Writing finished");
-                if (!strcmp(hc->version, "1.0"))
+                struct http_buf *wb = hc->oqueue;
+                res = write(iochan_getfd(hc->iochan), wb->buf + wb->offset, wb->len);
+                if (res <= 0)
                 {
-                    yaz_log(YLOG_DEBUG, "Closing 1.0 connection");
+                    yaz_log(YLOG_WARN|YLOG_ERRNO, "write");
                     http_destroy(i);
+                    return;
+                }
+                if (res == wb->len)
+                {
+                    hc->oqueue = hc->oqueue->next;
+                    http_buf_destroy(wb);
                 }
                 else
-                    iochan_setflags(i, EVENT_INPUT); // Turns off output flag
+                {
+                    wb->len -= res;
+                    wb->offset += res;
+                }
+                if (!hc->oqueue) {
+                    if (!strcmp(hc->version, "1.0"))
+                    {
+                        http_destroy(i);
+                        return;
+                    }
+                    else
+                        iochan_setflags(i, EVENT_INPUT); // Turns off output flag
+                }
             }
+
+            if (!hc->oqueue && hc->proxy && !hc->proxy->iochan) 
+                http_destroy(i); // Server closed; we're done
             break;
         default:
             yaz_log(YLOG_WARN, "Unexpected event on connection");
@@ -277,6 +614,80 @@ static void http_io(IOCHAN i, int event)
     }
 }
 
+// Handles I/O on a client connection to a backend web server (proxy mode)
+static void proxy_io(IOCHAN pi, int event)
+{
+    struct http_proxy *pc = iochan_getdata(pi);
+    struct http_channel *hc = pc->channel;
+
+    switch (event)
+    {
+        int res;
+        struct http_buf *htbuf;
+
+        case EVENT_INPUT:
+            htbuf = http_buf_create();
+            res = read(iochan_getfd(pi), htbuf->buf, HTTP_BUF_SIZE -1);
+            if (res == 0 || (res < 0 && errno != EINPROGRESS))
+            {
+                if (hc->oqueue)
+                {
+                    yaz_log(YLOG_WARN, "Proxy read came up short");
+                    // Close channel and alert client HTTP channel that we're gone
+                    http_buf_destroy(htbuf);
+                    close(iochan_getfd(pi));
+                    iochan_destroy(pi);
+                    pc->iochan = 0;
+                }
+                else
+                {
+                    http_destroy(hc->iochan);
+                    return;
+                }
+            }
+            else
+            {
+                htbuf->buf[res] = '\0';
+                htbuf->len = res;
+                http_buf_enqueue(&hc->oqueue, htbuf);
+            }
+            iochan_setflag(hc->iochan, EVENT_OUTPUT);
+            break;
+        case EVENT_OUTPUT:
+            if (!(htbuf = pc->oqueue))
+            {
+                iochan_clearflag(pi, EVENT_OUTPUT);
+                return;
+            }
+            res = write(iochan_getfd(pi), htbuf->buf + htbuf->offset, htbuf->len);
+            if (res <= 0)
+            {
+                yaz_log(YLOG_WARN|YLOG_ERRNO, "write");
+                http_destroy(hc->iochan);
+                return;
+            }
+            if (res == htbuf->len)
+            {
+                struct http_buf *np = htbuf->next;
+                http_buf_destroy(htbuf);
+                pc->oqueue = np;
+            }
+            else
+            {
+                htbuf->len -= res;
+                htbuf->offset += res;
+            }
+
+            if (!pc->oqueue) {
+                iochan_setflags(pi, EVENT_INPUT); // Turns off output flag
+            }
+            break;
+        default:
+            yaz_log(YLOG_WARN, "Unexpected event on connection");
+            http_destroy(hc->iochan);
+    }
+}
+
 /* Accept a new command connection */
 static void http_accept(IOCHAN i, int event)
 {
@@ -303,17 +714,17 @@ static void http_accept(IOCHAN i, int event)
     c = iochan_create(s, http_io, EVENT_INPUT | EVENT_EXCEPT);
 
     ch = xmalloc(sizeof(*ch));
-    ch->read = 0;
+    ch->proxy = 0;
     ch->nmem = nmem_create();
     ch->wrbuf = wrbuf_alloc();
     ch->iochan = c;
+    ch->iqueue = ch->oqueue = 0;
     iochan_setdata(c, ch);
 
     c->next = channel_list;
     channel_list = c;
 }
 
-
 /* Create a http-channel listener */
 void http_init(int port)
 {
@@ -347,6 +758,31 @@ void http_init(int port)
     channel_list = c;
 }
 
+void http_set_proxyaddr(char *host)
+{
+    char *p;
+    int port;
+    struct hostent *he;
+
+    strcpy(proxy_url, host);
+    p = strchr(host, ':');
+    yaz_log(YLOG_DEBUG, "Proxying for %s", host);
+    if (p) {
+        port = atoi(p + 1);
+        *p = '\0';
+    }
+    else
+        port = 80;
+    if (!(he = gethostbyname(host))) 
+    {
+        fprintf(stderr, "Failed to lookup '%s'\n", host);
+        exit(1);
+    }
+    proxy_addr = xmalloc(sizeof(struct sockaddr_in));
+    proxy_addr->sin_family = he->h_addrtype;
+    memcpy(&proxy_addr->sin_addr.s_addr, he->h_addr_list[0], he->h_length);
+    proxy_addr->sin_port = htons(port);
+}
 
 /*
  * Local variables: