cool.io 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +2 -1
- data/CHANGES.md +9 -0
- data/Rakefile +0 -15
- data/cool.io.gemspec +1 -1
- data/examples/callbacked_echo_server.rb +24 -0
- data/ext/libev/Changes +117 -3
- data/ext/libev/LICENSE +2 -1
- data/ext/libev/README +8 -8
- data/ext/libev/ev.c +118 -24
- data/ext/libev/ev.h +6 -4
- data/ext/libev/ev_epoll.c +4 -1
- data/ext/libev/ev_kqueue.c +1 -1
- data/ext/libev/ev_vars.h +3 -2
- data/ext/libev/ev_win32.c +1 -1
- data/ext/libev/win_select.patch +115 -0
- data/lib/cool.io.rb +0 -2
- data/lib/cool.io/io.rb +0 -5
- data/lib/cool.io/meta.rb +1 -1
- data/lib/cool.io/version.rb +1 -1
- metadata +5 -15
- data/examples/httpclient.rb +0 -38
- data/ext/http11_client/.gitignore +0 -5
- data/ext/http11_client/LICENSE +0 -31
- data/ext/http11_client/ext_help.h +0 -14
- data/ext/http11_client/extconf.rb +0 -6
- data/ext/http11_client/http11_client.c +0 -300
- data/ext/http11_client/http11_parser.c +0 -403
- data/ext/http11_client/http11_parser.h +0 -48
- data/ext/http11_client/http11_parser.rl +0 -173
- data/lib/cool.io/eventmachine.rb +0 -234
- data/lib/cool.io/http_client.rb +0 -430
@@ -1,173 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Copyright (c) 2005 Zed A. Shaw
|
3
|
-
* You can redistribute it and/or modify it under the same terms as Ruby.
|
4
|
-
*/
|
5
|
-
|
6
|
-
#include "http11_parser.h"
|
7
|
-
#include <stdio.h>
|
8
|
-
#include <assert.h>
|
9
|
-
#include <stdlib.h>
|
10
|
-
#include <ctype.h>
|
11
|
-
#include <string.h>
|
12
|
-
|
13
|
-
#define LEN(AT, FPC) (FPC - buffer - parser->AT)
|
14
|
-
#define MARK(M,FPC) (parser->M = (FPC) - buffer)
|
15
|
-
#define PTR_TO(F) (buffer + parser->F)
|
16
|
-
#define L(M) fprintf(stderr, "" # M "\n");
|
17
|
-
|
18
|
-
|
19
|
-
/** machine **/
|
20
|
-
%%{
|
21
|
-
machine httpclient_parser;
|
22
|
-
|
23
|
-
action mark {MARK(mark, fpc); }
|
24
|
-
|
25
|
-
action start_field { MARK(field_start, fpc); }
|
26
|
-
|
27
|
-
action write_field {
|
28
|
-
parser->field_len = LEN(field_start, fpc);
|
29
|
-
}
|
30
|
-
|
31
|
-
action start_value { MARK(mark, fpc); }
|
32
|
-
|
33
|
-
action write_value {
|
34
|
-
parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc));
|
35
|
-
}
|
36
|
-
|
37
|
-
action reason_phrase {
|
38
|
-
parser->reason_phrase(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
39
|
-
}
|
40
|
-
|
41
|
-
action status_code {
|
42
|
-
parser->status_code(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
43
|
-
}
|
44
|
-
|
45
|
-
action http_version {
|
46
|
-
parser->http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
47
|
-
}
|
48
|
-
|
49
|
-
action chunk_size {
|
50
|
-
parser->chunk_size(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
51
|
-
}
|
52
|
-
|
53
|
-
action last_chunk {
|
54
|
-
parser->last_chunk(parser->data, NULL, 0);
|
55
|
-
}
|
56
|
-
|
57
|
-
action done {
|
58
|
-
parser->body_start = fpc - buffer + 1;
|
59
|
-
if(parser->header_done != NULL)
|
60
|
-
parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
|
61
|
-
fbreak;
|
62
|
-
}
|
63
|
-
|
64
|
-
# line endings
|
65
|
-
CRLF = "\r\n";
|
66
|
-
|
67
|
-
# character types
|
68
|
-
CTL = (cntrl | 127);
|
69
|
-
tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
|
70
|
-
|
71
|
-
# elements
|
72
|
-
token = (ascii -- (CTL | tspecials));
|
73
|
-
|
74
|
-
Reason_Phrase = (any -- CRLF)* >mark %reason_phrase;
|
75
|
-
Status_Code = digit{3} >mark %status_code;
|
76
|
-
http_number = (digit+ "." digit+) ;
|
77
|
-
HTTP_Version = ("HTTP/" http_number) >mark %http_version ;
|
78
|
-
Status_Line = HTTP_Version " " Status_Code " "? Reason_Phrase :> CRLF;
|
79
|
-
|
80
|
-
field_name = token+ >start_field %write_field;
|
81
|
-
field_value = any* >start_value %write_value;
|
82
|
-
message_header = field_name ":" " "* field_value :> CRLF;
|
83
|
-
|
84
|
-
Response = Status_Line (message_header)* (CRLF @done);
|
85
|
-
|
86
|
-
chunk_ext_val = token+;
|
87
|
-
chunk_ext_name = token+;
|
88
|
-
chunk_extension = (";" chunk_ext_name >start_field %write_field %start_value ("=" chunk_ext_val >start_value)? %write_value )*;
|
89
|
-
last_chunk = "0"? chunk_extension :> (CRLF @last_chunk @done);
|
90
|
-
chunk_size = xdigit+;
|
91
|
-
chunk = chunk_size >mark %chunk_size chunk_extension space* :> (CRLF @done);
|
92
|
-
Chunked_Header = (chunk | last_chunk);
|
93
|
-
|
94
|
-
main := Response | Chunked_Header;
|
95
|
-
}%%
|
96
|
-
|
97
|
-
/** Data **/
|
98
|
-
%% write data;
|
99
|
-
|
100
|
-
int httpclient_parser_init(httpclient_parser *parser) {
|
101
|
-
int cs = 0;
|
102
|
-
%% write init;
|
103
|
-
parser->cs = cs;
|
104
|
-
parser->body_start = 0;
|
105
|
-
parser->content_len = 0;
|
106
|
-
parser->mark = 0;
|
107
|
-
parser->nread = 0;
|
108
|
-
parser->field_len = 0;
|
109
|
-
parser->field_start = 0;
|
110
|
-
|
111
|
-
return(1);
|
112
|
-
}
|
113
|
-
|
114
|
-
|
115
|
-
/** exec **/
|
116
|
-
size_t httpclient_parser_execute(httpclient_parser *parser, const char *buffer, size_t len, size_t off) {
|
117
|
-
const char *p, *pe;
|
118
|
-
int cs = parser->cs;
|
119
|
-
|
120
|
-
assert(off <= len && "offset past end of buffer");
|
121
|
-
|
122
|
-
p = buffer+off;
|
123
|
-
pe = buffer+len;
|
124
|
-
|
125
|
-
assert(*pe == '\0' && "pointer does not end on NUL");
|
126
|
-
assert(pe - p == len - off && "pointers aren't same distance");
|
127
|
-
|
128
|
-
|
129
|
-
%% write exec;
|
130
|
-
|
131
|
-
parser->cs = cs;
|
132
|
-
parser->nread += p - (buffer + off);
|
133
|
-
|
134
|
-
assert(p <= pe && "buffer overflow after parsing execute");
|
135
|
-
assert(parser->nread <= len && "nread longer than length");
|
136
|
-
assert(parser->body_start <= len && "body starts after buffer end");
|
137
|
-
assert(parser->mark < len && "mark is after buffer end");
|
138
|
-
assert(parser->field_len <= len && "field has length longer than whole buffer");
|
139
|
-
assert(parser->field_start < len && "field starts after buffer end");
|
140
|
-
|
141
|
-
if(parser->body_start) {
|
142
|
-
/* final \r\n combo encountered so stop right here */
|
143
|
-
%%write eof;
|
144
|
-
parser->nread++;
|
145
|
-
}
|
146
|
-
|
147
|
-
return(parser->nread);
|
148
|
-
}
|
149
|
-
|
150
|
-
int httpclient_parser_finish(httpclient_parser *parser)
|
151
|
-
{
|
152
|
-
int cs = parser->cs;
|
153
|
-
|
154
|
-
%%write eof;
|
155
|
-
|
156
|
-
parser->cs = cs;
|
157
|
-
|
158
|
-
if (httpclient_parser_has_error(parser) ) {
|
159
|
-
return -1;
|
160
|
-
} else if (httpclient_parser_is_finished(parser) ) {
|
161
|
-
return 1;
|
162
|
-
} else {
|
163
|
-
return 0;
|
164
|
-
}
|
165
|
-
}
|
166
|
-
|
167
|
-
int httpclient_parser_has_error(httpclient_parser *parser) {
|
168
|
-
return parser->cs == httpclient_parser_error;
|
169
|
-
}
|
170
|
-
|
171
|
-
int httpclient_parser_is_finished(httpclient_parser *parser) {
|
172
|
-
return parser->cs == httpclient_parser_first_final;
|
173
|
-
}
|
data/lib/cool.io/eventmachine.rb
DELETED
@@ -1,234 +0,0 @@
|
|
1
|
-
#--
|
2
|
-
# Copyright (C)2007-10 Tony Arcieri, Roger Pack
|
3
|
-
# You can redistribute this under the terms of the Ruby license
|
4
|
-
# See file LICENSE for details
|
5
|
-
#++
|
6
|
-
|
7
|
-
require 'cool.io'
|
8
|
-
|
9
|
-
# EventMachine emulation for Cool.io:
|
10
|
-
#
|
11
|
-
# require 'coolio/eventmachine'
|
12
|
-
#
|
13
|
-
# Drawbacks: slightly slower than EM.
|
14
|
-
# Benefits: timers are more accurate using libev than using EM
|
15
|
-
# TODO: some things like connection timeouts aren't implemented yet
|
16
|
-
# DONE: timers and normal socket functions are implemented.
|
17
|
-
module EventMachine
|
18
|
-
class << self
|
19
|
-
# Start the Reactor loop
|
20
|
-
def run
|
21
|
-
yield if block_given?
|
22
|
-
Coolio::Loop.default.run
|
23
|
-
end
|
24
|
-
|
25
|
-
# Stop the Reactor loop
|
26
|
-
def stop_event_loop
|
27
|
-
Coolio::Loop.default.stop
|
28
|
-
end
|
29
|
-
|
30
|
-
class OneShotEMTimer < Coolio::TimerWatcher
|
31
|
-
def setup(proc)
|
32
|
-
@proc = proc
|
33
|
-
end
|
34
|
-
|
35
|
-
def on_timer
|
36
|
-
@proc.call
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# ltodo: use Coolio's PeriodicTimer to wrap EM's two similar to it
|
41
|
-
# todo: close all connections on 'stop', I believe
|
42
|
-
|
43
|
-
def add_timer(interval, proc = nil, &block)
|
44
|
-
block ||= proc
|
45
|
-
t = OneShotEMTimer.new(interval, false) # non repeating
|
46
|
-
t.setup(block)
|
47
|
-
|
48
|
-
# fire 'er off ltodo: do we keep track of these timers in memory?
|
49
|
-
t.attach(Coolio::Loop.default)
|
50
|
-
t
|
51
|
-
end
|
52
|
-
|
53
|
-
def cancel_timer(t)
|
54
|
-
# guess there's a case where EM you can say 'cancel' but it's already fired?
|
55
|
-
# kind of odd but it happens
|
56
|
-
t.detach if t.attached?
|
57
|
-
end
|
58
|
-
|
59
|
-
def set_comm_inactivity_timeout(*args); end # TODO
|
60
|
-
|
61
|
-
# Make an outgoing connection
|
62
|
-
def connect(addr, port, handler = Connection, *args, &block)
|
63
|
-
block = args.pop if Proc === args[-1]
|
64
|
-
|
65
|
-
# make sure we're a 'real' class here
|
66
|
-
klass = if (handler and handler.is_a?(Class))
|
67
|
-
handler
|
68
|
-
else
|
69
|
-
Class.new( Connection ) {handler and include handler}
|
70
|
-
end
|
71
|
-
|
72
|
-
wrapped_child = CallsBackToEM.connect(addr, port, *args) # ltodo: args? what? they're used? also TODOC TODO FIX
|
73
|
-
conn = klass.new(wrapped_child) # ltodo [?] addr, port, *args)
|
74
|
-
wrapped_child.attach(Coolio::Loop.default) # necessary
|
75
|
-
conn.heres_your_socket(wrapped_child)
|
76
|
-
wrapped_child.call_back_to_this(conn) # calls post_init for us
|
77
|
-
yield conn if block_given?
|
78
|
-
end
|
79
|
-
|
80
|
-
# Start a TCP server on the given address and port
|
81
|
-
def start_server(addr, port, handler = Connection, *args, &block)
|
82
|
-
# make sure we're a 'real' class here
|
83
|
-
klass = if (handler and handler.is_a?(Class))
|
84
|
-
handler
|
85
|
-
else
|
86
|
-
Class.new( Connection ) {handler and include handler}
|
87
|
-
end
|
88
|
-
|
89
|
-
server = Coolio::TCPServer.new(addr, port, CallsBackToEM, *args) do |wrapped_child|
|
90
|
-
conn = klass.new(wrapped_child)
|
91
|
-
conn.heres_your_socket(wrapped_child) # ideally NOT have this :)
|
92
|
-
wrapped_child.call_back_to_this(conn)
|
93
|
-
block.call(conn) if block
|
94
|
-
end
|
95
|
-
|
96
|
-
server.attach(Coolio::Loop.default)
|
97
|
-
end
|
98
|
-
|
99
|
-
def stop_server(server)
|
100
|
-
server.close
|
101
|
-
end
|
102
|
-
|
103
|
-
# Set the maximum number of descriptors available to this process
|
104
|
-
def set_descriptor_table_size(nfds)
|
105
|
-
Coolio::Utils.maxfds = nfds
|
106
|
-
end
|
107
|
-
|
108
|
-
# Compatibility noop. Handled automatically by libev
|
109
|
-
def epoll; end
|
110
|
-
|
111
|
-
# Compatibility noop. Handled automatically by libev
|
112
|
-
def kqueue; end
|
113
|
-
end
|
114
|
-
|
115
|
-
class CallsBackToEM < Coolio::TCPSocket
|
116
|
-
class ConnectTimer < Coolio::TimerWatcher
|
117
|
-
attr_accessor :parent
|
118
|
-
def on_timer
|
119
|
-
@parent.connection_has_timed_out
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def call_back_to_this parent
|
124
|
-
@call_back_to_this = parent
|
125
|
-
parent.post_init
|
126
|
-
end
|
127
|
-
|
128
|
-
def on_connect
|
129
|
-
# @connection_timer.detach if @connection_timer
|
130
|
-
# won't need that anymore :) -- with server connecteds we don't have it, anyway
|
131
|
-
|
132
|
-
# TODO should server accepted's call this? They don't currently
|
133
|
-
# [and can't, since on_connect gets called basically in the initializer--needs some code love for that to happen :)
|
134
|
-
@call_back_to_this.connection_completed if @call_back_to_this
|
135
|
-
end
|
136
|
-
|
137
|
-
def connection_has_timed_out
|
138
|
-
return if closed?
|
139
|
-
|
140
|
-
# wonder if this works when you're within a half-connected phase.
|
141
|
-
# I think it does. What about TCP state?
|
142
|
-
close unless closed?
|
143
|
-
@call_back_to_this.unbind
|
144
|
-
end
|
145
|
-
|
146
|
-
def on_write_complete
|
147
|
-
close if @should_close_after_writing
|
148
|
-
end
|
149
|
-
|
150
|
-
def should_close_after_writing
|
151
|
-
@should_close_after_writing = true;
|
152
|
-
end
|
153
|
-
|
154
|
-
def on_close
|
155
|
-
@call_back_to_this.unbind # about the same ltodo check if they ARE the same here
|
156
|
-
end
|
157
|
-
|
158
|
-
def on_resolve_failed
|
159
|
-
fail
|
160
|
-
end
|
161
|
-
|
162
|
-
def on_connect_failed
|
163
|
-
fail
|
164
|
-
end
|
165
|
-
|
166
|
-
def on_read(data)
|
167
|
-
@call_back_to_this.receive_data data
|
168
|
-
end
|
169
|
-
|
170
|
-
def fail
|
171
|
-
#@connection_timer.detch if @connection_timer
|
172
|
-
@call_back_to_this.unbind
|
173
|
-
end
|
174
|
-
|
175
|
-
def self.connect(*args)
|
176
|
-
a = super *args
|
177
|
-
# the connect timer currently kills TCPServer classes. I'm not sure why.
|
178
|
-
#@connection_timer = ConnectTimer.new(14) # needs to be at least higher than 12 :)
|
179
|
-
#@connection_timer.parent = a
|
180
|
-
#@connection_timer.attach(Coolio::Loop.default)
|
181
|
-
a
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
class Connection
|
186
|
-
def self.new(*args)
|
187
|
-
allocate#.instance_eval do
|
188
|
-
# initialize *args
|
189
|
-
#end
|
190
|
-
end
|
191
|
-
|
192
|
-
# we will need to call 'their functions' appropriately -- the commented out ones, here
|
193
|
-
#
|
194
|
-
# Callback fired when connection is created
|
195
|
-
def post_init
|
196
|
-
# I thought we were 'overriding' EM's existing methods, here.
|
197
|
-
# Huh? Why do we have to define these then?
|
198
|
-
end
|
199
|
-
|
200
|
-
# Callback fired when connection is closed
|
201
|
-
def unbind; end
|
202
|
-
|
203
|
-
# Callback fired when data is received
|
204
|
-
# def receive_data(data); end
|
205
|
-
def heres_your_socket(instantiated_coolio_socket)
|
206
|
-
instantiated_coolio_socket.call_back_to_this self
|
207
|
-
@wrapped_coolio = instantiated_coolio_socket
|
208
|
-
end
|
209
|
-
|
210
|
-
# Send data to the current connection -- called by them
|
211
|
-
def send_data(data)
|
212
|
-
@wrapped_coolio.write data
|
213
|
-
end
|
214
|
-
|
215
|
-
# Close the connection, optionally after writing
|
216
|
-
def close_connection(after_writing = false)
|
217
|
-
return close_connection_after_writing if after_writing
|
218
|
-
@wrapped_coolio.close
|
219
|
-
end
|
220
|
-
|
221
|
-
# Close the connection after all data has been written
|
222
|
-
def close_connection_after_writing
|
223
|
-
@wrapped_coolio.output_buffer_size.zero? ? @wrapped_coolio.close : @wrapped_coolio.should_close_after_writing
|
224
|
-
end
|
225
|
-
|
226
|
-
def get_peername
|
227
|
-
family, port, host_name, host_ip = @wrapped_coolio.peeraddr
|
228
|
-
Socket.pack_sockaddr_in(port, host_ip) # pack it up :)
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
# Shortcut constant
|
234
|
-
EM = EventMachine
|
data/lib/cool.io/http_client.rb
DELETED
@@ -1,430 +0,0 @@
|
|
1
|
-
#--
|
2
|
-
# Copyright (C)2007-10 Tony Arcieri
|
3
|
-
# Includes portions originally Copyright (C)2005 Zed Shaw
|
4
|
-
# You can redistribute this under the terms of the Ruby license
|
5
|
-
# See file LICENSE for details
|
6
|
-
#++
|
7
|
-
|
8
|
-
require "cool.io/custom_require"
|
9
|
-
cool_require 'http11_client'
|
10
|
-
|
11
|
-
module Coolio
|
12
|
-
# A simple hash is returned for each request made by HttpClient with
|
13
|
-
# the headers that were given by the server for that request.
|
14
|
-
class HttpResponseHeader < Hash
|
15
|
-
# The reason returned in the http response ("OK","File not found",etc.)
|
16
|
-
attr_accessor :http_reason
|
17
|
-
|
18
|
-
# The HTTP version returned.
|
19
|
-
attr_accessor :http_version
|
20
|
-
|
21
|
-
# The status code (as a string!)
|
22
|
-
attr_accessor :http_status
|
23
|
-
|
24
|
-
# HTTP response status as an integer
|
25
|
-
def status
|
26
|
-
Integer(http_status) rescue nil
|
27
|
-
end
|
28
|
-
|
29
|
-
# Length of content as an integer, or nil if chunked/unspecified
|
30
|
-
def content_length
|
31
|
-
Integer(self[HttpClient::CONTENT_LENGTH]) rescue nil
|
32
|
-
end
|
33
|
-
|
34
|
-
# Is the transfer encoding chunked?
|
35
|
-
def chunked_encoding?
|
36
|
-
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class HttpChunkHeader < Hash
|
41
|
-
# When parsing chunked encodings this is set
|
42
|
-
attr_accessor :http_chunk_size
|
43
|
-
|
44
|
-
# Size of the chunk as an integer
|
45
|
-
def chunk_size
|
46
|
-
return @chunk_size unless @chunk_size.nil?
|
47
|
-
@chunk_size = @http_chunk_size ? @http_chunk_size.to_i(base=16) : 0
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# Methods for building HTTP requests
|
52
|
-
module HttpEncoding
|
53
|
-
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
54
|
-
FIELD_ENCODING = "%s: %s\r\n"
|
55
|
-
|
56
|
-
# Escapes a URI.
|
57
|
-
def escape(s)
|
58
|
-
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
59
|
-
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
60
|
-
}.tr(' ', '+')
|
61
|
-
end
|
62
|
-
|
63
|
-
# Unescapes a URI escaped string.
|
64
|
-
def unescape(s)
|
65
|
-
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
66
|
-
[$1.delete('%')].pack('H*')
|
67
|
-
}
|
68
|
-
end
|
69
|
-
|
70
|
-
# Map all header keys to a downcased string version
|
71
|
-
def munge_header_keys(head)
|
72
|
-
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
73
|
-
end
|
74
|
-
|
75
|
-
# HTTP is kind of retarded that you have to specify
|
76
|
-
# a Host header, but if you include port 80 then further
|
77
|
-
# redirects will tack on the :80 which is annoying.
|
78
|
-
def encode_host
|
79
|
-
remote_host + (remote_port.to_i != 80 ? ":#{remote_port}" : "")
|
80
|
-
end
|
81
|
-
|
82
|
-
def encode_request(method, path, query)
|
83
|
-
HTTP_REQUEST_HEADER % [method.to_s.upcase, encode_query(path, query)]
|
84
|
-
end
|
85
|
-
|
86
|
-
def encode_query(path, query)
|
87
|
-
return path unless query
|
88
|
-
path + "?" + query.map { |k, v| encode_param(k, v) }.join('&')
|
89
|
-
end
|
90
|
-
|
91
|
-
# URL encodes a single k=v parameter.
|
92
|
-
def encode_param(k, v)
|
93
|
-
escape(k) + "=" + escape(v)
|
94
|
-
end
|
95
|
-
|
96
|
-
# Encode a field in an HTTP header
|
97
|
-
def encode_field(k, v)
|
98
|
-
FIELD_ENCODING % [k, v]
|
99
|
-
end
|
100
|
-
|
101
|
-
def encode_headers(head)
|
102
|
-
head.inject('') do |result, (key, value)|
|
103
|
-
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
104
|
-
key = key.split('-').map { |k| k.capitalize }.join('-')
|
105
|
-
result << encode_field(key, value)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def encode_cookies(cookies)
|
110
|
-
cookies.inject('') { |result, (k, v)| result << encode_field('Cookie', encode_param(k, v)) }
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
# HttpClient is tested on only CRuby. AFAIK, there is no HttpClient users.
|
115
|
-
# So HttpClient will not be maintained in the future.
|
116
|
-
|
117
|
-
# HTTP client class implemented as a subclass of Coolio::TCPSocket. Encodes
|
118
|
-
# requests and allows streaming consumption of the response. Response is
|
119
|
-
# parsed with a Ragel-generated whitelist parser which supports chunked
|
120
|
-
# HTTP encoding.
|
121
|
-
#
|
122
|
-
# == Example
|
123
|
-
#
|
124
|
-
# loop = Coolio::Loop.default
|
125
|
-
# client = Coolio::HttpClient.connect("www.google.com").attach(loop)
|
126
|
-
# client.request('GET', '/search', query: {q: 'foobar'})
|
127
|
-
# loop.run
|
128
|
-
#
|
129
|
-
class HttpClient < TCPSocket
|
130
|
-
include HttpEncoding
|
131
|
-
|
132
|
-
ALLOWED_METHODS=[:put, :get, :post, :delete, :head]
|
133
|
-
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
134
|
-
CONTENT_LENGTH="CONTENT_LENGTH"
|
135
|
-
SET_COOKIE="SET_COOKIE"
|
136
|
-
LOCATION="LOCATION"
|
137
|
-
HOST="HOST"
|
138
|
-
CRLF="\r\n"
|
139
|
-
|
140
|
-
# Connect to the given server, with port 80 as the default
|
141
|
-
def self.connect(addr, port = 80, *args)
|
142
|
-
super
|
143
|
-
end
|
144
|
-
|
145
|
-
def initialize(socket)
|
146
|
-
super
|
147
|
-
|
148
|
-
@parser = HttpClientParser.new
|
149
|
-
@parser_nbytes = 0
|
150
|
-
|
151
|
-
@state = :response_header
|
152
|
-
@data = ::IO::Buffer.new
|
153
|
-
|
154
|
-
@response_header = HttpResponseHeader.new
|
155
|
-
@chunk_header = HttpChunkHeader.new
|
156
|
-
end
|
157
|
-
|
158
|
-
# Send an HTTP request and consume the response.
|
159
|
-
# Supports the following options:
|
160
|
-
#
|
161
|
-
# head: {Key: Value}
|
162
|
-
# Specify an HTTP header, e.g. {'Connection': 'close'}
|
163
|
-
#
|
164
|
-
# query: {Key: Value}
|
165
|
-
# Specify query string parameters (auto-escaped)
|
166
|
-
#
|
167
|
-
# cookies: {Key: Value}
|
168
|
-
# Specify hash of cookies (auto-escaped)
|
169
|
-
#
|
170
|
-
# body: String
|
171
|
-
# Specify the request body (you must encode it for now)
|
172
|
-
#
|
173
|
-
def request(method, path, options = {})
|
174
|
-
raise ArgumentError, "invalid request path" unless /^\// === path
|
175
|
-
raise RuntimeError, "request already sent" if @requested
|
176
|
-
|
177
|
-
@method, @path, @options = method, path, options
|
178
|
-
@requested = true
|
179
|
-
|
180
|
-
return unless @connected
|
181
|
-
send_request
|
182
|
-
end
|
183
|
-
|
184
|
-
# Enable the HttpClient if it has been disabled
|
185
|
-
def enable
|
186
|
-
super
|
187
|
-
dispatch unless @data.empty?
|
188
|
-
end
|
189
|
-
|
190
|
-
# Called when response header has been received
|
191
|
-
def on_response_header(response_header)
|
192
|
-
end
|
193
|
-
|
194
|
-
# Called when part of the body has been read
|
195
|
-
def on_body_data(data)
|
196
|
-
STDOUT.write data
|
197
|
-
STDOUT.flush
|
198
|
-
end
|
199
|
-
|
200
|
-
# Called when the request has completed
|
201
|
-
def on_request_complete
|
202
|
-
@state == :finished ? close : @state = :finished
|
203
|
-
end
|
204
|
-
|
205
|
-
# called by close
|
206
|
-
def on_close
|
207
|
-
if @state != :finished and @state == :body
|
208
|
-
on_request_complete
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
# Called when an error occurs dispatching the request
|
213
|
-
def on_error(reason)
|
214
|
-
close
|
215
|
-
raise RuntimeError, reason
|
216
|
-
end
|
217
|
-
|
218
|
-
#########
|
219
|
-
protected
|
220
|
-
#########
|
221
|
-
|
222
|
-
#
|
223
|
-
# Coolio callbacks
|
224
|
-
#
|
225
|
-
|
226
|
-
def on_connect
|
227
|
-
@connected = true
|
228
|
-
send_request if @method and @path
|
229
|
-
end
|
230
|
-
|
231
|
-
def on_read(data)
|
232
|
-
@data << data
|
233
|
-
dispatch
|
234
|
-
end
|
235
|
-
|
236
|
-
#
|
237
|
-
# Request sending
|
238
|
-
#
|
239
|
-
|
240
|
-
def send_request
|
241
|
-
send_request_header
|
242
|
-
send_request_body
|
243
|
-
end
|
244
|
-
|
245
|
-
def send_request_header
|
246
|
-
query = @options[:query]
|
247
|
-
head = @options[:head] ? munge_header_keys(@options[:head]) : {}
|
248
|
-
cookies = @options[:cookies]
|
249
|
-
body = @options[:body]
|
250
|
-
|
251
|
-
# Set the Host header if it hasn't been specified already
|
252
|
-
head['host'] ||= encode_host
|
253
|
-
|
254
|
-
# Set the Content-Length if it hasn't been specified already and a body was given
|
255
|
-
head['content-length'] ||= body ? body.length : 0
|
256
|
-
|
257
|
-
# Set the User-Agent if it hasn't been specified
|
258
|
-
head['user-agent'] ||= "Coolio #{Coolio::VERSION}"
|
259
|
-
|
260
|
-
# Default to Connection: close
|
261
|
-
head['connection'] ||= 'close'
|
262
|
-
|
263
|
-
# Build the request
|
264
|
-
request_header = encode_request(@method, @path, query)
|
265
|
-
request_header << encode_headers(head)
|
266
|
-
request_header << encode_cookies(cookies) if cookies
|
267
|
-
request_header << CRLF
|
268
|
-
|
269
|
-
write request_header
|
270
|
-
end
|
271
|
-
|
272
|
-
def send_request_body
|
273
|
-
write @options[:body] if @options[:body]
|
274
|
-
end
|
275
|
-
|
276
|
-
#
|
277
|
-
# Response processing
|
278
|
-
#
|
279
|
-
|
280
|
-
def dispatch
|
281
|
-
while enabled? and case @state
|
282
|
-
when :response_header
|
283
|
-
parse_response_header
|
284
|
-
when :chunk_header
|
285
|
-
parse_chunk_header
|
286
|
-
when :chunk_body
|
287
|
-
process_chunk_body
|
288
|
-
when :chunk_footer
|
289
|
-
process_chunk_footer
|
290
|
-
when :response_footer
|
291
|
-
process_response_footer
|
292
|
-
when :body
|
293
|
-
process_body
|
294
|
-
when :finished, :invalid
|
295
|
-
break
|
296
|
-
else raise RuntimeError, "invalid state: #{@state}"
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
def parse_header(header)
|
302
|
-
return false if @data.empty?
|
303
|
-
|
304
|
-
begin
|
305
|
-
@parser_nbytes = @parser.execute(header, @data.to_str, @parser_nbytes)
|
306
|
-
rescue Coolio::HttpClientParserError
|
307
|
-
on_error "invalid HTTP format, parsing fails"
|
308
|
-
@state = :invalid
|
309
|
-
end
|
310
|
-
|
311
|
-
return false unless @parser.finished?
|
312
|
-
|
313
|
-
# Clear parsed data from the buffer
|
314
|
-
@data.read(@parser_nbytes)
|
315
|
-
@parser.reset
|
316
|
-
@parser_nbytes = 0
|
317
|
-
|
318
|
-
true
|
319
|
-
end
|
320
|
-
|
321
|
-
def parse_response_header
|
322
|
-
return false unless parse_header(@response_header)
|
323
|
-
|
324
|
-
unless @response_header.http_status and @response_header.http_reason
|
325
|
-
on_error "no HTTP response"
|
326
|
-
@state = :invalid
|
327
|
-
return false
|
328
|
-
end
|
329
|
-
|
330
|
-
on_response_header(@response_header)
|
331
|
-
|
332
|
-
if @response_header.chunked_encoding?
|
333
|
-
@state = :chunk_header
|
334
|
-
else
|
335
|
-
@state = :body
|
336
|
-
@bytes_remaining = @response_header.content_length
|
337
|
-
end
|
338
|
-
|
339
|
-
true
|
340
|
-
end
|
341
|
-
|
342
|
-
def parse_chunk_header
|
343
|
-
return false unless parse_header(@chunk_header)
|
344
|
-
|
345
|
-
@bytes_remaining = @chunk_header.chunk_size
|
346
|
-
@chunk_header = HttpChunkHeader.new
|
347
|
-
|
348
|
-
@state = @bytes_remaining > 0 ? :chunk_body : :response_footer
|
349
|
-
true
|
350
|
-
end
|
351
|
-
|
352
|
-
def process_chunk_body
|
353
|
-
if @data.size < @bytes_remaining
|
354
|
-
@bytes_remaining -= @data.size
|
355
|
-
on_body_data @data.read
|
356
|
-
return false
|
357
|
-
end
|
358
|
-
|
359
|
-
on_body_data @data.read(@bytes_remaining)
|
360
|
-
@bytes_remaining = 0
|
361
|
-
|
362
|
-
@state = :chunk_footer
|
363
|
-
true
|
364
|
-
end
|
365
|
-
|
366
|
-
def process_chunk_footer
|
367
|
-
return false if @data.size < 2
|
368
|
-
|
369
|
-
if @data.read(2) == CRLF
|
370
|
-
@state = :chunk_header
|
371
|
-
else
|
372
|
-
on_error "non-CRLF chunk footer"
|
373
|
-
@state = :invalid
|
374
|
-
end
|
375
|
-
|
376
|
-
true
|
377
|
-
end
|
378
|
-
|
379
|
-
def process_response_footer
|
380
|
-
return false if @data.size < 2
|
381
|
-
|
382
|
-
if @data.read(2) == CRLF
|
383
|
-
if @data.empty?
|
384
|
-
on_request_complete
|
385
|
-
@state = :finished
|
386
|
-
else
|
387
|
-
on_error "garbage at end of chunked response"
|
388
|
-
@state = :invalid
|
389
|
-
end
|
390
|
-
else
|
391
|
-
on_error "non-CRLF response footer"
|
392
|
-
@state = :invalid
|
393
|
-
end
|
394
|
-
|
395
|
-
false
|
396
|
-
end
|
397
|
-
|
398
|
-
def process_body
|
399
|
-
if @bytes_remaining.nil?
|
400
|
-
on_body_data @data.read
|
401
|
-
return false
|
402
|
-
end
|
403
|
-
|
404
|
-
if @bytes_remaining.zero?
|
405
|
-
on_request_complete
|
406
|
-
@state = :finished
|
407
|
-
return false
|
408
|
-
end
|
409
|
-
|
410
|
-
if @data.size < @bytes_remaining
|
411
|
-
@bytes_remaining -= @data.size
|
412
|
-
on_body_data @data.read
|
413
|
-
return false
|
414
|
-
end
|
415
|
-
|
416
|
-
on_body_data @data.read(@bytes_remaining)
|
417
|
-
@bytes_remaining = 0
|
418
|
-
|
419
|
-
if @data.empty?
|
420
|
-
on_request_complete
|
421
|
-
@state = :finished
|
422
|
-
else
|
423
|
-
on_error "garbage at end of body"
|
424
|
-
@state = :invalid
|
425
|
-
end
|
426
|
-
|
427
|
-
false
|
428
|
-
end
|
429
|
-
end
|
430
|
-
end
|