/* Copyright (c) 2008, The Tor Project, Inc. */
/* See LICENSE for licensing information */
/* $Id: testservice.c $ */
const char testservice_c_id[] =
  "$Id: testservice.c $";

#include "or.h"
#include "testservice.h"

/**
 * \file testservice.c
 * \brief Code to serve test html pages to browsers on localhost.
 **/

/* In-points to testservice.c:
 *
 */
static int testservice_handle_command(connection_t *conn);

/********* START VARIABLES **********/

char *dnsleak_token=NULL, *proxytest_token=NULL, *connectivitytest_token=NULL;
char *testip=NULL;
char testpage[2056];
uint32_t testresult;

/********* END VARIABLES ************/
char *random_token(int len);
char *testservice_serve_page(uint32_t pagetype, int showingresult,
                             int automated);
int random_ip(char **ip);
char *html_redirectheader(uint32_t pagetype, int showingresult);

/** There is no reason for us to receive a resource request greater than 1K
 * long.
 */
#define MAX_RESOURCE_REQUEST_SIZE (1<<10)
#define LEN_TOKEN 8

/** Generate a random hexadecimal token of length LEN_TOKEN * 2.
 * The return value must be freed by the caller.
 */
char *
random_token(int len)
{
  char *data;
  char *output;
  int hexlen = (len*2);

  output = tor_malloc_zero(hexlen+1);
  tor_assert(output);
  data = tor_malloc_zero(len);
  tor_assert(data);
  tor_assert(!crypto_seed_rng(0));
  crypto_rand(data, len);
  base16_encode(output, hexlen+1, data, len);
  tor_free(data);
  return output;
}

/**
 * Generate a random, non-local IP address
 */

int
random_ip(char **ip)
{
  tor_addr_t addr;
  char tmpip[INET_NTOA_BUF_LEN];
  uint64_t a;

  repeat:
    tor_assert(!crypto_seed_rng(0));
    a = crypto_rand_uint64(UINT64_MAX-1);
    tor_snprintf(tmpip, sizeof(tmpip), "%d.%d.%d.%d",
                        (int)(uint8_t)((a>>24)&0xff),
                        (int)(uint8_t)((a>>16)&0xff),
                        (int)(uint8_t)((a>>8 )&0xff),
                        (int)(uint8_t)((a    )&0xff));
    tor_addr_from_ipv4h(&addr, a);

  if (is_local_addr(&addr))
    goto repeat;

  *ip = tor_malloc_zero(sizeof(tmpip));
  strncpy(*ip,tmpip,sizeof(tmpip));
  return 0;
}

/** Read handler for connections to the test service
 */
int
connection_testserv_process_inbuf(connection_t *conn)
{
  tor_assert(conn);

  /* Look for a command. */
  if (conn->state == TEST_CONN_STATE_SERVER_COMMAND_WAIT) {
    if (testservice_handle_command(conn) < 0) {
      connection_mark_for_close(conn);
      return -1;
    }
    return 0;
  }

  if (buf_datalen(conn->inbuf) > MAX_RESOURCE_REQUEST_SIZE) {
    log_warn(LD_HTTP, "Too much data received from test server connection: "
             "denial of service attempt, or you need to upgrade?");
    connection_mark_for_close(conn);
    return -1;
  }

  if (!conn->inbuf_reached_eof)
    log_debug(LD_HTTP,"Got data, not eof. Leaving on inbuf.");
  return 0;
}

/** Create an http response for the client <b>conn</b> out of
 * <b>status</b> and <b>reason_phrase</b>. Write it to <b>conn</b>.
 */
static void
write_http_status_line(connection_t *conn, int status,
                       const char *reason_phrase)
{
  char buf[256];
  if (tor_snprintf(buf, sizeof(buf), "HTTP/1.0 %d %s\r\n\r\n",
      status, reason_phrase ? reason_phrase : "OK") < 0) {
    log_warn(LD_BUG,"status line too long.");
    return;
  }
  connection_write_to_buf(buf, strlen(buf), conn);
}

