ftw 0.0.1 → 0.0.4
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.
- data/README.md +7 -8
- data/lib/ftw.rb +4 -0
- data/lib/ftw/agent.rb +203 -20
- data/lib/ftw/connection.rb +117 -63
- data/lib/ftw/cookies.rb +87 -0
- data/lib/ftw/crlf.rb +1 -1
- data/lib/ftw/dns.rb +14 -5
- data/lib/ftw/http/headers.rb +15 -1
- data/lib/ftw/http/message.rb +9 -1
- data/lib/ftw/namespace.rb +1 -0
- data/lib/ftw/pool.rb +50 -0
- data/lib/ftw/poolable.rb +19 -0
- data/lib/ftw/request.rb +92 -28
- data/lib/ftw/response.rb +179 -0
- data/lib/ftw/version.rb +1 -1
- data/lib/ftw/websocket.rb +194 -0
- data/lib/ftw/websocket/parser.rb +183 -0
- data/test/all.rb +16 -0
- data/test/ftw/crlf.rb +12 -0
- data/test/ftw/http/dns.rb +6 -0
- data/test/{net/ftw → ftw}/http/headers.rb +5 -5
- data/test/testing.rb +0 -9
- metadata +13 -26
- data/lib/net-ftw.rb +0 -1
- data/lib/net/ftw.rb +0 -5
- data/lib/net/ftw/agent.rb +0 -10
- data/lib/net/ftw/connection.rb +0 -296
- data/lib/net/ftw/connection2.rb +0 -247
- data/lib/net/ftw/crlf.rb +0 -6
- data/lib/net/ftw/dns.rb +0 -57
- data/lib/net/ftw/http.rb +0 -2
- data/lib/net/ftw/http/client.rb +0 -116
- data/lib/net/ftw/http/client2.rb +0 -80
- data/lib/net/ftw/http/connection.rb +0 -42
- data/lib/net/ftw/http/headers.rb +0 -122
- data/lib/net/ftw/http/machine.rb +0 -38
- data/lib/net/ftw/http/message.rb +0 -91
- data/lib/net/ftw/http/request.rb +0 -80
- data/lib/net/ftw/http/response.rb +0 -80
- data/lib/net/ftw/http/server.rb +0 -5
- data/lib/net/ftw/machine.rb +0 -59
- data/lib/net/ftw/namespace.rb +0 -6
- data/lib/net/ftw/protocol/tls.rb +0 -12
- data/lib/net/ftw/websocket.rb +0 -139
- data/test/net/ftw/crlf.rb +0 -12
- data/test/net/ftw/http/dns.rb +0 -6
data/lib/ftw/version.rb
CHANGED
@@ -0,0 +1,194 @@
|
|
1
|
+
require "ftw/namespace"
|
2
|
+
require "openssl"
|
3
|
+
require "base64" # stdlib
|
4
|
+
require "digest/sha1" # stdlib
|
5
|
+
require "cabin"
|
6
|
+
require "ftw/websocket/parser"
|
7
|
+
|
8
|
+
# WebSockets, RFC6455.
|
9
|
+
#
|
10
|
+
# TODO(sissel): Find a comfortable way to make this websocket stuff
|
11
|
+
# both use HTTP::Connection for the HTTP handshake and also be usable
|
12
|
+
# from HTTP::Client
|
13
|
+
# TODO(sissel): Also consider SPDY and the kittens.
|
14
|
+
class FTW::WebSocket
|
15
|
+
include FTW::CRLF
|
16
|
+
include Cabin::Inspectable
|
17
|
+
|
18
|
+
TEXTFRAME = 0x0001
|
19
|
+
|
20
|
+
WEBSOCKET_ACCEPT_UUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
21
|
+
|
22
|
+
# Protocol phases
|
23
|
+
# 1. tcp connect
|
24
|
+
# 2. http handshake (RFC6455 section 4)
|
25
|
+
# 3. websocket protocol
|
26
|
+
|
27
|
+
# Creates a new websocket and fills in the given http request with any
|
28
|
+
# necessary settings.
|
29
|
+
public
|
30
|
+
def initialize(request)
|
31
|
+
@key_nonce = generate_key_nonce
|
32
|
+
@request = request
|
33
|
+
prepare(@request)
|
34
|
+
@parser = FTW::WebSocket::Parser.new
|
35
|
+
end # def initialize
|
36
|
+
|
37
|
+
# Set the connection for this websocket. This is usually invoked by FTW::Agent
|
38
|
+
# after the websocket upgrade and handshake have been successful.
|
39
|
+
#
|
40
|
+
# You probably don't call this yourself.
|
41
|
+
public
|
42
|
+
def connection=(connection)
|
43
|
+
@connection = connection
|
44
|
+
end # def connection=
|
45
|
+
|
46
|
+
# Prepare the request. This sets any required headers and attributes as
|
47
|
+
# specified by RFC6455
|
48
|
+
private
|
49
|
+
def prepare(request)
|
50
|
+
# RFC6455 section 4.1:
|
51
|
+
# "2. The method of the request MUST be GET, and the HTTP version MUST
|
52
|
+
# be at least 1.1."
|
53
|
+
request.method = "GET"
|
54
|
+
request.version = 1.1
|
55
|
+
|
56
|
+
# RFC6455 section 4.2.1 bullet 3
|
57
|
+
request.headers.set("Upgrade", "websocket")
|
58
|
+
# RFC6455 section 4.2.1 bullet 4
|
59
|
+
request.headers.set("Connection", "Upgrade")
|
60
|
+
# RFC6455 section 4.2.1 bullet 5
|
61
|
+
request.headers.set("Sec-WebSocket-Key", @key_nonce)
|
62
|
+
# RFC6455 section 4.2.1 bullet 6
|
63
|
+
request.headers.set("Sec-WebSocket-Version", 13)
|
64
|
+
# RFC6455 section 4.2.1 bullet 7 (optional)
|
65
|
+
# The Origin header is optional for non-browser clients.
|
66
|
+
#request.headers.set("Origin", ...)
|
67
|
+
# RFC6455 section 4.2.1 bullet 8 (optional)
|
68
|
+
#request.headers.set("Sec-Websocket-Protocol", ...)
|
69
|
+
# RFC6455 section 4.2.1 bullet 9 (optional)
|
70
|
+
#request.headers.set("Sec-Websocket-Extensions", ...)
|
71
|
+
# RFC6455 section 4.2.1 bullet 10 (optional)
|
72
|
+
# TODO(sissel): Any other headers like cookies, auth headers, are allowed.
|
73
|
+
end # def prepare
|
74
|
+
|
75
|
+
# Generate a websocket key nonce.
|
76
|
+
private
|
77
|
+
def generate_key_nonce
|
78
|
+
# RFC6455 section 4.1 says:
|
79
|
+
# ---
|
80
|
+
# 7. The request MUST include a header field with the name
|
81
|
+
# |Sec-WebSocket-Key|. The value of this header field MUST be a
|
82
|
+
# nonce consisting of a randomly selected 16-byte value that has
|
83
|
+
# been base64-encoded (see Section 4 of [RFC4648]). The nonce
|
84
|
+
# MUST be selected randomly for each connection.
|
85
|
+
# ---
|
86
|
+
#
|
87
|
+
# It's not totally clear to me how cryptographically strong this random
|
88
|
+
# nonce needs to be, and if it does not need to be strong and it would
|
89
|
+
# benefit users who do not have ruby with openssl enabled, maybe just use
|
90
|
+
# rand() to generate this string.
|
91
|
+
#
|
92
|
+
# Thus, generate a random 16 byte string and encode i with base64.
|
93
|
+
# Array#pack("m") packs with base64 encoding.
|
94
|
+
return Base64.strict_encode64(OpenSSL::Random.random_bytes(16))
|
95
|
+
end # def generate_key_nonce
|
96
|
+
|
97
|
+
# Is this Response acceptable for our WebSocket Upgrade request?
|
98
|
+
public
|
99
|
+
def handshake_ok?(response)
|
100
|
+
# See RFC6455 section 4.2.2
|
101
|
+
return false unless response.status == 101 # "Switching Protocols"
|
102
|
+
return false unless response.headers.get("upgrade") == "websocket"
|
103
|
+
return false unless response.headers.get("connection") == "Upgrade"
|
104
|
+
|
105
|
+
# Now verify Sec-WebSocket-Accept. It should be the SHA-1 of the
|
106
|
+
# Sec-WebSocket-Key (in base64) + WEBSOCKET_ACCEPT_UUID
|
107
|
+
expected = @key_nonce + WEBSOCKET_ACCEPT_UUID
|
108
|
+
expected_hash = Digest::SHA1.base64digest(expected)
|
109
|
+
return false unless response.headers.get("Sec-WebSocket-Accept") == expected_hash
|
110
|
+
|
111
|
+
return true
|
112
|
+
end # def handshake_ok?
|
113
|
+
|
114
|
+
# Iterate over each WebSocket message. This method will run forever unless you
|
115
|
+
# break from it.
|
116
|
+
#
|
117
|
+
# The text payload of each message will be yielded to the block.
|
118
|
+
public
|
119
|
+
def each(&block)
|
120
|
+
loop do
|
121
|
+
payload = @parser.feed(@connection.read)
|
122
|
+
next if payload.nil?
|
123
|
+
yield payload
|
124
|
+
end
|
125
|
+
end # def each
|
126
|
+
|
127
|
+
# Implement masking as described by http://tools.ietf.org/html/rfc6455#section-5.3
|
128
|
+
# Basically, we take a 4-byte random string and use it, round robin, to XOR
|
129
|
+
# every byte. Like so:
|
130
|
+
# message[0] ^ key[0]
|
131
|
+
# message[1] ^ key[1]
|
132
|
+
# message[2] ^ key[2]
|
133
|
+
# message[3] ^ key[3]
|
134
|
+
# message[4] ^ key[0]
|
135
|
+
# ...
|
136
|
+
private
|
137
|
+
def mask(message, key)
|
138
|
+
masked = []
|
139
|
+
mask_bytes = key.unpack("C4")
|
140
|
+
i = 0
|
141
|
+
message.each_byte do |byte|
|
142
|
+
masked << (byte ^ mask_bytes[i % 4])
|
143
|
+
i += 1
|
144
|
+
end
|
145
|
+
return masked.pack("C*")
|
146
|
+
end # def mask
|
147
|
+
|
148
|
+
# Publish a message text.
|
149
|
+
#
|
150
|
+
# This will send a websocket text frame over the connection.
|
151
|
+
public
|
152
|
+
def publish(message)
|
153
|
+
# TODO(sissel): Support server and client modes.
|
154
|
+
# Server MUST NOT mask. Client MUST mask.
|
155
|
+
#
|
156
|
+
# 0 1 2 3
|
157
|
+
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
158
|
+
# +-+-+-+-+-------+-+-------------+-------------------------------+
|
159
|
+
# |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
160
|
+
# |I|S|S|S| (4) |A| (7) | (16/64) |
|
161
|
+
# |N|V|V|V| |S| | (if payload len==126/127) |
|
162
|
+
# | |1|2|3| |K| | |
|
163
|
+
# +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
164
|
+
# | Extended payload length continued, if payload len == 127 |
|
165
|
+
# + - - - - - - - - - - - - - - - +-------------------------------+
|
166
|
+
# | |Masking-key, if MASK set to 1 |
|
167
|
+
# +-------------------------------+-------------------------------+
|
168
|
+
# | Masking-key (continued) | Payload Data |
|
169
|
+
# +-------------------------------- - - - - - - - - - - - - - - - +
|
170
|
+
# : Payload Data continued ... :
|
171
|
+
# + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
172
|
+
# | Payload Data continued ... |
|
173
|
+
# +---------------------------------------------------------------+
|
174
|
+
# TODO(sissel): Support 'fin' flag
|
175
|
+
# Set 'fin' flag and opcode of 'text frame'
|
176
|
+
length = message.length
|
177
|
+
mask_key = [rand(1 << 32)].pack("Q")
|
178
|
+
if message.length >= (1 << 16)
|
179
|
+
pack = "CCSA4A*" # flags+opcode, mask+len, 2-byte len, payload
|
180
|
+
data = [ 0x80 | TEXTFRAME, 0x80 | 126, message.length, mask_key, mask(message, mask_key) ]
|
181
|
+
@connection.write(data.pack(pack))
|
182
|
+
elsif message.length >= (1 << 7)
|
183
|
+
length = 126
|
184
|
+
pack = "CCQA4A*" # flags+opcode, mask+len, 8-byte len, payload
|
185
|
+
data = [ 0x80 | TEXTFRAME, 0x80 | 127, message.length, mask_key, mask(message, mask_key) ]
|
186
|
+
@connection.write(data.pack(pack))
|
187
|
+
else
|
188
|
+
data = [ 0x80 | TEXTFRAME, 0x80 | message.length, mask_key, mask(message, mask_key) ]
|
189
|
+
pack = "CCA4A*" # flags+opcode, mask+len, payload
|
190
|
+
@connection.write(data.pack(pack))
|
191
|
+
end
|
192
|
+
end # def publish
|
193
|
+
end # class FTW::WebSocket
|
194
|
+
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require "ftw/namespace"
|
2
|
+
require "ftw/websocket"
|
3
|
+
|
4
|
+
# This class implements a parser for WebSocket messages over a stream.
|
5
|
+
#
|
6
|
+
# Protocol diagram copied from RFC6455
|
7
|
+
# 0 1 2 3
|
8
|
+
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
9
|
+
# +-+-+-+-+-------+-+-------------+-------------------------------+
|
10
|
+
# |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
11
|
+
# |I|S|S|S| (4) |A| (7) | (16/64) |
|
12
|
+
# |N|V|V|V| |S| | (if payload len==126/127) |
|
13
|
+
# | |1|2|3| |K| | |
|
14
|
+
# +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
15
|
+
# | Extended payload length continued, if payload len == 127 |
|
16
|
+
# + - - - - - - - - - - - - - - - +-------------------------------+
|
17
|
+
# | |Masking-key, if MASK set to 1 |
|
18
|
+
# +-------------------------------+-------------------------------+
|
19
|
+
# | Masking-key (continued) | Payload Data |
|
20
|
+
# +-------------------------------- - - - - - - - - - - - - - - - +
|
21
|
+
# : Payload Data continued ... :
|
22
|
+
# + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
23
|
+
# | Payload Data continued ... |
|
24
|
+
# +---------------------------------------------------------------+
|
25
|
+
class FTW::WebSocket::Parser
|
26
|
+
# XXX: Implement control frames: http://tools.ietf.org/html/rfc6455#section-5.5
|
27
|
+
|
28
|
+
# States are based on the minimal unit of 'byte'
|
29
|
+
STATES = [ :flags_and_opcode, :mask_and_payload_init, :payload_length, :payload ]
|
30
|
+
|
31
|
+
# A new WebSocket protocol parser.
|
32
|
+
def initialize
|
33
|
+
@logger = Cabin::Channel.get($0)
|
34
|
+
@opcode = 0
|
35
|
+
@masking_key = ""
|
36
|
+
@flag_final_payload = 0
|
37
|
+
@flag_mask = 0
|
38
|
+
|
39
|
+
transition(:flags_and_opcode, 1)
|
40
|
+
@buffer = ""
|
41
|
+
@buffer.force_encoding("BINARY")
|
42
|
+
end # def initialize
|
43
|
+
|
44
|
+
# Transition to a specified state and set the next required read length.
|
45
|
+
private
|
46
|
+
def transition(state, next_length)
|
47
|
+
@logger.debug("Transitioning", :transition => state, :nextlen => next_length)
|
48
|
+
@state = state
|
49
|
+
need(next_length)
|
50
|
+
end # def transition
|
51
|
+
|
52
|
+
# Feed data to this parser.
|
53
|
+
#
|
54
|
+
# Currently, it will return the raw payload of websocket messages.
|
55
|
+
# Otherwise, it returns nil if no complete message has yet been consumed.
|
56
|
+
public
|
57
|
+
def feed(data)
|
58
|
+
@buffer << data
|
59
|
+
while have?(@need)
|
60
|
+
value = send(@state)
|
61
|
+
# Return if our state yields a value.
|
62
|
+
return value if !value.nil?
|
63
|
+
#yield value if !value.nil? and block_given?
|
64
|
+
end
|
65
|
+
return nil
|
66
|
+
end # def <<
|
67
|
+
|
68
|
+
# Do we have at least 'length' bytes in the buffer?
|
69
|
+
private
|
70
|
+
def have?(length)
|
71
|
+
return length <= @buffer.size
|
72
|
+
end # def have?
|
73
|
+
|
74
|
+
# Get 'length' string from the buffer.
|
75
|
+
private
|
76
|
+
def get(length=nil)
|
77
|
+
length = @need if length.nil?
|
78
|
+
data = @buffer[0 ... length]
|
79
|
+
@buffer = @buffer[length .. -1]
|
80
|
+
return data
|
81
|
+
end # def get
|
82
|
+
|
83
|
+
# Set the minimum number of bytes we need in the buffer for the next read.
|
84
|
+
private
|
85
|
+
def need(length)
|
86
|
+
@need = length
|
87
|
+
end # def need
|
88
|
+
|
89
|
+
# State: Flags (fin, etc) and Opcode.
|
90
|
+
# See: http://tools.ietf.org/html/rfc6455#section-5.3
|
91
|
+
private
|
92
|
+
def flags_and_opcode
|
93
|
+
# 0
|
94
|
+
# 0 1 2 3 4 5 6 7
|
95
|
+
# +-+-+-+-+-------
|
96
|
+
# |F|R|R|R| opcode
|
97
|
+
# |I|S|S|S| (4)
|
98
|
+
# |N|V|V|V|
|
99
|
+
# | |1|2|3|
|
100
|
+
byte = get.bytes.first
|
101
|
+
@opcode = byte & 0xF # last 4 bites
|
102
|
+
@fin = (byte & 0x80 == 0x80)# first bit
|
103
|
+
|
104
|
+
#p :byte => byte, :bits => byte.to_s(2), :opcode => @opcode, :fin => @fin
|
105
|
+
# mask_and_payload_length has a minimum length
|
106
|
+
# of 1 byte, so start there.
|
107
|
+
transition(:mask_and_payload_init, 1)
|
108
|
+
|
109
|
+
# This state yields no output.
|
110
|
+
return nil
|
111
|
+
end # def flags_and_opcode
|
112
|
+
|
113
|
+
# State: mask_and_payload_init
|
114
|
+
# See: http://tools.ietf.org/html/rfc6455#section-5.2
|
115
|
+
private
|
116
|
+
def mask_and_payload_init
|
117
|
+
byte = get.bytes.first
|
118
|
+
@mask = byte & 0x80 # first bit (msb)
|
119
|
+
@payload_length = byte & 0x7F # remaining bits are the length
|
120
|
+
case @payload_length
|
121
|
+
when 126 # 2 byte, unsigned value is the payload length
|
122
|
+
transition(:extended_payload_length, 2)
|
123
|
+
when 127 # 8 byte, unsigned value is the payload length
|
124
|
+
transition(:extended_payload_length, 8)
|
125
|
+
else
|
126
|
+
# Keep the current payload length, a 7 bit value.
|
127
|
+
# Go to read the payload
|
128
|
+
transition(:payload, @payload_length)
|
129
|
+
end # case @payload_length
|
130
|
+
|
131
|
+
# This state yields no output.
|
132
|
+
return nil
|
133
|
+
end # def mask_and_payload_init
|
134
|
+
|
135
|
+
# State: payload_length
|
136
|
+
# This is the 'extended payload length' with support for both 16
|
137
|
+
# and 64 bit lengths.
|
138
|
+
# See: http://tools.ietf.org/html/rfc6455#section-5.2
|
139
|
+
private
|
140
|
+
def payload_length
|
141
|
+
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
142
|
+
# +-+-+-+-+-------+-+-------------+-------------------------------+
|
143
|
+
# |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
144
|
+
# |I|S|S|S| (4) |A| (7) | (16/64) |
|
145
|
+
# |N|V|V|V| |S| | (if payload len==126/127) |
|
146
|
+
# | |1|2|3| |K| | |
|
147
|
+
# +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
148
|
+
# | Extended payload length continued, if payload len == 127 |
|
149
|
+
# + - - - - - - - - - - - - - - - +-------------------------------+
|
150
|
+
# | |Masking-key, if MASK set to 1 |
|
151
|
+
# +-------------------------------+-------------------------------+
|
152
|
+
data = get
|
153
|
+
case @need
|
154
|
+
when 2
|
155
|
+
@payload_length = data.unpack("S")
|
156
|
+
when 8
|
157
|
+
@payload_length = data.unpack("Q")
|
158
|
+
else
|
159
|
+
raise "Unknown payload_length byte length '#{@need}'"
|
160
|
+
end
|
161
|
+
|
162
|
+
transition(:payload, @payload_length)
|
163
|
+
|
164
|
+
# This state yields no output.
|
165
|
+
return nil
|
166
|
+
end # def payload_length
|
167
|
+
|
168
|
+
# State: payload
|
169
|
+
# Read the full payload and return it.
|
170
|
+
# See: http://tools.ietf.org/html/rfc6455#section-5.3
|
171
|
+
#
|
172
|
+
private
|
173
|
+
def payload
|
174
|
+
# TODO(sissel): Handle massive payload lengths without exceeding memory.
|
175
|
+
# Perhaps if the payload is large (say, larger than 500KB by default),
|
176
|
+
# instead of returning the whole thing, simply return an Enumerable that
|
177
|
+
# yields chunks of the payload. There's no reason to buffer the entire
|
178
|
+
# thing. Have the consumer of this library make that decision.
|
179
|
+
data = get(@need)
|
180
|
+
transition(:flags_and_opcode, 1)
|
181
|
+
return data
|
182
|
+
end # def payload
|
183
|
+
end # class FTW::WebSocket::Parser
|
data/test/all.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "minitest/spec"
|
3
|
+
require "minitest/autorun"
|
4
|
+
|
5
|
+
# Get coverage report
|
6
|
+
require "simplecov"
|
7
|
+
SimpleCov.start
|
8
|
+
|
9
|
+
# Add '../lib' to the require path.
|
10
|
+
$: << File.join(File.dirname(__FILE__), "..", "lib")
|
11
|
+
|
12
|
+
glob = File.join(File.dirname(__FILE__), "ftw", "**", "*.rb")
|
13
|
+
Dir.glob(glob).each do |path|
|
14
|
+
puts "Loading tests from #{path}"
|
15
|
+
require File.expand_path(path)
|
16
|
+
end
|
data/test/ftw/crlf.rb
ADDED
@@ -1,9 +1,9 @@
|
|
1
|
-
require File.join(File.expand_path(__FILE__).sub(/\/
|
2
|
-
require "
|
1
|
+
require File.join(File.expand_path(__FILE__).sub(/\/ftw\/.*/, "/testing"))
|
2
|
+
require "ftw/http/headers"
|
3
3
|
|
4
|
-
describe
|
4
|
+
describe FTW::HTTP::Headers do
|
5
5
|
before do
|
6
|
-
@headers =
|
6
|
+
@headers = FTW::HTTP::Headers.new
|
7
7
|
end
|
8
8
|
|
9
9
|
test "add adds" do
|
@@ -47,4 +47,4 @@ describe Net::FTW::HTTP::Headers do
|
|
47
47
|
@headers.remove("foo", "two")
|
48
48
|
assert_equal("one", @headers.get("foo"))
|
49
49
|
end
|
50
|
-
end # describe
|
50
|
+
end # describe FTW::HTTP::Headers
|
data/test/testing.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "minitest/spec"
|
3
|
-
require "minitest/autorun"
|
4
3
|
|
5
4
|
# Add '../lib' to the require path.
|
6
5
|
$: << File.join(File.dirname(__FILE__), "..", "lib")
|
@@ -13,11 +12,3 @@ class MiniTest::Spec
|
|
13
12
|
alias :test :it
|
14
13
|
end
|
15
14
|
end
|
16
|
-
|
17
|
-
if __FILE__ == $0
|
18
|
-
glob = File.join(File.dirname(__FILE__), "net", "**", "*.rb")
|
19
|
-
Dir.glob(glob).each do |path|
|
20
|
-
puts "Loading tests from #{path}"
|
21
|
-
require File.expand_path(path)
|
22
|
-
end
|
23
|
-
end
|