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

Contents of /trunk/jscoverage-server.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 583 - (show annotations)
Sat Sep 11 20:20:14 2010 UTC (8 years ago) by siliconforks
File MIME type: text/plain
File size: 41723 byte(s)
Allow shutdown of server even if --ip-address was specified as a non-loopback address, as long as client has same address.

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

  ViewVC Help
Powered by ViewVC 1.1.24