/** Write the header for an HTTP/1.0 response onto <b>conn</b>-\>outbuf,
 * with <b>type</b> as the Content-Type.
 *
 * If <b>length</b> is nonnegative, it is the Content-Length.
 * If <b>encoding</b> is provided, it is the Content-Encoding.
 * If <b>cache_lifetime</b> is greater than 0, the content may be cached for
 * up to cache_lifetime seconds.  Otherwise, the content may not be cached. */
static void
write_http_response_header_impl(connection_t *conn, ssize_t length,
                           const char *type, const char *encoding,
                           const char *extra_headers,
                           long cache_lifetime)
{
  char date[RFC1123_TIME_LEN+1];
  char tmp[1024];
  char *cp;
  time_t now = time(NULL);

  tor_assert(conn);

  format_rfc1123_time(date, now);
  cp = tmp;
  tor_snprintf(cp, sizeof(tmp),
               "HTTP/1.0 200 OK\r\nDate: %s\r\n",
               date);
  cp += strlen(tmp);
  if (type) {
    tor_snprintf(cp, sizeof(tmp)-(cp-tmp), "Content-Type: %s\r\n", type);
    cp += strlen(cp);
  }

  if (encoding) {
    tor_snprintf(cp, sizeof(tmp)-(cp-tmp),
                 "Content-Encoding: %s\r\n", encoding);
    cp += strlen(cp);
  }
  if (length >= 0) {
    tor_snprintf(cp, sizeof(tmp)-(cp-tmp),
                 "Content-Length: %ld\r\n", (long)length);
    cp += strlen(cp);
  }
  if (cache_lifetime > 0) {
    char expbuf[RFC1123_TIME_LEN+1];
    format_rfc1123_time(expbuf, now + cache_lifetime);
    /* We could say 'Cache-control: max-age=%d' here if we start doing
     * http/1.1 */
    tor_snprintf(cp, sizeof(tmp)-(cp-tmp),
                 "Expires: %s\r\n", expbuf);
    cp += strlen(cp);
  } else if (cache_lifetime == 0) {
    /* We could say 'Cache-control: no-cache' here if we start doing
     * http/1.1 */
    strlcpy(cp, "Pragma: no-cache\r\n", sizeof(tmp)-(cp-tmp));
    cp += strlen(cp);
  }
  if (extra_headers)
    strlcpy(cp, extra_headers, sizeof(tmp)-(cp-tmp));
  if (sizeof(tmp)-(cp-tmp) > 3)
    memcpy(cp, "\r\n", 3);
  else
    tor_assert(0);

  connection_write_to_buf(tmp, strlen(tmp), conn);
}

/** Parse an HTTP request string <b>headers</b> of the form
 * \verbatim
 * "\%s [http[s]://]\%s HTTP/1..."
 * \endverbatim
 * If it's well-formed, strdup the second \%s into *<b>url</b>, and
 * nul-terminate it. Return 0.
 * Otherwise, return -1.
 */
static int
parse_http_url(const char *headers, char **url, char **stage, char **method)
{
  char *s, *start, *tmp;

  s = (char *)eat_whitespace_no_nl(headers);
  if (!*s) return -1;
  s = (char *)find_whitespace(s); /* get past GET/POST */
  if (!*s) return -1;
  s = (char *)eat_whitespace_no_nl(s);
  if (!*s) return -1;
  start = s; /* this is it, assuming it's valid */
  s = (char *)find_whitespace(start);
  if (!*s) return -1;

  /* tolerate the http[s] proxy style of putting the hostname in the url */
  if (s-start >= 4 && !strcmpstart(start,"http")) {
    tmp = start + 4;
    if (*tmp == 's')
      tmp++;
    if (s-tmp >= 3 && !strcmpstart(tmp,"://")) {
      tmp = strchr(tmp+3, '/');
      if (tmp && tmp < s) {
        log_debug(LD_DIR,"Skipping over 'http[s]://hostname' string");
        start = tmp;
      }
    }
  }

  *url = tor_strndup(start, s-start);

  /* Find the name of the next test, if present */
  tmp = strchr(start, '?');
  if ((tmp && tmp < s) && !strcmpstart(tmp,"?stage=")) {
    log_debug(LD_DIR,"Found get method.");
    start = tmp+7;
    *stage = tor_strndup(start, s-start);
  }

  tmp = strchr(start, '&');
  if ((tmp && tmp < s) && !strcmpstart(tmp,"&method=")) {
    log_debug(LD_DIR,"Found get method.");
    start = tmp+8;
    *method = tor_strndup(start, s-start);
  }

  return 0;
}

