PageRenderTime 186ms CodeModel.GetById 30ms app.highlight 121ms RepoModel.GetById 20ms app.codeStats 1ms

/js/lib/Socket.IO-node/support/expresso/deps/jscoverage/jscoverage-server.c

http://github.com/onedayitwillmake/RealtimeMultiplayerNodeJs
C | 1307 lines | 1246 code | 32 blank | 29 comment | 61 complexity | a82a7f18446fdbb10937257b2ccf7c0e MD5 | raw file
   1/*
   2    jscoverage-server.c - JSCoverage server main routine
   3    Copyright (C) 2008 siliconforks.com
   4
   5    This program is free software; you can redistribute it and/or modify
   6    it under the terms of the GNU General Public License as published by
   7    the Free Software Foundation; either version 2 of the License, or
   8    (at your option) any later version.
   9
  10    This program is distributed in the hope that it will be useful,
  11    but WITHOUT ANY WARRANTY; without even the implied warranty of
  12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13    GNU General Public License for more details.
  14
  15    You should have received a copy of the GNU General Public License along
  16    with this program; if not, write to the Free Software Foundation, Inc.,
  17    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  18*/
  19
  20#include <config.h>
  21
  22#include <assert.h>
  23#include <ctype.h>
  24#include <signal.h>
  25#include <stdint.h>
  26#include <string.h>
  27
  28#include <dirent.h>
  29#ifdef HAVE_PTHREAD_H
  30#include <pthread.h>
  31#endif
  32
  33#include "encoding.h"
  34#include "global.h"
  35#include "http-server.h"
  36#include "instrument-js.h"
  37#include "resource-manager.h"
  38#include "stream.h"
  39#include "util.h"
  40
  41static const char * specified_encoding = NULL;
  42const char * jscoverage_encoding = "ISO-8859-1";
  43bool jscoverage_highlight = true;
  44
  45typedef struct SourceCache {
  46  char * url;
  47  uint16_t * characters;
  48  size_t num_characters;
  49  struct SourceCache * next;
  50} SourceCache;
  51
  52static SourceCache * source_cache = NULL;
  53
  54static const struct {
  55  const char * extension;
  56  const char * mime_type;
  57} mime_types[] = {
  58  {".gif", "image/gif"},
  59  {".jpg", "image/jpeg"},
  60  {".jpeg", "image/jpeg"},
  61  {".png", "image/png"},
  62  {".css", "text/css"},
  63  {".html", "text/html"},
  64  {".htm", "text/html"},
  65  {".js", "text/javascript"},
  66  {".txt", "text/plain"},
  67  {".xml", "application/xml"},
  68};
  69
  70static bool verbose = false;
  71static const char * report_directory = "jscoverage-report";
  72static const char * document_root = ".";
  73static bool proxy = false;
  74static const char ** no_instrument;
  75static size_t num_no_instrument = 0;
  76
  77#ifdef __MINGW32__
  78CRITICAL_SECTION javascript_mutex;
  79CRITICAL_SECTION source_cache_mutex;
  80#define LOCK EnterCriticalSection
  81#define UNLOCK LeaveCriticalSection
  82#else
  83pthread_mutex_t javascript_mutex = PTHREAD_MUTEX_INITIALIZER;
  84pthread_mutex_t source_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
  85#define LOCK pthread_mutex_lock
  86#define UNLOCK pthread_mutex_unlock
  87#endif
  88
  89static const SourceCache * find_cached_source(const char * url) {
  90  SourceCache * result = NULL;
  91  LOCK(&source_cache_mutex);
  92  for (SourceCache * p = source_cache; p != NULL; p = p->next) {
  93    if (strcmp(url, p->url) == 0) {
  94      result = p;
  95      break;
  96    }
  97  }
  98  UNLOCK(&source_cache_mutex);
  99  return result;
 100}
 101
 102static void add_cached_source(const char * url, uint16_t * characters, size_t num_characters) {
 103  SourceCache * new_source_cache = xmalloc(sizeof(SourceCache));
 104  new_source_cache->url = xstrdup(url);
 105  new_source_cache->characters = characters;
 106  new_source_cache->num_characters = num_characters;
 107  LOCK(&source_cache_mutex);
 108  new_source_cache->next = source_cache;
 109  source_cache = new_source_cache;
 110  UNLOCK(&source_cache_mutex);
 111}
 112
 113static int get(const char * url, uint16_t ** characters, size_t * num_characters) __attribute__((warn_unused_result));
 114
 115static int get(const char * url, uint16_t ** characters, size_t * num_characters) {
 116  char * host = NULL;
 117  uint16_t port;
 118  char * abs_path = NULL;
 119  char * query = NULL;
 120  HTTPConnection * connection = NULL;
 121  HTTPExchange * exchange = NULL;
 122  Stream * stream = NULL;
 123
 124  int result = URL_parse(url, &host, &port, &abs_path, &query);
 125  if (result != 0) {
 126    goto done;
 127  }
 128
 129  connection = HTTPConnection_new_client(host, port);
 130  if (connection == NULL) {
 131    result = -1;
 132    goto done;
 133  }
 134
 135  exchange = HTTPExchange_new(connection);
 136  HTTPExchange_set_request_uri(exchange, url);
 137  result = HTTPExchange_write_request_headers(exchange);
 138  if (result != 0) {
 139    goto done;
 140  }
 141
 142  result = HTTPExchange_read_response_headers(exchange);
 143  if (result != 0) {
 144    goto done;
 145  }
 146
 147  stream = Stream_new(0);
 148  result = HTTPExchange_read_entire_response_entity_body(exchange, stream);
 149  if (result != 0) {
 150    goto done;
 151  }
 152  char * encoding = HTTPMessage_get_charset(HTTPExchange_get_response_message(exchange));
 153  if (encoding == NULL) {
 154    encoding = xstrdup(jscoverage_encoding);
 155  }
 156  result = jscoverage_bytes_to_characters(encoding, stream->data, stream->length, characters, num_characters);
 157  free(encoding);
 158  if (result != 0) {
 159    goto done;
 160  }
 161
 162  result = 0;
 163
 164done:
 165  if (stream != NULL) {
 166    Stream_delete(stream);
 167  }
 168  if (exchange != NULL) {
 169    HTTPExchange_delete(exchange);
 170  }
 171  if (connection != NULL) {
 172    if (HTTPConnection_delete(connection) != 0) {
 173      HTTPServer_log_err("Warning: error closing connection after retrieving URL: %s\n", url);
 174    }
 175  }
 176  free(host);
 177  free(abs_path);
 178  free(query);
 179  return result;
 180}
 181
 182static void send_response(HTTPExchange * exchange, uint16_t status_code, const char * html) {
 183  HTTPExchange_set_status_code(exchange, status_code);
 184  if (HTTPExchange_write_response(exchange, html, strlen(html)) != 0) {
 185    HTTPServer_log_err("Warning: error writing to client\n");
 186  }
 187}
 188
 189/*
 190RFC 2396, Appendix A: we are checking for `pchar'
 191*/
 192static bool is_escaped(char c) {
 193  /* `pchar' */
 194  if (strchr(":@&=+$,", c) != NULL) {
 195    return false;
 196  }
 197
 198  if (isalnum((unsigned char) c)) {
 199    return false;
 200  }
 201
 202  /* `mark' */
 203  if (strchr("-_.!~*'()", c) != NULL) {
 204    return false;
 205  }
 206
 207  return true;
 208}
 209
 210static char * encode_uri_component(const char * s) {
 211  size_t length = 0;
 212  for (const char * p = s; *p != '\0'; p++) {
 213    if (is_escaped(*p)) {
 214      length = addst(length, 3);
 215    }
 216    else {
 217      length = addst(length, 1);
 218    }
 219  }
 220
 221  length = addst(length, 1);
 222  char * result = xmalloc(length);
 223  size_t i = 0;
 224  for (const char * p = s; *p != '\0'; p++) {
 225    if (is_escaped(*p)) {
 226      result[i] = '%';
 227      i++;
 228      snprintf(result + i, 3, "%02X", *p);
 229      i += 2;
 230    }
 231    else {
 232      result[i] = *p;
 233      i++;
 234    }
 235  }
 236  result[i] = '\0';
 237
 238  return result;
 239}
 240
 241static unsigned int hex_value(char c) {
 242  if ('0' <= c && c <= '9') {
 243    return c - '0';
 244  }
 245  else if ('A' <= c && c <= 'F') {
 246    return c - 'A' + 10;
 247  }
 248  else if ('a' <= c && c <= 'f') {
 249    return c - 'a' + 10;
 250  }
 251  else {
 252    return 0;
 253  }
 254}
 255
 256static char * decode_uri_component(const char * s) {
 257  size_t length = strlen(s);
 258  char * result = xmalloc(length + 1);
 259  char * p = result;
 260  while (*s != '\0') {
 261    if (*s == '%') {
 262      if (s[1] == '\0' || s[2] == '\0') {
 263        *p = '\0';
 264        return result;
 265      }
 266      *p = hex_value(s[1]) * 16 + hex_value(s[2]);
 267      s += 2;
 268    }
 269    else {
 270      *p = *s;
 271    }
 272    p++;
 273    s++;
 274  }
 275  *p = '\0';
 276  return result;
 277}
 278
 279static const char * get_entity(char c) {
 280  switch(c) {
 281  case '<':
 282    return "&lt;";
 283  case '>':
 284    return "&gt;";
 285  case '&':
 286    return "&amp;";
 287  case '\'':
 288    return "&apos;";
 289  case '"':
 290    return "&quot;";
 291  default:
 292    return NULL;
 293  }
 294}
 295
 296static char * encode_html(const char * s) {
 297  size_t length = 0;
 298  for (const char * p = s; *p != '\0'; p++) {
 299    const char * entity = get_entity(*p);
 300    if (entity == NULL) {
 301      length = addst(length, 1);
 302    }
 303    else {
 304      length = addst(length, strlen(entity));
 305    }
 306  }
 307
 308  length = addst(length, 1);
 309  char * result = xmalloc(length);
 310  size_t i = 0;
 311  for (const char * p = s; *p != '\0'; p++) {
 312    const char * entity = get_entity(*p);
 313    if (entity == NULL) {
 314      result[i] = *p;
 315      i++;
 316    }
 317    else {
 318      strcpy(result + i, entity);
 319      i += strlen(entity);
 320    }
 321  }
 322  result[i] = '\0';
 323
 324  return result;
 325}
 326
 327static const char * get_content_type(const char * path) {
 328  char * last_dot = strrchr(path, '.');
 329  if (last_dot == NULL) {
 330    return "application/octet-stream";
 331  }
 332  for (size_t i = 0; i < sizeof(mime_types) / sizeof(mime_types[0]); i++) {
 333    if (strcmp(last_dot, mime_types[i].extension) == 0) {
 334      return mime_types[i].mime_type;
 335    }
 336  }
 337  return "application/octet-stream";
 338}
 339
 340/**
 341Checks whether a URI is on the no-instrument list.
 342@param  uri  the HTTP "Request-URI"; must not be NULL, and must not be a zero-length string
 343@return  true if the URI is on the no-instrument list, false otherwise
 344*/
 345static bool is_no_instrument(const char * uri) {
 346  assert(*uri != '\0');
 347
 348  for (size_t i = 0; i < num_no_instrument; i++) {
 349    if (str_starts_with(uri, no_instrument[i])) {
 350      return true;
 351    }
 352
 353    /*
 354    For a local URL, accept "/foo/bar" and "foo/bar" on the no-instrument list.
 355    */
 356    if (! proxy && str_starts_with(uri + 1, no_instrument[i])) {
 357      return true;
 358    }
 359  }
 360
 361  return false;
 362}
 363
 364static bool is_javascript(HTTPExchange * exchange) {
 365  const char * header = HTTPExchange_find_response_header(exchange, HTTP_CONTENT_TYPE);
 366  if (header == NULL) {
 367    /* guess based on extension */
 368    return str_ends_with(HTTPExchange_get_request_uri(exchange), ".js");
 369  }
 370  else {
 371    char * semicolon = strchr(header, ';');
 372    char * content_type;
 373    if (semicolon == NULL) {
 374      content_type = xstrdup(header);
 375    }
 376    else {
 377      content_type = xstrndup(header, semicolon - header);
 378    }
 379    /* RFC 4329 */
 380    bool result = strcmp(content_type, "text/javascript") == 0 ||
 381                  strcmp(content_type, "text/ecmascript") == 0 ||
 382                  strcmp(content_type, "text/javascript1.0") == 0 ||
 383                  strcmp(content_type, "text/javascript1.1") == 0 ||
 384                  strcmp(content_type, "text/javascript1.2") == 0 ||
 385                  strcmp(content_type, "text/javascript1.3") == 0 ||
 386                  strcmp(content_type, "text/javascript1.4") == 0 ||
 387                  strcmp(content_type, "text/javascript1.5") == 0 ||
 388                  strcmp(content_type, "text/jscript") == 0 ||
 389                  strcmp(content_type, "text/livescript") == 0 ||
 390                  strcmp(content_type, "text/x-javascript") == 0 ||
 391                  strcmp(content_type, "text/x-ecmascript") == 0 ||
 392                  strcmp(content_type, "application/x-javascript") == 0 ||
 393                  strcmp(content_type, "application/x-ecmascript") == 0 ||
 394                  strcmp(content_type, "application/javascript") == 0 ||
 395                  strcmp(content_type, "application/ecmascript") == 0;
 396    free(content_type);
 397    return result;
 398  }
 399}
 400
 401static bool should_instrument_request(HTTPExchange * exchange) {
 402  if (! is_javascript(exchange)) {
 403    return false;
 404  }
 405
 406  if (is_no_instrument(HTTPExchange_get_request_uri(exchange))) {
 407    return false;
 408  }
 409
 410  return true;
 411}
 412
 413static int merge(Coverage * coverage, FILE * f) __attribute__((warn_unused_result));
 414
 415static int merge(Coverage * coverage, FILE * f) {
 416  Stream * stream = Stream_new(0);
 417  Stream_write_file_contents(stream, f);
 418
 419  LOCK(&javascript_mutex);
 420  int result = jscoverage_parse_json(coverage, stream->data, stream->length);
 421  UNLOCK(&javascript_mutex);
 422
 423  Stream_delete(stream);
 424  return result;
 425}
 426
 427static void write_js_quoted_string(FILE * f, char * data, size_t length) {
 428  putc('"', f);
 429  for (size_t i = 0; i < length; i++) {
 430    char c = data[i];
 431    switch (c) {
 432    case '\b':
 433      fputs("\\b", f);
 434      break;
 435    case '\f':
 436      fputs("\\f", f);
 437      break;
 438    case '\n':
 439      fputs("\\n", f);
 440      break;
 441    case '\r':
 442      fputs("\\r", f);
 443      break;
 444    case '\t':
 445      fputs("\\t", f);
 446      break;
 447    /* IE doesn't support this */
 448    /*
 449    case '\v':
 450      fputs("\\v", f);
 451      break;
 452    */
 453    case '"':
 454      fputs("\\\"", f);
 455      break;
 456    case '\\':
 457      fputs("\\\\", f);
 458      break;
 459    default:
 460      putc(c, f);
 461      break;
 462    }
 463  }
 464  putc('"', f);
 465}
 466
 467static void write_source(const char * id, const uint16_t * characters, size_t num_characters, FILE * f) {
 468  Stream * output = Stream_new(num_characters);
 469  jscoverage_write_source(id, characters, num_characters, output);
 470  fwrite(output->data, 1, output->length, f);
 471  Stream_delete(output);
 472}
 473
 474static void write_json_for_file(const FileCoverage * file_coverage, int i, void * p) {
 475  FILE * f = p;
 476
 477  if (i > 0) {
 478    putc(',', f);
 479  }
 480
 481  write_js_quoted_string(f, file_coverage->id, strlen(file_coverage->id));
 482
 483  fputs(":{\"coverage\":[", f);
 484  for (uint32_t i = 0; i < file_coverage->num_coverage_lines; i++) {
 485    if (i > 0) {
 486      putc(',', f);
 487    }
 488    int timesExecuted = file_coverage->coverage_lines[i];
 489    if (timesExecuted < 0) {
 490      fputs("null", f);
 491    }
 492    else {
 493      fprintf(f, "%d", timesExecuted);
 494    }
 495  }
 496  fputs("],\"source\":", f);
 497  if (file_coverage->source_lines == NULL) {
 498    if (proxy) {
 499      const SourceCache * cached = find_cached_source(file_coverage->id);
 500      if (cached == NULL) {
 501        uint16_t * characters;
 502        size_t num_characters;
 503        if (get(file_coverage->id, &characters, &num_characters) == 0) {
 504          write_source(file_coverage->id, characters, num_characters, f);
 505          add_cached_source(file_coverage->id, characters, num_characters);
 506        }
 507        else {
 508          fputs("[]", f);
 509          HTTPServer_log_err("Warning: cannot retrieve URL: %s\n", file_coverage->id);
 510        }
 511      }
 512      else {
 513        write_source(file_coverage->id, cached->characters, cached->num_characters, f);
 514      }
 515    }
 516    else {
 517      /* check that the path begins with / */
 518      if (file_coverage->id[0] == '/') {
 519        char * source_path = make_path(document_root, file_coverage->id + 1);
 520        FILE * source_file = fopen(source_path, "rb");
 521        free(source_path);
 522        if (source_file == NULL) {
 523          fputs("[]", f);
 524          HTTPServer_log_err("Warning: cannot open file: %s\n", file_coverage->id);
 525        }
 526        else {
 527          Stream * stream = Stream_new(0);
 528          Stream_write_file_contents(stream, source_file);
 529          fclose(source_file);
 530          uint16_t * characters;
 531          size_t num_characters;
 532          int result = jscoverage_bytes_to_characters(jscoverage_encoding, stream->data, stream->length, &characters, &num_characters);
 533          Stream_delete(stream);
 534          if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
 535            fputs("[]", f);
 536            HTTPServer_log_err("Warning: encoding %s not supported\n", jscoverage_encoding);
 537          }
 538          else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
 539            fputs("[]", f);
 540            HTTPServer_log_err("Warning: error decoding %s in file %s\n", jscoverage_encoding, file_coverage->id);
 541          }
 542          else {
 543            write_source(file_coverage->id, characters, num_characters, f);
 544            free(characters);
 545          }
 546        }
 547      }
 548      else {
 549        /* path does not begin with / */
 550        fputs("[]", f);
 551        HTTPServer_log_err("Warning: invalid source path: %s\n", file_coverage->id);
 552      }
 553    }
 554  }
 555  else {
 556    fputc('[', f);
 557    for (uint32_t i = 0; i < file_coverage->num_source_lines; i++) {
 558      if (i > 0) {
 559        fputc(',', f);
 560      }
 561      char * source_line = file_coverage->source_lines[i];
 562      write_js_quoted_string(f, source_line, strlen(source_line));
 563    }
 564    fputc(']', f);
 565  }
 566  fputc('}', f);
 567}
 568
 569static int write_json(Coverage * coverage, const char * path) __attribute__((warn_unused_result));
 570
 571static int write_json(Coverage * coverage, const char * path) {
 572  /* write the JSON */
 573  FILE * f = fopen(path, "wb");
 574  if (f == NULL) {
 575    return -1;
 576  }
 577  putc('{', f);
 578  Coverage_foreach_file(coverage, write_json_for_file, f);
 579  putc('}', f);
 580  if (fclose(f) == EOF) {
 581    return -1;
 582  }
 583  return 0;
 584}
 585
 586static void handle_jscoverage_request(HTTPExchange * exchange) {
 587  /* set the `Server' response-header (RFC 2616 14.38, 3.8) */
 588  HTTPExchange_set_response_header(exchange, HTTP_SERVER, "jscoverage-server/" VERSION);
 589
 590  const char * abs_path = HTTPExchange_get_abs_path(exchange);
 591  assert(*abs_path != '\0');
 592  if (str_starts_with(abs_path, "/jscoverage-store")) {
 593    if (strcmp(HTTPExchange_get_method(exchange), "POST") != 0) {
 594      HTTPExchange_set_response_header(exchange, HTTP_ALLOW, "POST");
 595      send_response(exchange, 405, "Method not allowed\n");
 596      return;
 597    }
 598
 599    Stream * json = Stream_new(0);
 600
 601    /* read the POST body */
 602    if (HTTPExchange_read_entire_request_entity_body(exchange, json) != 0) {
 603      Stream_delete(json);
 604      send_response(exchange, 400, "Could not read request body\n");
 605      return;
 606    }
 607
 608    Coverage * coverage = Coverage_new();
 609    LOCK(&javascript_mutex);
 610    int result = jscoverage_parse_json(coverage, json->data, json->length);
 611    UNLOCK(&javascript_mutex);
 612    Stream_delete(json);
 613
 614    if (result != 0) {
 615      Coverage_delete(coverage);
 616      send_response(exchange, 400, "Could not parse coverage data\n");
 617      return;
 618    }
 619
 620    mkdir_if_necessary(report_directory);
 621    char * current_report_directory;
 622    if (str_starts_with(abs_path, "/jscoverage-store/") && abs_path[18] != '\0') {
 623      char * dir = decode_uri_component(abs_path + 18);
 624      current_report_directory = make_path(report_directory, dir);
 625      free(dir);
 626    }
 627    else {
 628      current_report_directory = xstrdup(report_directory);
 629    }
 630    mkdir_if_necessary(current_report_directory);
 631    char * path = make_path(current_report_directory, "jscoverage.json");
 632
 633    /* check if the JSON file exists */
 634    struct stat buf;
 635    if (stat(path, &buf) == 0) {
 636      /* it exists: merge */
 637      FILE * f = fopen(path, "rb");
 638      if (f == NULL) {
 639        result = 1;
 640      }
 641      else {
 642        result = merge(coverage, f);
 643        if (fclose(f) == EOF) {
 644          result = 1;
 645        }
 646      }
 647      if (result != 0) {
 648        free(current_report_directory);
 649        free(path);
 650        Coverage_delete(coverage);
 651        send_response(exchange, 500, "Could not merge with existing coverage data\n");
 652        return;
 653      }
 654    }
 655
 656    result = write_json(coverage, path);
 657    free(path);
 658    Coverage_delete(coverage);
 659    if (result != 0) {
 660      free(current_report_directory);
 661      send_response(exchange, 500, "Could not write coverage data\n");
 662      return;
 663    }
 664
 665    /* copy other files */
 666    jscoverage_copy_resources(current_report_directory);
 667    path = make_path(current_report_directory, "jscoverage.js");
 668    free(current_report_directory);
 669    FILE * f = fopen(path, "ab");
 670    free(path);
 671    if (f == NULL) {
 672      send_response(exchange, 500, "Could not write to file: jscoverage.js\n");
 673      return;
 674    }
 675    fputs("jscoverage_isReport = true;\r\n", f);
 676    if (fclose(f) == EOF) {
 677      send_response(exchange, 500, "Could not write to file: jscoverage.js\n");
 678      return;
 679    }
 680
 681    send_response(exchange, 200, "Coverage data stored\n");
 682  }
 683  else if (str_starts_with(abs_path, "/jscoverage-shutdown")) {
 684    if (strcmp(HTTPExchange_get_method(exchange), "POST") != 0) {
 685      HTTPExchange_set_response_header(exchange, HTTP_ALLOW, "POST");
 686      send_response(exchange, 405, "Method not allowed\n");
 687      return;
 688    }
 689
 690    /* allow only from localhost */
 691    struct sockaddr_in client;
 692    if (HTTPExchange_get_peer(exchange, &client) != 0) {
 693      send_response(exchange, 500, "Cannot get client address\n");
 694      return;
 695    }
 696    if (client.sin_addr.s_addr != htonl(INADDR_LOOPBACK)) {
 697      send_response(exchange, 403, "This operation can be performed only by localhost\n");
 698      return;
 699    }
 700
 701    send_response(exchange, 200, "The server will now shut down\n");
 702    HTTPServer_shutdown();
 703  }
 704  else {
 705    const char * path = abs_path + 1;
 706    const struct Resource * resource = get_resource(path);
 707    if (resource == NULL) {
 708      send_response(exchange, 404, "Not found\n");
 709      return;
 710    }
 711    HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, get_content_type(path));
 712    if (HTTPExchange_write_response(exchange, resource->data, resource->length) != 0) {
 713      HTTPServer_log_err("Warning: error writing to client\n");
 714      return;
 715    }
 716    if (strcmp(abs_path, "/jscoverage.js") == 0) {
 717      const char * s = "jscoverage_isServer = true;\r\n";
 718      if (HTTPExchange_write_response(exchange, s, strlen(s)) != 0) {
 719        HTTPServer_log_err("Warning: error writing to client\n");
 720      }
 721    }
 722  }
 723}
 724
 725static void instrument_js(const char * id, const uint16_t * characters, size_t num_characters, Stream * output_stream) {
 726  const struct Resource * resource = get_resource("report.js");
 727  Stream_write(output_stream, resource->data, resource->length);
 728
 729  LOCK(&javascript_mutex);
 730  jscoverage_instrument_js(id, characters, num_characters, output_stream);
 731  UNLOCK(&javascript_mutex);
 732}
 733
 734static bool is_hop_by_hop_header(const char * h) {
 735  /* hop-by-hop headers (RFC 2616 13.5.1) */
 736  return strcasecmp(h, HTTP_CONNECTION) == 0 ||
 737         strcasecmp(h, "Keep-Alive") == 0 ||
 738         strcasecmp(h, HTTP_PROXY_AUTHENTICATE) == 0 ||
 739         strcasecmp(h, HTTP_PROXY_AUTHORIZATION) == 0 ||
 740         strcasecmp(h, HTTP_TE) == 0 ||
 741         strcasecmp(h, HTTP_TRAILER) == 0 ||
 742         strcasecmp(h, HTTP_TRANSFER_ENCODING) == 0 ||
 743         strcasecmp(h, HTTP_UPGRADE) == 0;
 744}
 745
 746static void add_via_header(HTTPMessage * message, const char * version) {
 747  char * value;
 748  xasprintf(&value, "%s jscoverage-server", version);
 749  HTTPMessage_add_header(message, HTTP_VIA, value);
 750  free(value);
 751}
 752
 753static int copy_http_message_body(HTTPMessage * from, HTTPMessage * to) __attribute__((warn_unused_result));
 754
 755static int copy_http_message_body(HTTPMessage * from, HTTPMessage * to) {
 756  uint8_t * buffer[8192];
 757  for (;;) {
 758    size_t bytes_read;
 759    int result = HTTPMessage_read_message_body(from, buffer, 8192, &bytes_read);
 760    if (result != 0) {
 761      return result;
 762    }
 763    if (bytes_read == 0) {
 764      return 0;
 765    }
 766    result = HTTPMessage_write(to, buffer, bytes_read);
 767    if (result != 0) {
 768      return result;
 769    }
 770  }
 771}
 772
 773static void handle_proxy_request(HTTPExchange * client_exchange) {
 774  HTTPConnection * server_connection = NULL;
 775  HTTPExchange * server_exchange = NULL;
 776
 777  const char * abs_path = HTTPExchange_get_abs_path(client_exchange);
 778  if (str_starts_with(abs_path, "/jscoverage")) {
 779    handle_jscoverage_request(client_exchange);
 780    return;
 781  }
 782
 783  const char * host = HTTPExchange_get_host(client_exchange);
 784  uint16_t port = HTTPExchange_get_port(client_exchange);
 785
 786  /* create a new connection */
 787  server_connection = HTTPConnection_new_client(host, port);
 788  if (server_connection == NULL) {
 789    send_response(client_exchange, 504, "Could not connect to server\n");
 790    goto done;
 791  }
 792
 793  /* create a new exchange */
 794  server_exchange = HTTPExchange_new(server_connection);
 795  HTTPExchange_set_method(server_exchange, HTTPExchange_get_method(client_exchange));
 796  HTTPExchange_set_request_uri(server_exchange, HTTPExchange_get_request_uri(client_exchange));
 797  for (const HTTPHeader * h = HTTPExchange_get_request_headers(client_exchange); h != NULL; h = h->next) {
 798    if (strcasecmp(h->name, HTTP_TRAILER) == 0 || strcasecmp(h->name, HTTP_TRANSFER_ENCODING) == 0) {
 799      /* do nothing: we want to keep this header */
 800    }
 801    else if (is_hop_by_hop_header(h->name) ||
 802             strcasecmp(h->name, HTTP_ACCEPT_ENCODING) == 0 ||
 803             strcasecmp(h->name, HTTP_RANGE) == 0) {
 804      continue;
 805    }
 806    HTTPExchange_add_request_header(server_exchange, h->name, h->value);
 807  }
 808  add_via_header(HTTPExchange_get_request_message(server_exchange), HTTPExchange_get_request_http_version(client_exchange));
 809
 810  /* send the request */
 811  if (HTTPExchange_write_request_headers(server_exchange) != 0) {
 812    send_response(client_exchange, 502, "Could not write to server\n");
 813    goto done;
 814  }
 815
 816  /* handle POST or PUT */
 817  if (HTTPExchange_request_has_body(client_exchange)) {
 818    HTTPMessage * client_request = HTTPExchange_get_request_message(client_exchange);
 819    HTTPMessage * server_request = HTTPExchange_get_request_message(server_exchange);
 820    if (copy_http_message_body(client_request, server_request) != 0) {
 821      send_response(client_exchange, 400, "Error copying request body from client to server\n");
 822      goto done;
 823    }
 824  }
 825
 826  if (HTTPExchange_flush_request(server_exchange) != 0) {
 827    send_response(client_exchange, 502, "Could not write to server\n");
 828    goto done;
 829  }
 830
 831  /* receive the response */
 832  if (HTTPExchange_read_response_headers(server_exchange) != 0) {
 833    send_response(client_exchange, 502, "Could not read headers from server\n");
 834    goto done;
 835  }
 836
 837  HTTPExchange_set_status_code(client_exchange, HTTPExchange_get_status_code(server_exchange));
 838
 839  if (HTTPExchange_response_has_body(server_exchange) && should_instrument_request(server_exchange)) {
 840    /* needs instrumentation */
 841    Stream * input_stream = Stream_new(0);
 842    if (HTTPExchange_read_entire_response_entity_body(server_exchange, input_stream) != 0) {
 843      Stream_delete(input_stream);
 844      send_response(client_exchange, 502, "Could not read body from server\n");
 845      goto done;
 846    }
 847
 848    const char * request_uri = HTTPExchange_get_request_uri(client_exchange);
 849    char * encoding = HTTPMessage_get_charset(HTTPExchange_get_response_message(server_exchange));
 850    if (encoding == NULL) {
 851      encoding = xstrdup(jscoverage_encoding);
 852    }
 853    uint16_t * characters;
 854    size_t num_characters;
 855    int result = jscoverage_bytes_to_characters(encoding, input_stream->data, input_stream->length, &characters, &num_characters);
 856    free(encoding);
 857    Stream_delete(input_stream);
 858    if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
 859      send_response(client_exchange, 500, "Encoding not supported\n");
 860      goto done;
 861    }
 862    else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
 863      send_response(client_exchange, 502, "Error decoding response\n");
 864      goto done;
 865    }
 866
 867    Stream * output_stream = Stream_new(0);
 868    instrument_js(request_uri, characters, num_characters, output_stream);
 869
 870    /* send the headers to the client */
 871    for (const HTTPHeader * h = HTTPExchange_get_response_headers(server_exchange); h != NULL; h = h->next) {
 872      if (is_hop_by_hop_header(h->name) || strcasecmp(h->name, HTTP_CONTENT_LENGTH) == 0) {
 873        continue;
 874      }
 875      else if (strcasecmp(h->name, HTTP_CONTENT_TYPE) == 0) {
 876        HTTPExchange_add_response_header(client_exchange, HTTP_CONTENT_TYPE, "text/javascript; charset=ISO-8859-1");
 877        continue;
 878      }
 879      HTTPExchange_add_response_header(client_exchange, h->name, h->value);
 880    }
 881    add_via_header(HTTPExchange_get_response_message(client_exchange), HTTPExchange_get_response_http_version(server_exchange));
 882    HTTPExchange_set_response_content_length(client_exchange, output_stream->length);
 883
 884    /* send the instrumented code to the client */
 885    if (HTTPExchange_write_response(client_exchange, output_stream->data, output_stream->length) != 0) {
 886      HTTPServer_log_err("Warning: error writing to client\n");
 887    }
 888
 889    /* characters go on the cache */
 890    /*
 891    free(characters);
 892    */
 893    Stream_delete(output_stream);
 894    add_cached_source(request_uri, characters, num_characters);
 895  }
 896  else {
 897    /* does not need instrumentation */
 898
 899    /* send the headers to the client */
 900    for (const HTTPHeader * h = HTTPExchange_get_response_headers(server_exchange); h != NULL; h = h->next) {
 901      if (strcasecmp(h->name, HTTP_TRAILER) == 0 || strcasecmp(h->name, HTTP_TRANSFER_ENCODING) == 0) {
 902        /* do nothing: we want to keep this header */
 903      }
 904      else if (is_hop_by_hop_header(h->name)) {
 905        continue;
 906      }
 907      HTTPExchange_add_response_header(client_exchange, h->name, h->value);
 908    }
 909    add_via_header(HTTPExchange_get_response_message(client_exchange), HTTPExchange_get_response_http_version(server_exchange));
 910
 911    if (HTTPExchange_write_response_headers(client_exchange) != 0) {
 912      HTTPServer_log_err("Warning: error writing to client\n");
 913      goto done;
 914    }
 915
 916    if (HTTPExchange_response_has_body(server_exchange)) {
 917      /* read the body from the server and send it to the client */
 918      HTTPMessage * client_response = HTTPExchange_get_response_message(client_exchange);
 919      HTTPMessage * server_response = HTTPExchange_get_response_message(server_exchange);
 920      if (copy_http_message_body(server_response, client_response) != 0) {
 921        HTTPServer_log_err("Warning: error copying response body from server to client\n");
 922        goto done;
 923      }
 924    }
 925  }
 926
 927done:
 928  if (server_exchange != NULL) {
 929    HTTPExchange_delete(server_exchange);
 930  }
 931  if (server_connection != NULL) {
 932    if (HTTPConnection_delete(server_connection) != 0) {
 933      HTTPServer_log_err("Warning: error closing connection to server\n");
 934    }
 935  }
 936}
 937
 938static void handle_local_request(HTTPExchange * exchange) {
 939  /* add the `Server' response-header (RFC 2616 14.38, 3.8) */
 940  HTTPExchange_add_response_header(exchange, HTTP_SERVER, "jscoverage-server/" VERSION);
 941
 942  char * decoded_path = NULL;
 943  char * filesystem_path = NULL;
 944
 945  const char * abs_path = HTTPExchange_get_abs_path(exchange);
 946  assert(*abs_path != '\0');
 947
 948  decoded_path = decode_uri_component(abs_path);
 949
 950  if (str_starts_with(decoded_path, "/jscoverage")) {
 951    handle_jscoverage_request(exchange);
 952    goto done;
 953  }
 954
 955  if (strstr(decoded_path, "..") != NULL) {
 956    send_response(exchange, 403, "Forbidden\n");
 957    goto done;
 958  }
 959
 960  filesystem_path = make_path(document_root, decoded_path + 1);
 961  size_t filesystem_path_length = strlen(filesystem_path);
 962  if (filesystem_path_length > 0 && filesystem_path[filesystem_path_length - 1] == '/') {
 963    /* stat on Windows doesn't work with trailing slash */
 964    filesystem_path[filesystem_path_length - 1] = '\0';
 965  }
 966
 967  struct stat buf;
 968  if (stat(filesystem_path, &buf) == -1) {
 969    send_response(exchange, 404, "Not found\n");
 970    goto done;
 971  }
 972
 973  if (S_ISDIR(buf.st_mode)) {
 974    if (abs_path[strlen(abs_path) - 1] != '/') {
 975      const char * request_uri = HTTPExchange_get_request_uri(exchange);
 976      char * uri = xmalloc(strlen(request_uri) + 2);
 977      strcpy(uri, request_uri);
 978      strcat(uri, "/");
 979      HTTPExchange_add_response_header(exchange, "Location", uri);
 980      free(uri);
 981      send_response(exchange, 301, "Moved permanently\n");
 982      goto done;
 983    }
 984
 985    DIR * d = opendir(filesystem_path);
 986    if (d == NULL) {
 987      send_response(exchange, 404, "Not found\n");
 988      goto done;
 989    }
 990
 991    struct dirent * entry;
 992    while ((entry = readdir(d)) != NULL) {
 993      char * href = encode_uri_component(entry->d_name);
 994      char * html_href = encode_html(href);
 995      char * link = encode_html(entry->d_name);
 996      char * directory_entry;
 997      xasprintf(&directory_entry, "<a href=\"%s\">%s</a><br>\n", html_href, link);
 998      if (HTTPExchange_write_response(exchange, directory_entry, strlen(directory_entry)) != 0) {
 999        HTTPServer_log_err("Warning: error writing to client\n");
1000      }
1001      free(directory_entry);
1002      free(href);
1003      free(html_href);
1004      free(link);
1005    }
1006    closedir(d);
1007  }
1008  else if (S_ISREG(buf.st_mode)) {
1009    FILE * f = fopen(filesystem_path, "rb");
1010    if (f == NULL) {
1011      send_response(exchange, 404, "Not found\n");
1012      goto done;
1013    }
1014
1015    /*
1016    When do we send a charset with Content-Type?
1017    if Content-Type is "text" or "application"
1018      if instrumented JavaScript
1019        use Content-Type: application/javascript; charset=ISO-8859-1
1020      else if --encoding is given
1021        use that encoding
1022      else
1023        send no charset
1024    else
1025      send no charset
1026    */
1027    const char * content_type = get_content_type(filesystem_path);
1028    if (strcmp(content_type, "text/javascript") == 0 && ! is_no_instrument(abs_path)) {
1029      HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, "text/javascript; charset=ISO-8859-1");
1030
1031      Stream * input_stream = Stream_new(0);
1032      Stream_write_file_contents(input_stream, f);
1033
1034      uint16_t * characters;
1035      size_t num_characters;
1036      int result = jscoverage_bytes_to_characters(jscoverage_encoding, input_stream->data, input_stream->length, &characters, &num_characters);
1037      Stream_delete(input_stream);
1038
1039      if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
1040        send_response(exchange, 500, "Encoding not supported\n");
1041        goto done;
1042      }
1043      else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
1044        send_response(exchange, 500, "Error decoding JavaScript file\n");
1045        goto done;
1046      }
1047
1048      Stream * output_stream = Stream_new(0);
1049      instrument_js(abs_path, characters, num_characters, output_stream);
1050      free(characters);
1051
1052      if (HTTPExchange_write_response(exchange, output_stream->data, output_stream->length) != 0) {
1053        HTTPServer_log_err("Warning: error writing to client\n");
1054      }
1055      Stream_delete(output_stream);
1056    }
1057    else {
1058      /* send the Content-Type with charset if necessary */
1059      if (specified_encoding != NULL && (str_starts_with(content_type, "text/") || str_starts_with(content_type, "application/"))) {
1060        char * content_type_with_charset = NULL;
1061        xasprintf(&content_type_with_charset, "%s; charset=%s", content_type, specified_encoding);
1062        HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type_with_charset);
1063        free(content_type_with_charset);
1064      }
1065      else {
1066        HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type);
1067      }
1068
1069      char buffer[8192];
1070      size_t bytes_read;
1071      while ((bytes_read = fread(buffer, 1, 8192, f)) > 0) {
1072        if (HTTPExchange_write_response(exchange, buffer, bytes_read) != 0) {
1073          HTTPServer_log_err("Warning: error writing to client\n");
1074        }
1075      }
1076    }
1077    fclose(f);
1078  }
1079  else {
1080    send_response(exchange, 404, "Not found\n");
1081    goto done;
1082  }
1083
1084done:
1085  free(filesystem_path);
1086  free(decoded_path);
1087}
1088
1089static void handler(HTTPExchange * exchange) {
1090  if (verbose) {
1091    HTTPServer_log_out("%s", HTTPExchange_get_request_line(exchange));
1092  }
1093
1094  if (proxy) {
1095    handle_proxy_request(exchange);
1096  }
1097  else {
1098    handle_local_request(exchange);
1099  }
1100}
1101
1102int main(int argc, char ** argv) {
1103  program = "jscoverage-server";
1104
1105  const char * ip_address = "127.0.0.1";
1106  const char * port = "8080";
1107  int shutdown = 0;
1108
1109  no_instrument = xnew(const char *, argc - 1);
1110
1111  for (int i = 1; i < argc; i++) {
1112    if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
1113      copy_resource_to_stream("jscoverage-server-help.txt", stdout);
1114      exit(EXIT_SUCCESS);
1115    }
1116    else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
1117      version();
1118    }
1119    else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
1120      verbose = 1;
1121    }
1122
1123    else if (strcmp(argv[i], "--report-dir") == 0) {
1124      i++;
1125      if (i == argc) {
1126        fatal_command_line("--report-dir: option requires an argument");
1127      }
1128      report_directory = argv[i];
1129    }
1130    else if (strncmp(argv[i], "--report-dir=", 13) == 0) {
1131      report_directory = argv[i] + 13;
1132    }
1133
1134    else if (strcmp(argv[i], "--document-root") == 0) {
1135      i++;
1136      if (i == argc) {
1137        fatal_command_line("--document-root: option requires an argument");
1138      }
1139      document_root = argv[i];
1140    }
1141    else if (strncmp(argv[i], "--document-root=", 16) == 0) {
1142      document_root = argv[i] + 16;
1143    }
1144
1145    else if (strcmp(argv[i], "--encoding") == 0) {
1146      i++;
1147      if (i == argc) {
1148        fatal_command_line("--encoding: option requires an argument");
1149      }
1150      jscoverage_encoding = argv[i];
1151      specified_encoding = jscoverage_encoding;
1152    }
1153    else if (strncmp(argv[i], "--encoding=", 11) == 0) {
1154      jscoverage_encoding = argv[i] + 11;
1155      specified_encoding = jscoverage_encoding;
1156    }
1157
1158    else if (strcmp(argv[i], "--ip-address") == 0) {
1159      i++;
1160      if (i == argc) {
1161        fatal_command_line("--ip-address: option requires an argument");
1162      }
1163      ip_address = argv[i];
1164    }
1165    else if (strncmp(argv[i], "--ip-address=", 13) == 0) {
1166      ip_address = argv[i] + 13;
1167    }
1168
1169    else if (strcmp(argv[i], "--js-version") == 0) {
1170      i++;
1171      if (i == argc) {
1172        fatal_command_line("--js-version: option requires an argument");
1173      }
1174      jscoverage_set_js_version(argv[i]);
1175    }
1176    else if (strncmp(argv[i], "--js-version=", 13) == 0) {
1177      jscoverage_set_js_version(argv[i] + 13);
1178    }
1179
1180    else if (strcmp(argv[i], "--no-highlight") == 0) {
1181      jscoverage_highlight = false;
1182    }
1183
1184    else if (strcmp(argv[i], "--no-instrument") == 0) {
1185      i++;
1186      if (i == argc) {
1187        fatal_command_line("--no-instrument: option requires an argument");
1188      }
1189      no_instrument[num_no_instrument] = argv[i];
1190      num_no_instrument++;
1191    }
1192    else if (strncmp(argv[i], "--no-instrument=", 16) == 0) {
1193      no_instrument[num_no_instrument] = argv[i] + 16;
1194      num_no_instrument++;
1195    }
1196
1197    else if (strcmp(argv[i], "--port") == 0) {
1198      i++;
1199      if (i == argc) {
1200        fatal_command_line("--port: option requires an argument");
1201      }
1202      port = argv[i];
1203    }
1204    else if (strncmp(argv[i], "--port=", 7) == 0) {
1205      port = argv[i] + 7;
1206    }
1207
1208    else if (strcmp(argv[i], "--proxy") == 0) {
1209      proxy = 1;
1210    }
1211
1212    else if (strcmp(argv[i], "--shutdown") == 0) {
1213      shutdown = 1;
1214    }
1215
1216    else if (strncmp(argv[i], "-", 1) == 0) {
1217      fatal_command_line("unrecognized option `%s'", argv[i]);
1218    }
1219    else {
1220      fatal_command_line("too many arguments");
1221    }
1222  }
1223
1224  /* check the port */
1225  char * end;
1226  unsigned long numeric_port = strtoul(port, &end, 10);
1227  if (*end != '\0') {
1228    fatal_command_line("--port: option must be an integer");
1229  }
1230  if (numeric_port > UINT16_MAX) {
1231    fatal_command_line("--port: option must be 16 bits");
1232  }
1233
1234  /* is this a shutdown? */
1235  if (shutdown) {
1236#ifdef __MINGW32__
1237    WSADATA data;
1238    if (WSAStartup(MAKEWORD(1, 1), &data) != 0) {
1239      fatal("could not start Winsock");
1240    }
1241#endif
1242
1243    /* INADDR_LOOPBACK */
1244    HTTPConnection * connection = HTTPConnection_new_client("127.0.0.1", numeric_port);
1245    if (connection == NULL) {
1246      fatal("could not connect to server");
1247    }
1248    HTTPExchange * exchange = HTTPExchange_new(connection);
1249    HTTPExchange_set_method(exchange, "POST");
1250    HTTPExchange_set_request_uri(exchange, "/jscoverage-shutdown");
1251    if (HTTPExchange_write_request_headers(exchange) != 0) {
1252      fatal("could not write request headers to server");
1253    }
1254    if (HTTPExchange_read_response_headers(exchange) != 0) {
1255      fatal("could not read response headers from server");
1256    }
1257    Stream * stream = Stream_new(0);
1258    if (HTTPExchange_read_entire_response_entity_body(exchange, stream) != 0) {
1259      fatal("could not read response body from server");
1260    }
1261    fwrite(stream->data, 1, stream->length, stdout);
1262    Stream_delete(stream);
1263    HTTPExchange_delete(exchange);
1264    if (HTTPConnection_delete(connection) != 0) {
1265      fatal("could not close connection with server");
1266    }
1267    exit(EXIT_SUCCESS);
1268  }
1269
1270  jscoverage_init();
1271
1272#ifndef __MINGW32__
1273  /* handle broken pipe */
1274  signal(SIGPIPE, SIG_IGN);
1275#endif
1276
1277#ifdef __MINGW32__
1278InitializeCriticalSection(&javascript_mutex);
1279InitializeCriticalSection(&source_cache_mutex);
1280#endif
1281
1282  if (verbose) {
1283    printf("Starting HTTP server on %s:%lu\n", ip_address, numeric_port);
1284    fflush(stdout);
1285  }
1286  HTTPServer_run(ip_address, (uint16_t) numeric_port, handler);
1287  if (verbose) {
1288    printf("Stopping HTTP server\n");
1289    fflush(stdout);
1290  }
1291
1292  jscoverage_cleanup();
1293
1294  free(no_instrument);
1295
1296  LOCK(&source_cache_mutex);
1297  while (source_cache != NULL) {
1298    SourceCache * p = source_cache;
1299    source_cache = source_cache->next;
1300    free(p->url);
1301    free(p->characters);
1302    free(p);
1303  }
1304  UNLOCK(&source_cache_mutex);
1305
1306  return 0;
1307}