agoo 2.3.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of agoo might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/ext/agoo/con.c +56 -3
- data/ext/agoo/con.h +1 -0
- data/ext/agoo/method.h +1 -0
- data/ext/agoo/request.c +47 -2
- data/ext/agoo/request.h +1 -1
- data/ext/agoo/server.c +17 -3
- data/ext/agoo/upgraded.c +1 -0
- data/ext/agoo/upgraded.h +1 -0
- data/lib/agoo/version.rb +1 -1
- data/test/hijack_test.rb +76 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b76a004b8311f4a6966175ff830f0f38babc63efddde69c39a7b4ccc3d90b4a
|
4
|
+
data.tar.gz: 3356bf438fb1ebdcf6aa9c2d22c3536e5f5fe7c23cd909962effc965d269fc7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 200da7c5f41b3822cdcb68a0ddd79e7c66a976e33da33245eafa9a66907166be3144874c889c98ac6891a0f2c46746019fcdf100e18df8f6d4a3884355dc7986
|
7
|
+
data.tar.gz: 1753cd691ec0bd0443a5a2d6e7af894ca56d705a9e6c3ebe645f4be7ae2156784bc94b4d3dcf6ecb17a5a4003831991120ebb19cade82435fb0599cf71d605e0
|
data/CHANGELOG.md
CHANGED
data/ext/agoo/con.c
CHANGED
@@ -152,6 +152,25 @@ page_response(Con c, Page p, char *hend) {
|
|
152
152
|
return false;
|
153
153
|
}
|
154
154
|
|
155
|
+
static void
|
156
|
+
push_error(Upgraded up, const char *msg, int mlen) {
|
157
|
+
if (NULL != up && Qnil != up->handler && up->on_error) {
|
158
|
+
Req req = request_create(mlen);
|
159
|
+
|
160
|
+
if (NULL == req) {
|
161
|
+
return;
|
162
|
+
}
|
163
|
+
memcpy(req->msg, msg, mlen);
|
164
|
+
req->msg[mlen] = '\0';
|
165
|
+
req->up = up;
|
166
|
+
req->method = ON_ERROR;
|
167
|
+
req->handler_type = PUSH_HOOK;
|
168
|
+
req->handler = up->handler;
|
169
|
+
upgraded_ref(up);
|
170
|
+
queue_push(&the_server.eval_queue, (void*)req);
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
155
174
|
// Returns:
|
156
175
|
// 0 - when header has not been read
|
157
176
|
// message length - when length can be determined
|
@@ -487,6 +506,9 @@ con_ws_read(Con c) {
|
|
487
506
|
if (0 == cnt) {
|
488
507
|
log_cat(&warn_cat, "Nothing to read. Client closed socket on connection %llu.", (unsigned long long)c->id);
|
489
508
|
} else {
|
509
|
+
char msg[1024];
|
510
|
+
int len = snprintf(msg, sizeof(msg) - 1, "Failed to read WebSocket message. %s.", strerror(errno));
|
511
|
+
push_error(c->up, msg, len);
|
490
512
|
log_cat(&warn_cat, "Failed to read WebSocket message. %s.", strerror(errno));
|
491
513
|
}
|
492
514
|
}
|
@@ -525,10 +547,16 @@ con_ws_read(Con c) {
|
|
525
547
|
}
|
526
548
|
break;
|
527
549
|
case WS_OP_CONT:
|
528
|
-
default:
|
550
|
+
default: {
|
551
|
+
char msg[1024];
|
552
|
+
int len = snprintf(msg, sizeof(msg) - 1, "WebSocket op 0x%02x not supported on %llu.",
|
553
|
+
op, (unsigned long long)c->id);
|
554
|
+
|
555
|
+
push_error(c->up, msg, len);
|
529
556
|
log_cat(&error_cat, "WebSocket op 0x%02x not supported on %llu.", op, (unsigned long long)c->id);
|
530
557
|
return true;
|
531
558
|
}
|
559
|
+
}
|
532
560
|
}
|
533
561
|
if (NULL != c->req) {
|
534
562
|
mlen = c->req->mlen;
|
@@ -638,9 +666,15 @@ con_ws_write(Con c) {
|
|
638
666
|
if (NULL == message) {
|
639
667
|
if (res->ping) {
|
640
668
|
if (0 > (cnt = send(c->sock, ping_msg, sizeof(ping_msg) - 1, 0))) {
|
669
|
+
char msg[1024];
|
670
|
+
int len;
|
671
|
+
|
641
672
|
if (EAGAIN == errno) {
|
642
673
|
return false;
|
643
674
|
}
|
675
|
+
len = snprintf(msg, sizeof(msg) - 1, "Socket error @ %llu.", (unsigned long long)c->id);
|
676
|
+
push_error(c->up, msg, len);
|
677
|
+
|
644
678
|
log_cat(&error_cat, "Socket error @ %llu.", (unsigned long long)c->id);
|
645
679
|
ws_req_close(c);
|
646
680
|
res_destroy(res);
|
@@ -649,9 +683,14 @@ con_ws_write(Con c) {
|
|
649
683
|
}
|
650
684
|
} else if (res->pong) {
|
651
685
|
if (0 > (cnt = send(c->sock, pong_msg, sizeof(pong_msg) - 1, 0))) {
|
686
|
+
char msg[1024];
|
687
|
+
int len;
|
688
|
+
|
652
689
|
if (EAGAIN == errno) {
|
653
690
|
return false;
|
654
691
|
}
|
692
|
+
len = snprintf(msg, sizeof(msg) - 1, "Socket error @ %llu.", (unsigned long long)c->id);
|
693
|
+
push_error(c->up, msg, len);
|
655
694
|
log_cat(&error_cat, "Socket error @ %llu.", (unsigned long long)c->id);
|
656
695
|
ws_req_close(c);
|
657
696
|
res_destroy(res);
|
@@ -691,9 +730,14 @@ con_ws_write(Con c) {
|
|
691
730
|
}
|
692
731
|
}
|
693
732
|
if (0 > (cnt = send(c->sock, message->text + c->wcnt, message->len - c->wcnt, 0))) {
|
733
|
+
char msg[1024];
|
734
|
+
int len;
|
735
|
+
|
694
736
|
if (EAGAIN == errno) {
|
695
737
|
return false;
|
696
738
|
}
|
739
|
+
len = snprintf(msg, sizeof(msg) - 1, "Socket error @ %llu.", (unsigned long long)c->id);
|
740
|
+
push_error(c->up, msg, len);
|
697
741
|
log_cat(&error_cat, "Socket error @ %llu.", (unsigned long long)c->id);
|
698
742
|
ws_req_close(c);
|
699
743
|
|
@@ -741,9 +785,14 @@ con_sse_write(Con c) {
|
|
741
785
|
}
|
742
786
|
}
|
743
787
|
if (0 > (cnt = send(c->sock, message->text + c->wcnt, message->len - c->wcnt, 0))) {
|
788
|
+
char msg[1024];
|
789
|
+
int len;
|
790
|
+
|
744
791
|
if (EAGAIN == errno) {
|
745
792
|
return false;
|
746
793
|
}
|
794
|
+
len = snprintf(msg, sizeof(msg) - 1, "Socket error @ %llu.", (unsigned long long)c->id);
|
795
|
+
push_error(c->up, msg, len);
|
747
796
|
log_cat(&error_cat, "Socket error @ %llu.", (unsigned long long)c->id);
|
748
797
|
ws_req_close(c);
|
749
798
|
|
@@ -923,7 +972,11 @@ poll_setup(Con c, struct pollfd *pp) {
|
|
923
972
|
pp->revents = 0;
|
924
973
|
pp++;
|
925
974
|
for (; NULL != c; c = c->next) {
|
926
|
-
if (c->dead) {
|
975
|
+
if (c->dead || 0 == c->sock) {
|
976
|
+
continue;
|
977
|
+
}
|
978
|
+
if (c->hijacked) {
|
979
|
+
c->sock = 0;
|
927
980
|
continue;
|
928
981
|
}
|
929
982
|
c->pp = pp;
|
@@ -1085,7 +1138,7 @@ con_loop(void *x) {
|
|
1085
1138
|
goto CON_CHECK;
|
1086
1139
|
}
|
1087
1140
|
CON_CHECK:
|
1088
|
-
if (c->dead) {
|
1141
|
+
if (c->dead || 0 == c->sock) {
|
1089
1142
|
if (remove_dead_res(c)) {
|
1090
1143
|
goto CON_RM;
|
1091
1144
|
}
|
data/ext/agoo/con.h
CHANGED
data/ext/agoo/method.h
CHANGED
data/ext/agoo/request.c
CHANGED
@@ -26,6 +26,9 @@ static VALUE post_val = Qundef;
|
|
26
26
|
static VALUE put_val = Qundef;
|
27
27
|
static VALUE query_string_val = Qundef;
|
28
28
|
static VALUE rack_errors_val = Qundef;
|
29
|
+
static VALUE rack_hijack_io_val = Qundef;
|
30
|
+
static VALUE rack_hijack_val = Qundef;
|
31
|
+
static VALUE rack_hijackq_val = Qundef;
|
29
32
|
static VALUE rack_input_val = Qundef;
|
30
33
|
static VALUE rack_logger_val = Qundef;
|
31
34
|
static VALUE rack_multiprocess_val = Qundef;
|
@@ -546,7 +549,7 @@ rack_logger(VALUE self) {
|
|
546
549
|
* request is a more efficient encapsulation of the rack environment.
|
547
550
|
*/
|
548
551
|
VALUE
|
549
|
-
request_env(Req req) {
|
552
|
+
request_env(Req req, VALUE self) {
|
550
553
|
if (Qnil == req->env) {
|
551
554
|
volatile VALUE env = rb_hash_new();
|
552
555
|
|
@@ -570,6 +573,17 @@ request_env(Req req) {
|
|
570
573
|
rb_hash_aset(env, rack_run_once_val, Qfalse);
|
571
574
|
rb_hash_aset(env, rack_logger_val, req_rack_logger(req));
|
572
575
|
rb_hash_aset(env, rack_upgrade_val, req_rack_upgrade(req));
|
576
|
+
rb_hash_aset(env, rack_hijackq_val, Qtrue);
|
577
|
+
|
578
|
+
// TBD should return IO on #call and set hijack_io on env object that
|
579
|
+
// has a call method that wraps the req->res->con->sock then set the
|
580
|
+
// sock to 0 or maybe con. mutex? env[rack.hijack_io] = IO.new(sock,
|
581
|
+
// "rw") - maybe it works.
|
582
|
+
//
|
583
|
+
// set a flag on con to indicate it has been hijacked
|
584
|
+
// then set sock to 0 in con loop and destroy con
|
585
|
+
rb_hash_aset(env, rack_hijack_val, self);
|
586
|
+
rb_hash_aset(env, rack_hijack_io_val, Qnil);
|
573
587
|
|
574
588
|
req->env = env;
|
575
589
|
}
|
@@ -590,7 +604,7 @@ to_h(VALUE self) {
|
|
590
604
|
if (NULL == r) {
|
591
605
|
rb_raise(rb_eArgError, "Request is no longer valid.");
|
592
606
|
}
|
593
|
-
return request_env(r);
|
607
|
+
return request_env(r, self);
|
594
608
|
}
|
595
609
|
|
596
610
|
/* Document-method: to_s
|
@@ -606,6 +620,33 @@ to_s(VALUE self) {
|
|
606
620
|
return rb_funcall(h, rb_intern("to_s"), 0);
|
607
621
|
}
|
608
622
|
|
623
|
+
/* Document-method: call
|
624
|
+
*
|
625
|
+
* call-seq: call()
|
626
|
+
*
|
627
|
+
* Returns an IO like object and hijacks the connection.
|
628
|
+
*/
|
629
|
+
static VALUE
|
630
|
+
call(VALUE self) {
|
631
|
+
Req r = DATA_PTR(self);
|
632
|
+
VALUE args[1];
|
633
|
+
volatile VALUE io;
|
634
|
+
|
635
|
+
if (NULL == r) {
|
636
|
+
rb_raise(rb_eArgError, "Request is no longer valid.");
|
637
|
+
}
|
638
|
+
r->res->con->hijacked = true;
|
639
|
+
|
640
|
+
// TBD try basic IO first. If that fails define a socket class
|
641
|
+
// is a mode needed?
|
642
|
+
|
643
|
+
args[0] = INT2NUM(r->res->con->sock);
|
644
|
+
|
645
|
+
io = rb_class_new_instance(1, args, rb_cIO);
|
646
|
+
rb_hash_aset(r->env, rack_hijack_io_val, io);
|
647
|
+
|
648
|
+
return io;
|
649
|
+
}
|
609
650
|
void
|
610
651
|
request_destroy(Req req) {
|
611
652
|
DEBUG_FREE(mem_req, req)
|
@@ -649,6 +690,7 @@ request_init(VALUE mod) {
|
|
649
690
|
rb_define_method(req_class, "headers", headers, 0);
|
650
691
|
rb_define_method(req_class, "body", body, 0);
|
651
692
|
rb_define_method(req_class, "rack_logger", rack_logger, 0);
|
693
|
+
rb_define_method(req_class, "call", call, 0);
|
652
694
|
|
653
695
|
new_id = rb_intern("new");
|
654
696
|
|
@@ -674,6 +716,9 @@ request_init(VALUE mod) {
|
|
674
716
|
put_val = rb_str_new_cstr("PUT"); rb_gc_register_address(&put_val);
|
675
717
|
query_string_val = rb_str_new_cstr("QUERY_STRING"); rb_gc_register_address(&query_string_val);
|
676
718
|
rack_errors_val = rb_str_new_cstr("rack.errors"); rb_gc_register_address(&rack_errors_val);
|
719
|
+
rack_hijack_io_val = rb_str_new_cstr("rack.hijack_io"); rb_gc_register_address(&rack_hijack_io_val);
|
720
|
+
rack_hijack_val = rb_str_new_cstr("rack.hijack"); rb_gc_register_address(&rack_hijack_val);
|
721
|
+
rack_hijackq_val = rb_str_new_cstr("rack.hijack?"); rb_gc_register_address(&rack_hijackq_val);
|
677
722
|
rack_input_val = rb_str_new_cstr("rack.input"); rb_gc_register_address(&rack_input_val);
|
678
723
|
rack_logger_val = rb_str_new_cstr("rack.logger"); rb_gc_register_address(&rack_logger_val);
|
679
724
|
rack_multiprocess_val = rb_str_new_cstr("rack.multiprocess");rb_gc_register_address(&rack_multiprocess_val);
|
data/ext/agoo/request.h
CHANGED
@@ -44,7 +44,7 @@ typedef struct _Req {
|
|
44
44
|
extern Req request_create(size_t mlen);
|
45
45
|
extern void request_init(VALUE mod);
|
46
46
|
extern VALUE request_wrap(Req req);
|
47
|
-
extern VALUE request_env(Req req);
|
47
|
+
extern VALUE request_env(Req req, VALUE self);
|
48
48
|
extern void request_destroy(Req req);
|
49
49
|
|
50
50
|
#endif // __AGOO_REQUEST_H__
|
data/ext/agoo/server.c
CHANGED
@@ -56,6 +56,7 @@ static ID call_id;
|
|
56
56
|
static ID each_id;
|
57
57
|
static ID on_close_id;
|
58
58
|
static ID on_drained_id;
|
59
|
+
static ID on_error_id;
|
59
60
|
static ID on_message_id;
|
60
61
|
static ID on_request_id;
|
61
62
|
static ID to_i_id;
|
@@ -508,7 +509,7 @@ static VALUE
|
|
508
509
|
handle_rack_inner(void *x) {
|
509
510
|
Req req = (Req)x;
|
510
511
|
Text t;
|
511
|
-
volatile VALUE env = request_env(req);
|
512
|
+
volatile VALUE env = request_env(req, request_wrap(req));
|
512
513
|
volatile VALUE res = rb_funcall(req->handler, call_id, 1, env);
|
513
514
|
volatile VALUE hv;
|
514
515
|
volatile VALUE bv;
|
@@ -516,6 +517,10 @@ handle_rack_inner(void *x) {
|
|
516
517
|
const char *status_msg;
|
517
518
|
int bsize = 0;
|
518
519
|
|
520
|
+
if (req->res->con->hijacked) {
|
521
|
+
queue_wakeup(&the_server.con_queue);
|
522
|
+
return false;
|
523
|
+
}
|
519
524
|
rb_check_type(res, T_ARRAY);
|
520
525
|
if (3 != RARRAY_LEN(res)) {
|
521
526
|
rb_raise(rb_eArgError, "a rack call() response must be an array of 3 members.");
|
@@ -599,7 +604,7 @@ handle_rack_inner(void *x) {
|
|
599
604
|
break;
|
600
605
|
}
|
601
606
|
req->handler_type = PUSH_HOOK;
|
602
|
-
upgraded_create(req->res->con, req->handler, request_env(req));
|
607
|
+
upgraded_create(req->res->con, req->handler, request_env(req, Qnil));
|
603
608
|
t->len = snprintf(t->text, 1024, "HTTP/1.1 101 %s\r\n", status_msg);
|
604
609
|
t = ws_add_headers(req, t);
|
605
610
|
break;
|
@@ -611,7 +616,7 @@ handle_rack_inner(void *x) {
|
|
611
616
|
break;
|
612
617
|
}
|
613
618
|
req->handler_type = PUSH_HOOK;
|
614
|
-
upgraded_create(req->res->con, req->handler, request_env(req));
|
619
|
+
upgraded_create(req->res->con, req->handler, request_env(req, Qnil));
|
615
620
|
t = sse_upgrade(req, t);
|
616
621
|
res_set_message(req->res, t);
|
617
622
|
queue_wakeup(&the_server.con_queue);
|
@@ -708,6 +713,14 @@ handle_push_inner(void *x) {
|
|
708
713
|
case ON_SHUTDOWN:
|
709
714
|
rb_funcall(req->handler, rb_intern("on_shutdown"), 1, req->up->wrap);
|
710
715
|
break;
|
716
|
+
case ON_ERROR:
|
717
|
+
if (req->up->on_msg) {
|
718
|
+
volatile VALUE rstr = rb_str_new(req->msg, req->mlen);
|
719
|
+
|
720
|
+
rb_enc_associate(rstr, rb_ascii8bit_encoding());
|
721
|
+
rb_funcall(req->handler, on_error_id, 2, req->up->wrap, rstr);
|
722
|
+
}
|
723
|
+
break;
|
711
724
|
case ON_EMPTY:
|
712
725
|
rb_funcall(req->handler, on_drained_id, 1, req->up->wrap);
|
713
726
|
break;
|
@@ -1034,6 +1047,7 @@ server_init(VALUE mod) {
|
|
1034
1047
|
each_id = rb_intern("each");
|
1035
1048
|
on_close_id = rb_intern("on_close");
|
1036
1049
|
on_drained_id = rb_intern("on_drained");
|
1050
|
+
on_error_id = rb_intern("on_error");
|
1037
1051
|
on_message_id = rb_intern("on_message");
|
1038
1052
|
on_request_id = rb_intern("on_request");
|
1039
1053
|
to_i_id = rb_intern("to_i");
|
data/ext/agoo/upgraded.c
CHANGED
@@ -351,6 +351,7 @@ upgraded_create(Con c, VALUE obj, VALUE env) {
|
|
351
351
|
up->on_close = rb_respond_to(obj, rb_intern("on_close"));
|
352
352
|
up->on_shut = rb_respond_to(obj, rb_intern("on_shutdown"));
|
353
353
|
up->on_msg = rb_respond_to(obj, rb_intern("on_message"));
|
354
|
+
up->on_error = rb_respond_to(obj, rb_intern("on_error"));
|
354
355
|
up->wrap = Data_Wrap_Struct(upgraded_class, NULL, NULL, up);
|
355
356
|
up->subjects = NULL;
|
356
357
|
up->prev = NULL;
|
data/ext/agoo/upgraded.h
CHANGED
data/lib/agoo/version.rb
CHANGED
data/test/hijack_test.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.dirname(__FILE__)
|
4
|
+
$root_dir = File.dirname(File.expand_path(File.dirname(__FILE__)))
|
5
|
+
%w(lib ext).each do |dir|
|
6
|
+
$: << File.join($root_dir, dir)
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'minitest'
|
10
|
+
require 'minitest/autorun'
|
11
|
+
require 'net/http'
|
12
|
+
|
13
|
+
require 'oj'
|
14
|
+
|
15
|
+
require 'agoo'
|
16
|
+
|
17
|
+
class RackHandlerTest < Minitest::Test
|
18
|
+
|
19
|
+
class HijackHandler
|
20
|
+
def call(env)
|
21
|
+
io = env['rack.hijack'].call
|
22
|
+
io.write(%|HTTP/1.1 200 OK
|
23
|
+
Content-Length: 11
|
24
|
+
|
25
|
+
hello world|)
|
26
|
+
io.flush
|
27
|
+
[ -1, {}, []]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_hijack
|
32
|
+
begin
|
33
|
+
Agoo::Log.configure(dir: '',
|
34
|
+
console: true,
|
35
|
+
classic: true,
|
36
|
+
colorize: true,
|
37
|
+
states: {
|
38
|
+
INFO: false,
|
39
|
+
DEBUG: false,
|
40
|
+
connect: false,
|
41
|
+
request: false,
|
42
|
+
response: false,
|
43
|
+
eval: true,
|
44
|
+
})
|
45
|
+
|
46
|
+
Agoo::Server.init(6471, 'root', thread_count: 1)
|
47
|
+
|
48
|
+
handler = HijackHandler.new
|
49
|
+
Agoo::Server.handle(:GET, "/hijack", handler)
|
50
|
+
|
51
|
+
Agoo::Server.start()
|
52
|
+
|
53
|
+
jack
|
54
|
+
|
55
|
+
ensure
|
56
|
+
Agoo.shutdown
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def jack
|
61
|
+
uri = URI('http://localhost:6471/hijack')
|
62
|
+
req = Net::HTTP::Get.new(uri)
|
63
|
+
# Set the headers the way we want them.
|
64
|
+
req['Accept-Encoding'] = '*'
|
65
|
+
req['User-Agent'] = 'Ruby'
|
66
|
+
req['Host'] = 'localhost:6471'
|
67
|
+
|
68
|
+
res = Net::HTTP.start(uri.hostname, uri.port) { |h|
|
69
|
+
h.request(req)
|
70
|
+
}
|
71
|
+
content = res.body
|
72
|
+
|
73
|
+
assert_equal('hello world', content, 'content mismatch')
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: agoo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Ohler
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-07-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -107,6 +107,7 @@ files:
|
|
107
107
|
- lib/agoo/version.rb
|
108
108
|
- lib/rack/handler/agoo.rb
|
109
109
|
- test/base_handler_test.rb
|
110
|
+
- test/hijack_test.rb
|
110
111
|
- test/log_test.rb
|
111
112
|
- test/rack_handler_test.rb
|
112
113
|
- test/static_test.rb
|
@@ -148,5 +149,6 @@ summary: An HTTP server
|
|
148
149
|
test_files:
|
149
150
|
- test/log_test.rb
|
150
151
|
- test/rack_handler_test.rb
|
152
|
+
- test/hijack_test.rb
|
151
153
|
- test/base_handler_test.rb
|
152
154
|
- test/static_test.rb
|