/** As write_http_response_header_impl, but sets encoding and content-typed
 * based on whether the response will be <b>compressed</b> or not. */
static void
write_http_response_header(connection_t *conn, ssize_t length,
                           int image, long cache_lifetime)
{
  write_http_response_header_impl(conn, length,
                          image?"image/png":"text/html",
                          "identity",
                          NULL,
                          cache_lifetime);
}

/**
 * Return the html body text for the desired page.
 */

static const char *
testservice_find_message(int type, int state, int part)
{
  int i;
  for (i=0; status_messages[i].type; ++i) {
    if ((status_messages[i].state == state) &&
       (status_messages[i].type == type)) {
      if (part == 1)
        return status_messages[i].htmlpart1;
      else
        return status_messages[i].htmlpart2;
    }
  }
  return " ";
}

/**
 * Figure out the appropriate html page to serve and retrieve it's body.
 */
static int
testservice_printmessage(char *htmlpart1, char *htmlpart2, size_t msglen,
                         uint32_t type, int showingresult)
{
  int result=0;

  switch (type) {
    case MAIN_PAGE:
    case END_PAGE:
      result=TEST_COMPLETE;
      break;
    default:
      if (showingresult)
        if (testresult & type)
          result=TEST_SUCCESSFUL;
        else
          result=TEST_FAILED;
      else
        result=TEST_INPROGRESS;
  }

  strlcpy(htmlpart1, testservice_find_message(type,result,1),
          msglen);
  strlcpy(htmlpart2, testservice_find_message(type,result,2),
          msglen);
  return result;
}


/** Helper function: called when a test server gets a complete HTTP GET
 * request. If the request is unrecognized, send a 400.
 * Always return 0. */
static int
testservice_handle_command_get(connection_t *conn, const char *headers,
                             const char *body, size_t body_len)
{
  char *url, *stage=NULL, *method=NULL;
  int pagetoserve=0, showingresult=0, automated=0;
  
  /* We ignore the body of a GET request. */
  (void)body;
  (void)body_len;

  log_debug(LD_TESTSERV,"Received GET command.");

  conn->state = TEST_CONN_STATE_SERVER_WRITING;

  if (parse_http_url(headers, &url, &stage, &method) < 0) {
    write_http_status_line(conn, 400, "Bad request");
    return 0;
  }

  if (stage)
    showingresult = (!strncmp("check",stage,5));
  if (method)
    automated = (!strcmp("auto",method));

  log_debug(LD_TESTSERV,"rewritten url as '%s'.", url);

  if ((!strcmp(url,"/")) || (!strncmp(url,"/ind",4)))
    pagetoserve=MAIN_PAGE;
  else if (!strncmp(url,"/pro",4))
    pagetoserve=PROXY_TEST;
  else if (!strncmp(url,"/dns",4))
    pagetoserve=DNS_TEST;
  else if (!strncmp(url,"/tor",4))
    pagetoserve=CONNECTIVITY_TEST;
  else if (!strncmp(url,"/end",4))
    pagetoserve=END_PAGE;

  if (pagetoserve) {
    char *bytes = testservice_serve_page(pagetoserve, showingresult,
automated);
    size_t len = strlen(bytes);
    write_http_response_header(conn, len, 0, 0);
    connection_write_to_buf(bytes, len, conn);
    goto done;
  }


  /* we didn't recognize the url */
  write_http_status_line(conn, 404, "Not found");

 done:
  control_event_client_status(LOG_NOTICE,
                              "TESTSERVICE_REQUEST TYPE=MAIN "
                              "RESOURCE=%s",url);
  tor_free(url);
  tor_free(stage);
  tor_free(method);

  return 0;
}

