/[jscoverage]/trunk/jscoverage-server.c
ViewVC logotype

Contents of /trunk/jscoverage-server.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 480 - (show annotations)
Thu Oct 8 20:53:51 2009 UTC (9 years, 2 months ago) by siliconforks
File MIME type: text/plain
File size: 41116 byte(s)
Fix --no-instrument option for proxy.

1 /*
2 jscoverage-server.c - JSCoverage server main routine
3 Copyright (C) 2008, 2009 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
41 static const char * specified_encoding = NULL;
42 const char * jscoverage_encoding = "ISO-8859-1";
43 bool jscoverage_highlight = true;
44
45 typedef struct SourceCache {
46 char * url;
47 uint16_t * characters;
48 size_t num_characters;
49 struct SourceCache * next;
50 } SourceCache;
51
52 static SourceCache * source_cache = NULL;
53
54 static 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
70 static bool verbose = false;
71 static const char * report_directory = "jscoverage-report";
72 static const char * document_root = ".";
73 static bool proxy = false;
74 static const char ** no_instrument;
75 static size_t num_no_instrument = 0;
76
77 #ifdef __MINGW32__
78 CRITICAL_SECTION javascript_mutex;
79 CRITICAL_SECTION source_cache_mutex;
80 #define LOCK EnterCriticalSection
81 #define UNLOCK LeaveCriticalSection
82 #else
83 pthread_mutex_t javascript_mutex = PTHREAD_MUTEX_INITIALIZER;
84 pthread_mutex_t source_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
85 #define LOCK pthread_mutex_lock
86 #define UNLOCK pthread_mutex_unlock
87 #endif
88
89 static 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
102 static 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
113 static int get(const char * url, uint16_t ** characters, size_t * num_characters) __attribute__((warn_unused_result));
114
115 static 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
164 done:
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
182 static 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 /*
190 RFC 2396, Appendix A: we are checking for `pchar'
191 */
192 static 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
210 static 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
241 static 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
256 static 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
279 static 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
296 static 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
327 static 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 /**
341 Checks 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 */
345 static 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
364 static 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
401 static bool should_instrument_request(HTTPExchange * exchange, const char * uri) {
402 if (! is_javascript(exchange)) {
403 return false;
404 }
405
406 if (is_no_instrument(uri)) {
407 return false;
408 }
409
410 return true;
411 }
412
413 static int merge(Coverage * coverage, FILE * f) __attribute__((warn_unused_result));
414
415 static 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
427 static 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
467 static 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
474 static 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 * decoded_path = decode_uri_component(file_coverage->id);
520 if (strstr(decoded_path, "..") != NULL) {
521 free(decoded_path);
522 fputs("[]", f);
523 HTTPServer_log_err("Warning: invalid source path: %s\n", file_coverage->id);
524 goto done;
525 }
526 char * source_path = make_path(document_root, decoded_path + 1);
527 free(decoded_path);
528 FILE * source_file = fopen(source_path, "rb");
529 free(source_path);
530 if (source_file == NULL) {
531 fputs("[]", f);
532 HTTPServer_log_err("Warning: cannot open file: %s\n", file_coverage->id);
533 }
534 else {
535 Stream * stream = Stream_new(0);
536 Stream_write_file_contents(stream, source_file);
537 fclose(source_file);
538 uint16_t * characters;
539 size_t num_characters;
540 int result = jscoverage_bytes_to_characters(jscoverage_encoding, stream->data, stream->length, &characters, &num_characters);
541 Stream_delete(stream);
542 if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
543 fputs("[]", f);
544 HTTPServer_log_err("Warning: encoding %s not supported\n", jscoverage_encoding);
545 }
546 else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
547 fputs("[]", f);
548 HTTPServer_log_err("Warning: error decoding %s in file %s\n", jscoverage_encoding, file_coverage->id);
549 }
550 else {
551 write_source(file_coverage->id, characters, num_characters, f);
552 free(characters);
553 }
554 }
555 }
556 else {
557 /* path does not begin with / */
558 fputs("[]", f);
559 HTTPServer_log_err("Warning: invalid source path: %s\n", file_coverage->id);
560 }
561 }
562 }
563 else {
564 fputc('[', f);
565 for (uint32_t i = 0; i < file_coverage->num_source_lines; i++) {
566 if (i > 0) {
567 fputc(',', f);
568 }
569 char * source_line = file_coverage->source_lines[i];
570 write_js_quoted_string(f, source_line, strlen(source_line));
571 }
572 fputc(']', f);
573 }
574 done:
575 fputc('}', f);
576 }
577
578 static int write_json(Coverage * coverage, const char * path) __attribute__((warn_unused_result));
579
580 static int write_json(Coverage * coverage, const char * path) {
581 /* write the JSON */
582 FILE * f = fopen(path, "wb");
583 if (f == NULL) {
584 return -1;
585 }
586 putc('{', f);
587 Coverage_foreach_file(coverage, write_json_for_file, f);
588 putc('}', f);
589 if (fclose(f) == EOF) {
590 return -1;
591 }
592 return 0;
593 }
594
595 static void handle_jscoverage_request(HTTPExchange * exchange) {
596 /* set the `Server' response-header (RFC 2616 14.38, 3.8) */
597 HTTPExchange_set_response_header(exchange, HTTP_SERVER, "jscoverage-server/" VERSION);
598
599 const char * abs_path = HTTPExchange_get_abs_path(exchange);
600 assert(*abs_path != '\0');
601 if (str_starts_with(abs_path, "/jscoverage-store")) {
602 if (strcmp(HTTPExchange_get_method(exchange), "POST") != 0) {
603 HTTPExchange_set_response_header(exchange, HTTP_ALLOW, "POST");
604 send_response(exchange, 405, "Method not allowed\n");
605 return;
606 }
607
608 Stream * json = Stream_new(0);
609
610 /* read the POST body */
611 if (HTTPExchange_read_entire_request_entity_body(exchange, json) != 0) {
612 Stream_delete(json);
613 send_response(exchange, 400, "Could not read request body\n");
614 return;
615 }
616
617 Coverage * coverage = Coverage_new();
618 LOCK(&javascript_mutex);
619 int result = jscoverage_parse_json(coverage, json->data, json->length);
620 UNLOCK(&javascript_mutex);
621 Stream_delete(json);
622
623 if (result != 0) {
624 Coverage_delete(coverage);
625 send_response(exchange, 400, "Could not parse coverage data\n");
626 return;
627 }
628
629 mkdir_if_necessary(report_directory);
630 char * current_report_directory;
631 if (str_starts_with(abs_path, "/jscoverage-store/") && abs_path[18] != '\0') {
632 char * dir = decode_uri_component(abs_path + 18);
633 current_report_directory = make_path(report_directory, dir);
634 free(dir);
635 }
636 else {
637 current_report_directory = xstrdup(report_directory);
638 }
639 mkdir_if_necessary(current_report_directory);
640 char * path = make_path(current_report_directory, "jscoverage.json");
641
642 /* check if the JSON file exists */
643 struct stat buf;
644 if (stat(path, &buf) == 0) {
645 /* it exists: merge */
646 FILE * f = fopen(path, "rb");
647 if (f == NULL) {
648 result = 1;
649 }
650 else {
651 result = merge(coverage, f);
652 if (fclose(f) == EOF) {
653 result = 1;
654 }
655 }
656 if (result != 0) {
657 free(current_report_directory);
658 free(path);
659 Coverage_delete(coverage);
660 send_response(exchange, 500, "Could not merge with existing coverage data\n");
661 return;
662 }
663 }
664
665 result = write_json(coverage, path);
666 free(path);
667 Coverage_delete(coverage);
668 if (result != 0) {
669 free(current_report_directory);
670 send_response(exchange, 500, "Could not write coverage data\n");
671 return;
672 }
673
674 /* copy other files */
675 jscoverage_copy_resources(current_report_directory);
676 path = make_path(current_report_directory, "jscoverage.js");
677 free(current_report_directory);
678 FILE * f = fopen(path, "ab");
679 free(path);
680 if (f == NULL) {
681 send_response(exchange, 500, "Could not write to file: jscoverage.js\n");
682 return;
683 }
684 fputs("jscoverage_isReport = true;\r\n", f);
685 if (fclose(f) == EOF) {
686 send_response(exchange, 500, "Could not write to file: jscoverage.js\n");
687 return;
688 }
689
690 send_response(exchange, 200, "Coverage data stored\n");
691 }
692 else if (str_starts_with(abs_path, "/jscoverage-shutdown")) {
693 if (strcmp(HTTPExchange_get_method(exchange), "POST") != 0) {
694 HTTPExchange_set_response_header(exchange, HTTP_ALLOW, "POST");
695 send_response(exchange, 405, "Method not allowed\n");
696 return;
697 }
698
699 /* allow only from localhost */
700 struct sockaddr_in client;
701 if (HTTPExchange_get_peer(exchange, &client) != 0) {
702 send_response(exchange, 500, "Cannot get client address\n");
703 return;
704 }
705 if (client.sin_addr.s_addr != htonl(INADDR_LOOPBACK)) {
706 send_response(exchange, 403, "This operation can be performed only by localhost\n");
707 return;
708 }
709
710 send_response(exchange, 200, "The server will now shut down\n");
711 HTTPServer_shutdown();
712 }
713 else {
714 const char * path = abs_path + 1;
715 const struct Resource * resource = get_resource(path);
716 if (resource == NULL) {
717 send_response(exchange, 404, "Not found\n");
718 return;
719 }
720 HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, get_content_type(path));
721 if (HTTPExchange_write_response(exchange, resource->data, resource->length) != 0) {
722 HTTPServer_log_err("Warning: error writing to client\n");
723 return;
724 }
725 if (strcmp(abs_path, "/jscoverage.js") == 0) {
726 const char * s = "jscoverage_isServer = true;\r\n";
727 if (HTTPExchange_write_response(exchange, s, strlen(s)) != 0) {
728 HTTPServer_log_err("Warning: error writing to client\n");
729 }
730 }
731 }
732 }
733
734 static void instrument_js(const char * id, const uint16_t * characters, size_t num_characters, Stream * output_stream) {
735 const struct Resource * resource = get_resource("report.js");
736 Stream_write(output_stream, resource->data, resource->length);
737
738 LOCK(&javascript_mutex);
739 jscoverage_instrument_js(id, characters, num_characters, output_stream);
740 UNLOCK(&javascript_mutex);
741 }
742
743 static bool is_hop_by_hop_header(const char * h) {
744 /* hop-by-hop headers (RFC 2616 13.5.1) */
745 return strcasecmp(h, HTTP_CONNECTION) == 0 ||
746 strcasecmp(h, "Keep-Alive") == 0 ||
747 strcasecmp(h, HTTP_PROXY_AUTHENTICATE) == 0 ||
748 strcasecmp(h, HTTP_PROXY_AUTHORIZATION) == 0 ||
749 strcasecmp(h, HTTP_TE) == 0 ||
750 strcasecmp(h, HTTP_TRAILER) == 0 ||
751 strcasecmp(h, HTTP_TRANSFER_ENCODING) == 0 ||
752 strcasecmp(h, HTTP_UPGRADE) == 0;
753 }
754
755 static void add_via_header(HTTPMessage * message, const char * version) {
756 char * value;
757 xasprintf(&value, "%s jscoverage-server", version);
758 HTTPMessage_add_header(message, HTTP_VIA, value);
759 free(value);
760 }
761
762 static int copy_http_message_body(HTTPMessage * from, HTTPMessage * to) __attribute__((warn_unused_result));
763
764 static int copy_http_message_body(HTTPMessage * from, HTTPMessage * to) {
765 uint8_t * buffer[8192];
766 for (;;) {
767 size_t bytes_read;
768 int result = HTTPMessage_read_message_body(from, buffer, 8192, &bytes_read);
769 if (result != 0) {
770 return result;
771 }
772 if (bytes_read == 0) {
773 return 0;
774 }
775 result = HTTPMessage_write(to, buffer, bytes_read);
776 if (result != 0) {
777 return result;
778 }
779 }
780 }
781
782 static void handle_proxy_request(HTTPExchange * client_exchange) {
783 HTTPConnection * server_connection = NULL;
784 HTTPExchange * server_exchange = NULL;
785
786 const char * abs_path = HTTPExchange_get_abs_path(client_exchange);
787 if (str_starts_with(abs_path, "/jscoverage")) {
788 handle_jscoverage_request(client_exchange);
789 return;
790 }
791
792 const char * host = HTTPExchange_get_host(client_exchange);
793 uint16_t port = HTTPExchange_get_port(client_exchange);
794
795 /* create a new connection */
796 server_connection = HTTPConnection_new_client(host, port);
797 if (server_connection == NULL) {
798 send_response(client_exchange, 504, "Could not connect to server\n");
799 goto done;
800 }
801
802 /* create a new exchange */
803 server_exchange = HTTPExchange_new(server_connection);
804
805 HTTPExchange_set_method(server_exchange, HTTPExchange_get_method(client_exchange));
806
807 /* don't send full URI to origin server - just send abs_path and query */
808 const char * query = HTTPExchange_get_query(client_exchange);
809 char * origin_server_request_uri;
810 if (query == NULL) {
811 origin_server_request_uri = xstrdup(abs_path);
812 }
813 else {
814 size_t abs_path_length = strlen(abs_path);
815 size_t query_length = strlen(query);
816 size_t origin_server_request_uri_length = addst(abs_path_length, query_length);
817 origin_server_request_uri_length = addst(origin_server_request_uri_length, 2);
818 origin_server_request_uri = xmalloc(origin_server_request_uri_length);
819 strcpy(origin_server_request_uri, abs_path);
820 origin_server_request_uri[abs_path_length] = '?';
821 strcpy(origin_server_request_uri + abs_path_length + 1, query);
822 }
823 HTTPExchange_set_request_uri(server_exchange, origin_server_request_uri);
824 free(origin_server_request_uri);
825
826 for (const HTTPHeader * h = HTTPExchange_get_request_headers(client_exchange); h != NULL; h = h->next) {
827 if (strcasecmp(h->name, HTTP_TRAILER) == 0 || strcasecmp(h->name, HTTP_TRANSFER_ENCODING) == 0) {
828 /* do nothing: we want to keep this header */
829 }
830 else if (is_hop_by_hop_header(h->name) ||
831 strcasecmp(h->name, HTTP_ACCEPT_ENCODING) == 0 ||
832 strcasecmp(h->name, HTTP_RANGE) == 0) {
833 continue;
834 }
835 HTTPExchange_add_request_header(server_exchange, h->name, h->value);
836 }
837 add_via_header(HTTPExchange_get_request_message(server_exchange), HTTPExchange_get_request_http_version(client_exchange));
838
839 /* send the request */
840 if (HTTPExchange_write_request_headers(server_exchange) != 0) {
841 send_response(client_exchange, 502, "Could not write to server\n");
842 goto done;
843 }
844
845 /* handle POST or PUT */
846 if (HTTPExchange_request_has_body(client_exchange)) {
847 HTTPMessage * client_request = HTTPExchange_get_request_message(client_exchange);
848 HTTPMessage * server_request = HTTPExchange_get_request_message(server_exchange);
849 if (copy_http_message_body(client_request, server_request) != 0) {
850 send_response(client_exchange, 400, "Error copying request body from client to server\n");
851 goto done;
852 }
853 }
854
855 if (HTTPExchange_flush_request(server_exchange) != 0) {
856 send_response(client_exchange, 502, "Could not write to server\n");
857 goto done;
858 }
859
860 /* receive the response */
861 if (HTTPExchange_read_response_headers(server_exchange) != 0) {
862 send_response(client_exchange, 502, "Could not read headers from server\n");
863 goto done;
864 }
865
866 HTTPExchange_set_status_code(client_exchange, HTTPExchange_get_status_code(server_exchange));
867
868 if (HTTPExchange_response_has_body(server_exchange) && should_instrument_request(server_exchange, HTTPExchange_get_request_uri(client_exchange))) {
869 /* needs instrumentation */
870 Stream * input_stream = Stream_new(0);
871 if (HTTPExchange_read_entire_response_entity_body(server_exchange, input_stream) != 0) {
872 Stream_delete(input_stream);
873 send_response(client_exchange, 502, "Could not read body from server\n");
874 goto done;
875 }
876
877 const char * request_uri = HTTPExchange_get_request_uri(client_exchange);
878 char * encoding = HTTPMessage_get_charset(HTTPExchange_get_response_message(server_exchange));
879 if (encoding == NULL) {
880 encoding = xstrdup(jscoverage_encoding);
881 }
882 uint16_t * characters;
883 size_t num_characters;
884 int result = jscoverage_bytes_to_characters(encoding, input_stream->data, input_stream->length, &characters, &num_characters);
885 free(encoding);
886 Stream_delete(input_stream);
887 if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
888 send_response(client_exchange, 500, "Encoding not supported\n");
889 goto done;
890 }
891 else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
892 send_response(client_exchange, 502, "Error decoding response\n");
893 goto done;
894 }
895
896 Stream * output_stream = Stream_new(0);
897 instrument_js(request_uri, characters, num_characters, output_stream);
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 (is_hop_by_hop_header(h->name) || strcasecmp(h->name, HTTP_CONTENT_LENGTH) == 0) {
902 continue;
903 }
904 else if (strcasecmp(h->name, HTTP_CONTENT_TYPE) == 0) {
905 HTTPExchange_add_response_header(client_exchange, HTTP_CONTENT_TYPE, "text/javascript; charset=ISO-8859-1");
906 continue;
907 }
908 HTTPExchange_add_response_header(client_exchange, h->name, h->value);
909 }
910 add_via_header(HTTPExchange_get_response_message(client_exchange), HTTPExchange_get_response_http_version(server_exchange));
911 HTTPExchange_set_response_content_length(client_exchange, output_stream->length);
912
913 /* send the instrumented code to the client */
914 if (HTTPExchange_write_response(client_exchange, output_stream->data, output_stream->length) != 0) {
915 HTTPServer_log_err("Warning: error writing to client\n");
916 }
917
918 /* characters go on the cache */
919 /*
920 free(characters);
921 */
922 Stream_delete(output_stream);
923 add_cached_source(request_uri, characters, num_characters);
924 }
925 else {
926 /* does not need instrumentation */
927
928 /* send the headers to the client */
929 for (const HTTPHeader * h = HTTPExchange_get_response_headers(server_exchange); h != NULL; h = h->next) {
930 if (strcasecmp(h->name, HTTP_TRAILER) == 0 || strcasecmp(h->name, HTTP_TRANSFER_ENCODING) == 0) {
931 /* do nothing: we want to keep this header */
932 }
933 else if (is_hop_by_hop_header(h->name)) {
934 continue;
935 }
936 HTTPExchange_add_response_header(client_exchange, h->name, h->value);
937 }
938 add_via_header(HTTPExchange_get_response_message(client_exchange), HTTPExchange_get_response_http_version(server_exchange));
939
940 if (HTTPExchange_write_response_headers(client_exchange) != 0) {
941 HTTPServer_log_err("Warning: error writing to client\n");
942 goto done;
943 }
944
945 if (HTTPExchange_response_has_body(server_exchange)) {
946 /* read the body from the server and send it to the client */
947 HTTPMessage * client_response = HTTPExchange_get_response_message(client_exchange);
948 HTTPMessage * server_response = HTTPExchange_get_response_message(server_exchange);
949 if (copy_http_message_body(server_response, client_response) != 0) {
950 HTTPServer_log_err("Warning: error copying response body from server to client\n");
951 goto done;
952 }
953 }
954 }
955
956 done:
957 if (server_exchange != NULL) {
958 HTTPExchange_delete(server_exchange);
959 }
960 if (server_connection != NULL) {
961 if (HTTPConnection_delete(server_connection) != 0) {
962 HTTPServer_log_err("Warning: error closing connection to server\n");
963 }
964 }
965 }
966
967 static void handle_local_request(HTTPExchange * exchange) {
968 /* add the `Server' response-header (RFC 2616 14.38, 3.8) */
969 HTTPExchange_add_response_header(exchange, HTTP_SERVER, "jscoverage-server/" VERSION);
970
971 char * decoded_path = NULL;
972 char * filesystem_path = NULL;
973
974 const char * abs_path = HTTPExchange_get_abs_path(exchange);
975 assert(*abs_path != '\0');
976
977 decoded_path = decode_uri_component(abs_path);
978
979 if (str_starts_with(decoded_path, "/jscoverage")) {
980 handle_jscoverage_request(exchange);
981 goto done;
982 }
983
984 if (strstr(decoded_path, "..") != NULL) {
985 send_response(exchange, 403, "Forbidden\n");
986 goto done;
987 }
988
989 filesystem_path = make_path(document_root, decoded_path + 1);
990 size_t filesystem_path_length = strlen(filesystem_path);
991 if (filesystem_path_length > 0 && filesystem_path[filesystem_path_length - 1] == '/') {
992 /* stat on Windows doesn't work with trailing slash */
993 filesystem_path[filesystem_path_length - 1] = '\0';
994 }
995
996 struct stat buf;
997 if (stat(filesystem_path, &buf) == -1) {
998 send_response(exchange, 404, "Not found\n");
999 goto done;
1000 }
1001
1002 if (S_ISDIR(buf.st_mode)) {
1003 if (abs_path[strlen(abs_path) - 1] != '/') {
1004 const char * request_uri = HTTPExchange_get_request_uri(exchange);
1005 char * uri = xmalloc(strlen(request_uri) + 2);
1006 strcpy(uri, request_uri);
1007 strcat(uri, "/");
1008 HTTPExchange_add_response_header(exchange, "Location", uri);
1009 free(uri);
1010 send_response(exchange, 301, "Moved permanently\n");
1011 goto done;
1012 }
1013
1014 DIR * d = opendir(filesystem_path);
1015 if (d == NULL) {
1016 send_response(exchange, 404, "Not found\n");
1017 goto done;
1018 }
1019
1020 struct dirent * entry;
1021 while ((entry = readdir(d)) != NULL) {
1022 char * href = encode_uri_component(entry->d_name);
1023 char * html_href = encode_html(href);
1024 char * link = encode_html(entry->d_name);
1025 char * directory_entry;
1026 xasprintf(&directory_entry, "<a href=\"%s\">%s</a><br>\n", html_href, link);
1027 if (HTTPExchange_write_response(exchange, directory_entry, strlen(directory_entry)) != 0) {
1028 HTTPServer_log_err("Warning: error writing to client\n");
1029 }
1030 free(directory_entry);
1031 free(href);
1032 free(html_href);
1033 free(link);
1034 }
1035 closedir(d);
1036 }
1037 else if (S_ISREG(buf.st_mode)) {
1038 FILE * f = fopen(filesystem_path, "rb");
1039 if (f == NULL) {
1040 send_response(exchange, 404, "Not found\n");
1041 goto done;
1042 }
1043
1044 /*
1045 When do we send a charset with Content-Type?
1046 if Content-Type is "text" or "application"
1047 if instrumented JavaScript
1048 use Content-Type: application/javascript; charset=ISO-8859-1
1049 else if --encoding is given
1050 use that encoding
1051 else
1052 send no charset
1053 else
1054 send no charset
1055 */
1056 const char * content_type = get_content_type(filesystem_path);
1057 if (strcmp(content_type, "text/javascript") == 0 && ! is_no_instrument(abs_path)) {
1058 HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, "text/javascript; charset=ISO-8859-1");
1059
1060 Stream * input_stream = Stream_new(0);
1061 Stream_write_file_contents(input_stream, f);
1062
1063 uint16_t * characters;
1064 size_t num_characters;
1065 int result = jscoverage_bytes_to_characters(jscoverage_encoding, input_stream->data, input_stream->length, &characters, &num_characters);
1066 Stream_delete(input_stream);
1067
1068 if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
1069 send_response(exchange, 500, "Encoding not supported\n");
1070 goto done;
1071 }
1072 else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
1073 send_response(exchange, 500, "Error decoding JavaScript file\n");
1074 goto done;
1075 }
1076
1077 Stream * output_stream = Stream_new(0);
1078 instrument_js(abs_path, characters, num_characters, output_stream);
1079 free(characters);
1080
1081 if (HTTPExchange_write_response(exchange, output_stream->data, output_stream->length) != 0) {
1082 HTTPServer_log_err("Warning: error writing to client\n");
1083 }
1084 Stream_delete(output_stream);
1085 }
1086 else {
1087 /* send the Content-Type with charset if necessary */
1088 if (specified_encoding != NULL && (str_starts_with(content_type, "text/") || str_starts_with(content_type, "application/"))) {
1089 char * content_type_with_charset = NULL;
1090 xasprintf(&content_type_with_charset, "%s; charset=%s", content_type, specified_encoding);
1091 HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type_with_charset);
1092 free(content_type_with_charset);
1093 }
1094 else {
1095 HTTPExchange_set_response_header(exchange, HTTP_CONTENT_TYPE, content_type);
1096 }
1097
1098 char buffer[8192];
1099 size_t bytes_read;
1100 while ((bytes_read = fread(buffer, 1, 8192, f)) > 0) {
1101 if (HTTPExchange_write_response(exchange, buffer, bytes_read) != 0) {
1102 HTTPServer_log_err("Warning: error writing to client\n");
1103 }
1104 }
1105 }
1106 fclose(f);
1107 }
1108 else {
1109 send_response(exchange, 404, "Not found\n");
1110 goto done;
1111 }
1112
1113 done:
1114 free(filesystem_path);
1115 free(decoded_path);
1116 }
1117
1118 static void handler(HTTPExchange * exchange) {
1119 if (verbose) {
1120 HTTPServer_log_out("%s", HTTPExchange_get_request_line(exchange));
1121 }
1122
1123 if (proxy) {
1124 handle_proxy_request(exchange);
1125 }
1126 else {
1127 handle_local_request(exchange);
1128 }
1129 }
1130
1131 int main(int argc, char ** argv) {
1132 program = "jscoverage-server";
1133
1134 const char * ip_address = "127.0.0.1";
1135 const char * port = "8080";
1136 int shutdown = 0;
1137
1138 no_instrument = xnew(const char *, argc - 1);
1139
1140 for (int i = 1; i < argc; i++) {
1141 if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
1142 copy_resource_to_stream("jscoverage-server-help.txt", stdout);
1143 exit(EXIT_SUCCESS);
1144 }
1145 else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
1146 version();
1147 }
1148 else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
1149 verbose = 1;
1150 }
1151
1152 else if (strcmp(argv[i], "--report-dir") == 0) {
1153 i++;
1154 if (i == argc) {
1155 fatal_command_line("--report-dir: option requires an argument");
1156 }
1157 report_directory = argv[i];
1158 }
1159 else if (strncmp(argv[i], "--report-dir=", 13) == 0) {
1160 report_directory = argv[i] + 13;
1161 }
1162
1163 else if (strcmp(argv[i], "--document-root") == 0) {
1164 i++;
1165 if (i == argc) {
1166 fatal_command_line("--document-root: option requires an argument");
1167 }
1168 document_root = argv[i];
1169 }
1170 else if (strncmp(argv[i], "--document-root=", 16) == 0) {
1171 document_root = argv[i] + 16;
1172 }
1173
1174 else if (strcmp(argv[i], "--encoding") == 0) {
1175 i++;
1176 if (i == argc) {
1177 fatal_command_line("--encoding: option requires an argument");
1178 }
1179 jscoverage_encoding = argv[i];
1180 specified_encoding = jscoverage_encoding;
1181 }
1182 else if (strncmp(argv[i], "--encoding=", 11) == 0) {
1183 jscoverage_encoding = argv[i] + 11;
1184 specified_encoding = jscoverage_encoding;
1185 }
1186
1187 else if (strcmp(argv[i], "--ip-address") == 0) {
1188 i++;
1189 if (i == argc) {
1190 fatal_command_line("--ip-address: option requires an argument");
1191 }
1192 ip_address = argv[i];
1193 }
1194 else if (strncmp(argv[i], "--ip-address=", 13) == 0) {
1195 ip_address = argv[i] + 13;
1196 }
1197
1198 else if (strcmp(argv[i], "--js-version") == 0) {
1199 i++;
1200 if (i == argc) {
1201 fatal_command_line("--js-version: option requires an argument");
1202 }
1203 jscoverage_set_js_version(argv[i]);
1204 }
1205 else if (strncmp(argv[i], "--js-version=", 13) == 0) {
1206 jscoverage_set_js_version(argv[i] + 13);
1207 }
1208
1209 else if (strcmp(argv[i], "--no-highlight") == 0) {
1210 jscoverage_highlight = false;
1211 }
1212
1213 else if (strcmp(argv[i], "--no-instrument") == 0) {
1214 i++;
1215 if (i == argc) {
1216 fatal_command_line("--no-instrument: option requires an argument");
1217 }
1218 no_instrument[num_no_instrument] = argv[i];
1219 num_no_instrument++;
1220 }
1221 else if (strncmp(argv[i], "--no-instrument=", 16) == 0) {
1222 no_instrument[num_no_instrument] = argv[i] + 16;
1223 num_no_instrument++;
1224 }
1225
1226 else if (strcmp(argv[i], "--port") == 0) {
1227 i++;
1228 if (i == argc) {
1229 fatal_command_line("--port: option requires an argument");
1230 }
1231 port = argv[i];
1232 }
1233 else if (strncmp(argv[i], "--port=", 7) == 0) {
1234 port = argv[i] + 7;
1235 }
1236
1237 else if (strcmp(argv[i], "--proxy") == 0) {
1238 proxy = 1;
1239 }
1240
1241 else if (strcmp(argv[i], "--shutdown") == 0) {
1242 shutdown = 1;
1243 }
1244
1245 else if (strncmp(argv[i], "-", 1) == 0) {
1246 fatal_command_line("unrecognized option `%s'", argv[i]);
1247 }
1248 else {
1249 fatal_command_line("too many arguments");
1250 }
1251 }
1252
1253 /* check the port */
1254 char * end;
1255 unsigned long numeric_port = strtoul(port, &end, 10);
1256 if (*end != '\0') {
1257 fatal_command_line("--port: option must be an integer");
1258 }
1259 if (numeric_port > UINT16_MAX) {
1260 fatal_command_line("--port: option must be 16 bits");
1261 }
1262
1263 /* check the document root exists and is a directory */
1264 struct stat buf;
1265 xstat(document_root, &buf);
1266 if (! S_ISDIR(buf.st_mode)) {
1267 fatal_command_line("--document-root: option must be a directory");
1268 }
1269
1270 /* is this a shutdown? */
1271 if (shutdown) {
1272 #ifdef __MINGW32__
1273 WSADATA data;
1274 if (WSAStartup(MAKEWORD(1, 1), &data) != 0) {
1275 fatal("could not start Winsock");
1276 }
1277 #endif
1278
1279 /* INADDR_LOOPBACK */
1280 HTTPConnection * connection = HTTPConnection_new_client("127.0.0.1", numeric_port);
1281 if (connection == NULL) {
1282 fatal("could not connect to server");
1283 }
1284 HTTPExchange * exchange = HTTPExchange_new(connection);
1285 HTTPExchange_set_method(exchange, "POST");
1286 HTTPExchange_set_request_uri(exchange, "/jscoverage-shutdown");
1287 if (HTTPExchange_write_request_headers(exchange) != 0) {
1288 fatal("could not write request headers to server");
1289 }
1290 if (HTTPExchange_read_response_headers(exchange) != 0) {
1291 fatal("could not read response headers from server");
1292 }
1293 Stream * stream = Stream_new(0);
1294 if (HTTPExchange_read_entire_response_entity_body(exchange, stream) != 0) {
1295 fatal("could not read response body from server");
1296 }
1297 fwrite(stream->data, 1, stream->length, stdout);
1298 Stream_delete(stream);
1299 HTTPExchange_delete(exchange);
1300 if (HTTPConnection_delete(connection) != 0) {
1301 fatal("could not close connection with server");
1302 }
1303 exit(EXIT_SUCCESS);
1304 }
1305
1306 jscoverage_init();
1307
1308 #ifndef __MINGW32__
1309 /* handle broken pipe */
1310 signal(SIGPIPE, SIG_IGN);
1311 #endif
1312
1313 #ifdef __MINGW32__
1314 InitializeCriticalSection(&javascript_mutex);
1315 InitializeCriticalSection(&source_cache_mutex);
1316 #endif
1317
1318 if (verbose) {
1319 printf("Starting HTTP server on %s:%lu\n", ip_address, numeric_port);
1320 fflush(stdout);
1321 }
1322 HTTPServer_run(ip_address, (uint16_t) numeric_port, handler);
1323 if (verbose) {
1324 printf("Stopping HTTP server\n");
1325 fflush(stdout);
1326 }
1327
1328 jscoverage_cleanup();
1329
1330 free(no_instrument);
1331
1332 LOCK(&source_cache_mutex);
1333 while (source_cache != NULL) {
1334 SourceCache * p = source_cache;
1335 source_cache = source_cache->next;
1336 free(p->url);
1337 free(p->characters);
1338 free(p);
1339 }
1340 UNLOCK(&source_cache_mutex);
1341
1342 return 0;
1343 }

  ViewVC Help
Powered by ViewVC 1.1.24