Added SILC HTTP, very simple HTTP server.
[silc.git] / lib / silchttp / silchttpserver.c
diff --git a/lib/silchttp/silchttpserver.c b/lib/silchttp/silchttpserver.c
new file mode 100644 (file)
index 0000000..fb5dae1
--- /dev/null
@@ -0,0 +1,590 @@
+/*
+
+  silchttpserver.c
+
+  Author: Pekka Riikonen <priikone@silcnet.org>
+
+  Copyright (C) 2006 Pekka Riikonen
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; version 2 of the License.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+*/
+
+#include "silc.h"
+#include "silchttpserver.h"
+
+/************************** Types and definitions ***************************/
+
+#define SILC_HTTP_SERVER_TIMEOUT  120   /* Connection timeout */
+#define SILC_HTTP_SERVER_CONNS    2    /* Default number of connections */
+#define SILC_HTTP_SERVER_BUFLEN   1024  /* Default data buffer length */
+#define SILC_HTTP_SERVER_HEADER   "HTTP/1.1 200 OK\r\nServer: SILCHTTP/1.0\r\n"
+
+/* HTTP server context */
+struct SilcHttpServerStruct {
+  SilcNetListener listener;        /* Server listener */
+  SilcSchedule schedule;           /* Scheduler */
+  SilcList allconns;               /* All connections */
+  SilcList conns;                  /* Connection free list */
+  SilcHttpServerCallback callback;  /* Requset callback */
+  void *context;                   /* Request callback context */
+};
+
+/* HTTP connection context */
+struct SilcHttpConnectionStruct {
+  struct SilcHttpConnectionStruct *next;
+  SilcHttpServer httpd;                    /* Server */
+  SilcStream stream;               /* Connection stream */
+  SilcBuffer inbuf;                /* Read data buffer */
+  SilcBuffer outbuf;               /* Write data buffer */
+  SilcInt64 touched;               /* Time last connection was touched */
+  SilcMime curheaders;             /* HTTP request headers */
+  SilcMime headers;                /* HTTP reply headers */
+  unsigned int keepalive    : 1;    /* Keep alive */
+};
+
+/************************ Static utility functions **************************/
+
+/* Close HTTP connection */
+
+static void silc_http_server_close_connection(SilcHttpConnection conn)
+{
+  if (conn->headers) {
+    silc_mime_free(conn->headers);
+    conn->headers = NULL;
+  }
+  if (conn->curheaders) {
+    silc_mime_free(conn->curheaders);
+    conn->curheaders = NULL;
+  }
+  silc_buffer_clear(conn->inbuf);
+  silc_buffer_clear(conn->outbuf);
+  silc_buffer_reset(conn->inbuf);
+  silc_buffer_reset(conn->outbuf);
+
+  if (conn->keepalive)
+    return;
+
+  SILC_LOG_DEBUG(("Closing HTTP connection"));
+
+  silc_schedule_task_del_by_context(conn->httpd->schedule, conn);
+  silc_stream_destroy(conn->stream);
+
+  /* Add to free list */
+  silc_list_add(conn->httpd->conns, conn);
+}
+
+/* Parse HTTP data */
+
+static SilcBool silc_http_server_parse(SilcHttpServer httpd,
+                                      SilcHttpConnection conn)
+{
+  SilcUInt32 data_len;
+  unsigned char *data, *tmp;
+  char *method, *uri;
+  const char *value;
+  SilcBufferStruct postdata;
+
+  SILC_LOG_DEBUG(("Parsing HTTP data"));
+  SILC_LOG_HEXDUMP(("HTTP data"), silc_buffer_data(conn->inbuf),
+                  silc_buffer_len(conn->inbuf));
+
+  data = silc_buffer_data(conn->inbuf);
+  data_len = silc_buffer_len(conn->inbuf);
+
+  if (data_len < 3)
+    return TRUE;
+
+  tmp = memchr(data, '\n', data_len);
+  if (!tmp || tmp[-1] != '\r') {
+    if (data_len < SILC_HTTP_SERVER_BUFLEN)
+      return TRUE;
+    return FALSE;
+  }
+  *tmp = 0;
+
+  /* Get method */
+  if (strchr(data, ' '))
+    *strchr(data, ' ') = 0;
+  method = data;
+  SILC_LOG_DEBUG(("Method: '%s'", method));
+
+  /* Get URI */
+  tmp = memchr(data, '\0', data_len);
+  if (!tmp) {
+    if (data_len < SILC_HTTP_SERVER_BUFLEN)
+      return TRUE;
+    return FALSE;
+  }
+  tmp++;
+  if (strchr(tmp, ' '))
+    *strchr(tmp, ' ') = 0;
+  uri = tmp;
+  SILC_LOG_DEBUG(("URI: '%s'", uri));
+
+  /* Get HTTP headers */
+  tmp++;
+  tmp = memchr(tmp, '\n', data_len - (tmp - data)) + 1;
+  if (!tmp) {
+    if (data_len < SILC_HTTP_SERVER_BUFLEN)
+      return TRUE;
+    return FALSE;
+  }
+  conn->curheaders = silc_mime_decode(NULL, tmp, data_len - (tmp - data));
+  if (!conn->curheaders) {
+    if (data_len < SILC_HTTP_SERVER_BUFLEN)
+      return TRUE;
+    return FALSE;
+  }
+
+  /* Check for persistent connection */
+  value = silc_mime_get_field(conn->curheaders, "Keep-alive");
+  if (value)
+    conn->keepalive = TRUE;
+  value = silc_mime_get_field(conn->curheaders, "Connection");
+  if (value && !strcasecmp(value, "keep-alive"))
+    conn->keepalive = TRUE;
+  if (value && !strcasecmp(value, "close"))
+    conn->keepalive = FALSE;
+
+  /* Deliver request to caller */
+  if (!strcasecmp(method, "GET") || !strcasecmp(method, "HEAD")) {
+    /* Send request to caller */
+    httpd->callback(httpd, conn, uri, method, NULL, httpd->context);
+
+  } else if (!strcasecmp(method, "POST")) {
+    /* Get POST data */
+    tmp = (unsigned char *)silc_mime_get_data(conn->curheaders, &data_len);
+    if (!tmp) {
+      silc_mime_free(conn->curheaders);
+      conn->curheaders = NULL;
+      if (data_len < SILC_HTTP_SERVER_BUFLEN)
+       return TRUE;
+      return FALSE;
+    }
+    silc_buffer_set(&postdata, tmp, data_len);
+    SILC_LOG_HEXDUMP(("HTTP POST data"), tmp, data_len);
+
+    /* Send request to caller */
+    httpd->callback(httpd, conn, uri, method, &postdata, httpd->context);
+
+  } else {
+    /* XXX Send bad request error */
+  }
+
+  return TRUE;
+}
+
+/* Send HTTP data to connection */
+
+static SilcBool silc_http_server_send_internal(SilcHttpServer httpd,
+                                              SilcHttpConnection conn,
+                                              SilcBuffer data,
+                                              SilcBool headers)
+{
+  int ret;
+
+  SILC_LOG_HEXDUMP(("HTTP data"), silc_buffer_data(data),
+                  silc_buffer_len(data));
+
+  /* Write the packet to the stream */
+  while (silc_buffer_len(data) > 0) {
+    ret = silc_stream_write(conn->stream, silc_buffer_data(data),
+                           silc_buffer_len(data));
+    if (ret == 0 || ret == - 2)
+      return FALSE;
+
+    if (ret == -1) {
+      /* Cannot write now, write later. */
+      if (silc_buffer_len(data) - ret >= silc_buffer_taillen(conn->outbuf))
+       if (!silc_buffer_realloc(conn->outbuf,
+                                silc_buffer_truelen(conn->outbuf) +
+                                silc_buffer_len(data) - ret)) {
+         conn->keepalive = FALSE;
+         silc_http_server_close_connection(conn);
+         return FALSE;
+       }
+      silc_buffer_pull_tail(conn->outbuf, silc_buffer_len(data) - ret);
+      silc_buffer_put(conn->outbuf, silc_buffer_data(data) + ret,
+                     silc_buffer_len(data) - ret);
+      return TRUE;
+    }
+
+    /* Wrote data */
+    silc_buffer_pull(data, ret);
+  }
+
+  if (!headers) {
+    /* Data sent, close connection */
+    SILC_LOG_DEBUG(("Data sent"));
+    silc_http_server_close_connection(conn);
+  }
+
+  return TRUE;
+}
+
+/* Allocate connection context */
+
+static SilcHttpConnection silc_http_server_alloc_connection(void)
+{
+  SilcHttpConnection conn;
+
+  conn = silc_calloc(1, sizeof(*conn));
+  if (!conn)
+    return NULL;
+
+  conn->inbuf = silc_buffer_alloc(SILC_HTTP_SERVER_BUFLEN);
+  if (!conn->inbuf) {
+    silc_free(conn);
+    return NULL;
+  }
+
+  conn->outbuf = silc_buffer_alloc(SILC_HTTP_SERVER_BUFLEN);
+  if (!conn->outbuf) {
+    silc_buffer_free(conn->inbuf);
+    silc_free(conn);
+    return NULL;
+  }
+
+  silc_buffer_reset(conn->inbuf);
+  silc_buffer_reset(conn->outbuf);
+
+  return conn;
+}
+
+/* Check if connection has timedout */
+
+SILC_TASK_CALLBACK(silc_http_server_connection_timeout)
+{
+  SilcHttpConnection conn = context;
+  SilcInt64 curtime = silc_time();
+
+  if (curtime - conn->touched > SILC_HTTP_SERVER_TIMEOUT) {
+    SILC_LOG_DEBUG(("Connection timeout"));
+    conn->keepalive = FALSE;
+    silc_http_server_close_connection(conn);
+    return;
+  }
+
+  silc_schedule_task_add_timeout(conn->httpd->schedule,
+                                silc_http_server_connection_timeout, conn,
+                                SILC_HTTP_SERVER_TIMEOUT, 0);
+}
+
+/* Data I/O callback */
+
+static void silc_http_server_io(SilcStream stream, SilcStreamStatus status,
+                               void *context)
+{
+  SilcHttpConnection conn = context;
+  SilcHttpServer httpd = conn->httpd;
+  int ret;
+
+  switch (status) {
+  case SILC_STREAM_CAN_READ:
+    SILC_LOG_DEBUG(("Read HTTP data"));
+
+    conn->touched = silc_time();
+
+    /* Make sure we have fair amount of free space in inbuf */
+    if (silc_buffer_taillen(conn->inbuf) < SILC_HTTP_SERVER_BUFLEN)
+      if (!silc_buffer_realloc(conn->inbuf, silc_buffer_truelen(conn->inbuf) +
+                              SILC_HTTP_SERVER_BUFLEN * 2)) {
+       conn->keepalive = FALSE;
+       silc_http_server_close_connection(conn);
+       return;
+      }
+
+    /* Read data from stream */
+    ret = silc_stream_read(conn->stream, conn->inbuf->tail,
+                          silc_buffer_taillen(conn->inbuf));
+
+    if (ret == 0 || ret == -2) {
+      silc_http_server_close_connection(conn);
+      return;
+    }
+
+    if (ret == -1) {
+      /* Cannot read now, do it later. */
+      silc_buffer_pull(conn->inbuf, silc_buffer_len(conn->inbuf));
+      return;
+    }
+
+    SILC_LOG_DEBUG(("Read %d bytes data", ret));
+
+    /* Parse the data */
+    silc_buffer_pull_tail(conn->inbuf, ret);
+    if (!silc_http_server_parse(httpd, conn))
+      silc_buffer_reset(conn->outbuf);
+
+    break;
+
+  case SILC_STREAM_CAN_WRITE:
+    SILC_LOG_DEBUG(("Write HTTP data"));
+
+    conn->touched = silc_time();
+
+    /* Write pending data to stream */
+    while (silc_buffer_len(conn->outbuf) > 0) {
+      ret = silc_stream_write(conn->stream, silc_buffer_data(conn->outbuf),
+                             silc_buffer_len(conn->outbuf));
+
+      if (ret == 0 || ret == -2) {
+       conn->keepalive = FALSE;
+       silc_http_server_close_connection(conn);
+       return;
+      }
+
+      if (ret == -1)
+       /* Cannot write now, write later. */
+       return;
+
+      /* Wrote data */
+      silc_buffer_pull(conn->outbuf, ret);
+    }
+
+    /* Data sent, close connection */
+    SILC_LOG_DEBUG(("Data sent"));
+    silc_http_server_close_connection(conn);
+    break;
+
+  default:
+    conn->keepalive = FALSE;
+    silc_http_server_close_connection(conn);
+    break;
+  }
+}
+
+/* Accepts new connection */
+
+static void silc_http_server_new_connection(SilcNetStatus status,
+                                           SilcStream stream,
+                                           void *context)
+{
+  SilcHttpServer httpd = context;
+  SilcHttpConnection conn;
+  const char *hostname = NULL, *ip = NULL;
+
+  /* Get free connection */
+  silc_list_start(httpd->conns);
+  conn = silc_list_get(httpd->conns);
+  if (!conn) {
+    conn = silc_http_server_alloc_connection();
+    if (!conn) {
+      silc_stream_destroy(stream);
+      return;
+    }
+  }
+
+  conn->httpd = httpd;
+  conn->stream = stream;
+
+  silc_socket_stream_get_info(stream, NULL, &hostname, &ip, NULL);
+  SILC_LOG_INFO(("HTTPD: New connection %s (%s)", hostname, ip));
+
+  /* Schedule the connection for data I/O */
+  silc_stream_set_notifier(stream, httpd->schedule, silc_http_server_io, conn);
+
+  /* Add connection timeout check */
+  silc_schedule_task_add_timeout(httpd->schedule,
+                                silc_http_server_connection_timeout, conn,
+                                SILC_HTTP_SERVER_TIMEOUT, 0);
+}
+
+
+/******************************* Public API *********************************/
+
+/* Allocate HTTP server */
+
+SilcHttpServer silc_http_server_alloc(const char *ip, SilcUInt16 port,
+                                     SilcUInt32 max_connections,
+                                     SilcSchedule schedule,
+                                     SilcHttpServerCallback callback,
+                                     void *context)
+{
+  SilcHttpServer httpd;
+  SilcHttpConnection conn;
+  int i;
+
+  SILC_LOG_DEBUG(("Start HTTP server at %s:%d", ip, port));
+
+  if (!ip || !schedule || !callback)
+    return FALSE;
+
+  httpd = silc_calloc(1, sizeof(*httpd));
+  if (!httpd)
+    return NULL;
+
+  /* Create server listener */
+  httpd->listener =
+    silc_net_tcp_create_listener(&ip, 1, port, TRUE, FALSE, schedule,
+                                silc_http_server_new_connection, httpd);
+  if (!httpd->listener) {
+    SILC_LOG_ERROR(("Could not bind HTTP server at %s:%d", ip, port));
+    silc_http_server_free(httpd);
+    return NULL;
+  }
+
+  httpd->schedule = schedule;
+  httpd->callback = callback;
+  httpd->context = context;
+
+  /* Allocate connections list */
+  if (!max_connections)
+    max_connections = SILC_HTTP_SERVER_CONNS;
+  for (i = 0; i < max_connections; i++) {
+    conn = silc_http_server_alloc_connection();
+    if (!conn)
+      break;
+    silc_list_add(httpd->conns, conn);
+    silc_list_add(httpd->allconns, conn);
+  }
+
+  SILC_LOG_DEBUG(("HTTP Server started"));
+
+  return httpd;
+}
+
+/* Free HTTP server */
+
+void silc_http_server_free(SilcHttpServer httpd)
+{
+  SilcHttpConnection conn;
+
+  silc_list_start(httpd->allconns);
+  while ((conn = silc_list_get(httpd->allconns))) {
+    conn->keepalive = FALSE;
+    silc_http_server_close_connection(conn);
+    silc_buffer_free(conn->inbuf);
+    silc_buffer_free(conn->outbuf);
+    silc_free(conn);
+  }
+
+  if (httpd->listener)
+    silc_net_close_listener(httpd->listener);
+
+  silc_free(httpd);
+}
+
+/* Send HTTP data to connection */
+
+SilcBool silc_http_server_send(SilcHttpServer httpd,
+                              SilcHttpConnection conn,
+                              SilcBuffer data)
+{
+  SilcBufferStruct h;
+  unsigned char *headers, tmp[16];
+  SilcUInt32 headers_len;
+  SilcBool ret;
+
+  SILC_LOG_DEBUG(("Sending HTTP data"));
+
+  conn->touched = silc_time();
+
+  /* Write headers */
+  silc_buffer_set(&h, SILC_HTTP_SERVER_HEADER,
+                 strlen(SILC_HTTP_SERVER_HEADER));
+  ret = silc_http_server_send_internal(httpd, conn, &h, TRUE);
+  if (!ret) {
+    conn->keepalive = FALSE;
+    silc_http_server_close_connection(conn);
+    return FALSE;
+  }
+
+  if (!conn->headers) {
+    conn->headers = silc_mime_alloc();
+    if (!conn->headers) {
+      conn->keepalive = FALSE;
+      silc_http_server_close_connection(conn);
+      return FALSE;
+    }
+  }
+
+  snprintf(tmp, sizeof(tmp), "%d", (int)silc_buffer_len(data));
+  silc_mime_add_field(conn->headers, "Content-Length", tmp);
+  silc_mime_add_field(conn->headers, "Connection", "keep-alive");
+  snprintf(tmp, sizeof(tmp), "%d", (int)SILC_HTTP_SERVER_TIMEOUT);
+  silc_mime_add_field(conn->headers, "Keep-alive", tmp);
+  silc_mime_add_field(conn->headers, "Last-Modified",
+                     silc_time_string(conn->touched));
+
+  headers = silc_mime_encode(conn->headers, &headers_len);
+  if (headers) {
+    silc_buffer_set(&h, headers, headers_len);
+    if (!silc_http_server_send_internal(httpd, conn, &h, TRUE)) {
+      conn->keepalive = FALSE;
+      silc_http_server_close_connection(conn);
+      return FALSE;
+    }
+    silc_free(headers);
+  }
+
+  /* Write the page data */
+  return silc_http_server_send_internal(httpd, conn, data, FALSE);
+}
+
+/* Send error reply */
+
+SilcBool silc_http_server_send_error(SilcHttpServer httpd,
+                                    SilcHttpConnection conn,
+                                    const char *error,
+                                    const char *error_message)
+{
+  SilcBool ret;
+  SilcBufferStruct data;
+
+  memset(&data, 0, sizeof(data));
+  silc_buffer_strformat(&data,
+                       "HTTP/1.1 ", error, "\r\n\r\n", error_message,
+                       SILC_STRFMT_END);
+
+  /* Send the message */
+  ret = silc_http_server_send_internal(httpd, conn, &data, FALSE);
+
+  silc_buffer_purge(&data);
+
+  /* Close connection */
+  conn->keepalive = FALSE;
+  silc_http_server_close_connection(conn);
+
+  return ret;
+}
+
+/* Get field */
+
+const char *silc_http_server_get_header(SilcHttpServer httpd,
+                                       SilcHttpConnection conn,
+                                       const char *field)
+{
+  if (!conn->curheaders)
+    return NULL;
+  return silc_mime_get_field(conn->curheaders, field);
+}
+
+/* Add field */
+
+SilcBool silc_http_server_add_header(SilcHttpServer httpd,
+                                    SilcHttpConnection conn,
+                                    const char *field,
+                                    const char *value)
+{
+  SILC_LOG_DEBUG(("Adding header %s:%s", field, value));
+
+  if (!conn->headers) {
+    conn->headers = silc_mime_alloc();
+    if (!conn->headers) {
+      silc_http_server_close_connection(conn);
+      return FALSE;
+    }
+  }
+
+  silc_mime_add_field(conn->headers, field, value);
+  return TRUE;
+}