/** Called when a test server receives data; looks for an HTTP request.
 * If the request is complete, remove it from the inbuf, try to process it;
 * otherwise, leave it on the buffer.  Return a 0 on success, or -1 on error.
 */
static int
testservice_handle_command(connection_t *conn)
{
  char *headers=NULL, *body=NULL;
  size_t body_len=0;
  int r;

  tor_assert(conn);
  tor_assert(conn->type == CONN_TYPE_TEST);

  switch (fetch_from_buf_http(conn->inbuf,
                              &headers, MAX_HEADERS_SIZE,
                              &body, &body_len, MAX_DIR_UL_SIZE, 0)) {
    case -1: /* overflow */
      log_warn(LD_TESTSERV,
               "Invalid input from address '%s'. Closing.",
               conn->address);
      return -1;
    case 0:
      log_debug(LD_TESTSERV,"command not all here yet.");
      return 0;
    /* case 1, fall through */
  }

  if (!strncasecmp(headers,"GET",3))
    r = testservice_handle_command_get(conn, headers, body, body_len);
  else {
    log_fn(LOG_PROTOCOL_WARN, LD_PROTOCOL,
           "Got headers %s with unknown command. Closing.",
           escaped(headers));
    r = -1;
  }

  tor_free(headers); tor_free(body);
  return r;
}

/** Write handler for test service connections; called when all data has
 * been flushed.  Close the connection or wait for a response as
 * appropriate.
 */
int
connection_testserv_finished_flushing(connection_t *conn)
{
  tor_assert(conn);
  tor_assert(conn->type == CONN_TYPE_TEST);

  switch (conn->state) {
    case TEST_CONN_STATE_SERVER_WRITING:
      log_debug(LD_TESTSERV,"Finished writing server response. Closing.");
      connection_mark_for_close(conn);
      return 0;
    default:
      log_warn(LD_BUG,"called in unexpected state %d.",
               conn->state);
      tor_fragile_assert();
      return -1;
  }
  return 0;
}

/**
 * Return the appropriate html Refresh header, or NULL if none is required
 */
char *
html_redirectheader(uint32_t pagetype, int showingresult)
{
  char redirecturl[24];
  char *redirectheader=NULL,*args=NULL;

  args=tor_malloc_zero(32);
  tor_snprintf(args, 32,"?stage=%s&method=auto",
              (showingresult)?"test":"check");

  /* We write a redirect header if we are not doing a manual test,
     have not failed a test, and are not serving a static page */
  switch (pagetype) {
    case CONNECTIVITY_TEST:
      tor_snprintf(redirecturl,sizeof(redirecturl),"%s",
                  (showingresult)?"end.html":"tortest.html");
      break;
    case PROXY_TEST:
      tor_snprintf(redirecturl,sizeof(redirecturl),"%s",
                  (showingresult)?"dnstest.html":"proxy.html");
      break;
    case DNS_TEST:
      tor_snprintf(redirecturl,sizeof(redirecturl),"%s",
                   (showingresult)?"tortest.html":"dnstest.html");
      break;
    default:
      return NULL;
  }

  redirectheader = tor_malloc_zero(1024);
  tor_snprintf(redirectheader,1024,
               "<title>Tor</title><META HTTP-EQUIV='Refresh'"
               " CONTENT='3; URL=%s%s'></head>",
               redirecturl,
               (strcmp(redirecturl,"end.html"))?args:"");
  tor_free(args);

  return redirectheader;
}

/**
 * Serve a web page
 */
char *
testservice_serve_page(uint32_t pagetype, int showingresult, int automated)
{
  char testimage[2056];
  char htmlpart1[2056];
  char htmlpart2[2056];
  int continuetesting=0;
  char *redirectheader=NULL;

  testimage[0]='\0';

  /* Prepare the appropriate test image if this a test page, otherwise
     reset the test results.*/
  switch (pagetype) {
    case MAIN_PAGE:
      testresult=0;
      break;
    case END_PAGE:
      if ((testresult & PROXY_TEST) &&
          (testresult & DNS_TEST) &&
          (testresult & CONNECTIVITY_TEST))
          strlcpy(testimage,
                  "<div align='center'><h2>Successful!</h2></div>",
                  sizeof(testimage));
      else
          strlcpy(testimage,
                  "<div align='center'><h2>A Failure!</h2></div>",
                  sizeof(testimage));

      testresult=0;
      break;
    case PROXY_TEST:
      random_ip(&testip);
      proxytest_token = random_token(LEN_TOKEN);
      tor_snprintf(testimage, sizeof(testimage), "<IMG src="
                  "\"http://%s/%s.png\" "
                  "alt=\"If this page finishes loading or takes a long time "
                  "to load and you can still "
                  "see this text, your browser is not "
                  "configured to work with Tor.\" width=\"200\""
                  "height=\"200\""
                  "align=\"middle\" border=\"2\">\n",
                  testip,proxytest_token);
      break;
    case DNS_TEST:
      dnsleak_token = random_token(LEN_TOKEN);
      tor_snprintf(testimage, sizeof(testimage), "<IMG src="
                  "\"http://%s/%s.png\""
                  " alt=\"If this page finishes loading and you can still "
                  "see this text, your browser's DNS requests "
                  "are not being routed through Tor.\" width=\"200\" "
                  "height=\"200\" align=\"middle\" border=\"2\">\n",
                  dnsleak_token,dnsleak_token);
      break;
    case CONNECTIVITY_TEST:
      connectivitytest_token = random_token(LEN_TOKEN);
      tor_snprintf(testimage, sizeof(testimage), "<IMG src="
                  "\"http://roberthogan.net/images/%s-tortest.png\" "
                  "alt=\"If this page finishes loading and you can still "
                  "see this text, your Tor installation "
                  "cannot connect to the Internet.\" width=\"200\"  "
                  "height=\"200\" align=\"middle\" border=\"2\">\n",
                  connectivitytest_token);
      break;
    default:
      strlcpy(testpage,errorpage,sizeof(testpage));
      return testpage;
  }


  /* Prepare the text for the page. If we're about to inform the user of
     a failed test or if we're at the beginning or end of a test set,
     then we don't want to write a redirect. */
  continuetesting = testservice_printmessage(htmlpart1,
                                             htmlpart2, sizeof(htmlpart2),
                                             pagetype,
                                             showingresult);

  if ((continuetesting) && (automated))
    redirectheader = html_redirectheader(pagetype, showingresult);

  tor_snprintf(testpage,sizeof(testpage),"<HTML>%s<BODY>%s%s%s</BODY></HTML>",
               (redirectheader)?redirectheader:"",
               htmlpart1,testimage,htmlpart2);

  tor_free(redirectheader);
  return testpage;
}

/**
 * Is the address in the SOCKS request one embedded in an image resource
 * served by the test service? (DNS Test Image and Proxy Test Image Only)
 */
int
testservice_istestaddress(const char *address)
{
  if ((dnsleak_token) && (!strcasecmp(address,dnsleak_token))) {
      return 1;
  }

  if ((testip) && (!strcasecmp(address,testip))) {
      tor_free(testip);
      return 1;
  }

  return 0;
}

/**
 * If we have received a GET request on the SOCKS Port, serve a warning image.
 * XXXXX: SOCKS doesn't like responses greater than 2056, so the image served
 * here should not exceed that.
 */
int
testservice_handlebrowserusingtorasproxy(const char *buf,socks_request_t *req)
{
  char proxyrequest[256];
  char proxyresponse[MAX_SOCKS_REPLY_LEN];
  size_t len;
  tor_snprintf(proxyrequest, sizeof(proxyrequest),
               "GET http://%s/%s.png",testip,proxytest_token);
  if (!strncmp(buf,proxyrequest,strlen(proxyrequest))) {
      tor_snprintf(proxyresponse,sizeof(proxyresponse),
                  "HTTP/1.0 200 OK\r\n"
                  "Content-Type: image/png\r\n"
                  "Content-Encoding: identity\r\n"
                  "Content-Length: %i\r\n"
                  "Connection: close\r\n\r\n",
                  sizeof(proxytest_image));
      len = strlcpy(req->reply,
              proxyresponse,
              sizeof(proxyresponse));
      memcpy(req->reply+len,
             proxytest_image,
             sizeof(proxytest_image));
      req->replylen = ((sizeof(proxytest_image)+len+1) > MAX_SOCKS_REPLY_LEN) ?
                      MAX_SOCKS_REPLY_LEN:(sizeof(proxytest_image)+len+1);
      return 1;
  }
  return 0;
}

/**
 * Is the GET request received on Tor's SOCKSPort for a test image? If so
 * serve it. A return code of 1 tells the caller to close the conn on the
 * SOCKSPort.
 */
int
testservice_istestrequest(connection_t *conn)
{
  char dnsleak_resource[256], proxytest_resource[256];
  char connectivitytest_resource[256];

  tor_assert(conn);
  tor_assert(conn->type == CONN_TYPE_AP);

  if ((!dnsleak_token) &&
     (!proxytest_token) &&
     (!connectivitytest_token))
    return 0;

  if (dnsleak_token) {
    tor_snprintf(dnsleak_resource, sizeof(dnsleak_resource),
                 "GET /%s.png",dnsleak_token);
    if (buf_startswith(conn->inbuf,dnsleak_resource,25)) {
      write_http_response_header(conn, sizeof(onion_image), 1, 1);
      connection_write_to_buf(onion_image, sizeof(onion_image), conn);
      tor_free(dnsleak_token);
      dnsleak_token=NULL;
      testresult = (testresult | DNS_TEST);
      control_event_client_status(LOG_NOTICE,
                                  "TESTSERVICE_REQUEST TYPE=DNS RESOURCE=%s",
                                  dnsleak_resource);
      return 1;
    }
  }

  if (proxytest_token) {
    tor_snprintf(proxytest_resource, sizeof(proxytest_resource),
                 "GET /%s.png",proxytest_token);
    if (buf_startswith(conn->inbuf,proxytest_resource,25)) {
      write_http_response_header(conn, sizeof(onion_image), 1, 1);
      connection_write_to_buf(onion_image, sizeof(onion_image), conn);
      tor_free(proxytest_token);
      proxytest_token=NULL;
      testresult = (testresult | PROXY_TEST);
      control_event_client_status(LOG_NOTICE,
                                  "TESTSERVICE_REQUEST TYPE=PROXY "
                                  "RESOURCE=%s",
                                  proxytest_resource);
      return 1;
    }
  }

  // If we see the connectivitytest_token it means a circuit has been built
  // so we send the image back and close the connection.
  if (connectivitytest_token) {
    tor_snprintf(connectivitytest_resource, sizeof(connectivitytest_resource),
                 "GET /images/%s-tortest.png", connectivitytest_token);
    if (buf_startswith(conn->inbuf,connectivitytest_resource,40)) {
      write_http_response_header(conn, sizeof(onion_image), 1, 1);
      connection_write_to_buf(onion_image,
                              sizeof(onion_image), conn);
      tor_free(connectivitytest_token);
      connectivitytest_token=NULL;
      testresult = (testresult | CONNECTIVITY_TEST);
      control_event_client_status(LOG_NOTICE,
                                  "TESTSERVICE_REQUEST TYPE=PROXY "
                                  "RESOURCE=%s",
                                  connectivitytest_resource);
      return 1;
     }
  }
  return 0;
}

