ftw 0.0.13 → 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ftw.rb +1 -0
- data/lib/ftw/agent.rb +3 -1
- data/lib/ftw/agent/configuration.rb +6 -2
- data/lib/ftw/http/message.rb +3 -3
- data/lib/ftw/protocol.rb +103 -1
- data/lib/ftw/request.rb +4 -6
- data/lib/ftw/response.rb +3 -63
- data/lib/ftw/version.rb +1 -1
- data/lib/ftw/webserver.rb +105 -0
- data/lib/ftw/websocket/writer.rb +2 -1
- data/lib/rack/handler/ftw.rb +8 -3
- metadata +3 -2
data/lib/ftw.rb
CHANGED
data/lib/ftw/agent.rb
CHANGED
@@ -40,6 +40,8 @@ class FTW::Agent
|
|
40
40
|
require "ftw/agent/configuration"
|
41
41
|
include FTW::Agent::Configuration
|
42
42
|
|
43
|
+
# Thrown when too many redirects are encountered
|
44
|
+
# See also {FTW::Agent::Configuration::REDIRECTION_LIMIT}
|
43
45
|
class TooManyRedirects < StandardError
|
44
46
|
attr_accessor :response
|
45
47
|
def initialize(reason, response)
|
@@ -202,7 +204,7 @@ class FTW::Agent
|
|
202
204
|
# Throw away the body
|
203
205
|
response.body = connection
|
204
206
|
# read_body will consume the body and release this connection
|
205
|
-
response.
|
207
|
+
response.read_http_body { |chunk| }
|
206
208
|
end
|
207
209
|
|
208
210
|
# TODO(sissel): If this response has any cookies, store them in the
|
@@ -1,9 +1,13 @@
|
|
1
1
|
require "ftw"
|
2
2
|
|
3
|
+
# Experimentation with an agent configuration similar to Firefox's about:config
|
3
4
|
module FTW::Agent::Configuration
|
5
|
+
# The config key for setting how many redirects will be followed before
|
6
|
+
# giving up.
|
4
7
|
REDIRECTION_LIMIT = "redirection-limit".freeze
|
5
8
|
|
9
|
+
# Get the configuration hash
|
6
10
|
def configuration
|
7
11
|
return @configuration ||= Hash.new
|
8
|
-
end
|
9
|
-
end
|
12
|
+
end # def configuration
|
13
|
+
end # def FTW::Agent::Configuration
|
data/lib/ftw/http/message.rb
CHANGED
@@ -33,7 +33,7 @@ module FTW::HTTP::Message
|
|
33
33
|
#
|
34
34
|
# @param [String] the name of the field. (case insensitive)
|
35
35
|
def [](field)
|
36
|
-
return @headers[
|
36
|
+
return @headers[field]
|
37
37
|
end # def []
|
38
38
|
|
39
39
|
# Set a header field
|
@@ -41,7 +41,7 @@ module FTW::HTTP::Message
|
|
41
41
|
# @param [String] the name of the field. (case insensitive)
|
42
42
|
# @param [String] the value to set for this field
|
43
43
|
def []=(field, value)
|
44
|
-
@headers[field] =
|
44
|
+
@headers[field] = value
|
45
45
|
end # def []=
|
46
46
|
|
47
47
|
# Set the body of this message
|
@@ -54,7 +54,7 @@ module FTW::HTTP::Message
|
|
54
54
|
# TODO(sissel): if it's an IO object, set Transfer-Encoding to chunked
|
55
55
|
# TODO(sissel): if it responds to each or appears to be Enumerable, then
|
56
56
|
# set Transfer-Encoding to chunked.
|
57
|
-
if message_body.
|
57
|
+
if message_body.respond_to?(:read) or message_body.respond_to?(:each)
|
58
58
|
headers["Transfer-Encoding"] = "chunked"
|
59
59
|
else
|
60
60
|
headers["Content-Length"] = message_body.length
|
data/lib/ftw/protocol.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
require "ftw/namespace"
|
2
|
+
require "ftw/crlf"
|
2
3
|
require "cabin"
|
3
4
|
require "logger"
|
4
5
|
|
5
6
|
# This module provides web protocol handling as a mixin.
|
6
7
|
module FTW::Protocol
|
8
|
+
include FTW::CRLF
|
9
|
+
|
7
10
|
# Read an HTTP message from a given connection
|
8
11
|
#
|
9
12
|
# This method blocks until a full http message header has been consumed
|
@@ -56,5 +59,104 @@ module FTW::Protocol
|
|
56
59
|
end
|
57
60
|
end # def read_http_message
|
58
61
|
|
59
|
-
|
62
|
+
def write_http_body(body, io, chunked=false)
|
63
|
+
if chunked
|
64
|
+
write_http_body_chunked(body, io)
|
65
|
+
else
|
66
|
+
write_http_body_normal(body, io)
|
67
|
+
end
|
68
|
+
end # def write_http_body
|
69
|
+
|
70
|
+
# Encode the given text as in 'chunked' encoding.
|
71
|
+
def encode_chunked(text)
|
72
|
+
return sprintf("%x%s%s%s", text.size, CRLF, text, CRLF)
|
73
|
+
end # def encode_chunked
|
74
|
+
|
75
|
+
def write_http_body_chunked(body, io)
|
76
|
+
if body.is_a?(String)
|
77
|
+
io.write(encode_chunked(body))
|
78
|
+
elsif body.respond_to?(:sysread)
|
79
|
+
true while io.write(encode_chunked(body.sysread(16384)))
|
80
|
+
elsif body.respond_to?(:read)
|
81
|
+
true while io.write(encode_chunked(body.read(16384)))
|
82
|
+
elsif body.respond_to?(:each)
|
83
|
+
body.each { |s| io.write(encode_chunked(s)) }
|
84
|
+
end
|
85
|
+
|
86
|
+
# The terminating chunk is an empty one.
|
87
|
+
io.write(encode_chunked(""))
|
88
|
+
end # def write_http_body_chunked
|
89
|
+
|
90
|
+
def write_http_body_normal(body, io)
|
91
|
+
if body.is_a?(String)
|
92
|
+
io.write(body)
|
93
|
+
elsif body.respond_to?(:read)
|
94
|
+
true while io.write(body.read(16384))
|
95
|
+
elsif body.respond_to?(:each)
|
96
|
+
body.each { |s| io.write(s) }
|
97
|
+
end
|
98
|
+
end # def write_http_body_normal
|
99
|
+
|
100
|
+
# Read the body of this message. The block is called with chunks of the
|
101
|
+
# response as they are read in.
|
102
|
+
#
|
103
|
+
# This method is generally only called by http clients, not servers.
|
104
|
+
def read_http_body(&block)
|
105
|
+
if @body.respond_to?(:read)
|
106
|
+
if headers.include?("Content-Length") and headers["Content-Length"].to_i > 0
|
107
|
+
@logger.debug("Reading body with Content-Length")
|
108
|
+
read_http_body_length(headers["Content-Length"].to_i, &block)
|
109
|
+
elsif headers["Transfer-Encoding"] == "chunked"
|
110
|
+
@logger.debug("Reading body with chunked encoding")
|
111
|
+
read_http_body_chunked(&block)
|
112
|
+
end
|
113
|
+
|
114
|
+
# If this is a poolable resource, release it (like a FTW::Connection)
|
115
|
+
@body.release if @body.respond_to?(:release)
|
116
|
+
elsif !@body.nil?
|
117
|
+
yield @body
|
118
|
+
end
|
119
|
+
end # def read_http_body
|
120
|
+
|
121
|
+
# Old api compat
|
122
|
+
alias_method :read_body, :read_http_body
|
123
|
+
|
124
|
+
# Read the length bytes from the body. Yield each chunk read to the block
|
125
|
+
# given. This method is generally only called by http clients, not servers.
|
126
|
+
def read_http_body_length(length, &block)
|
127
|
+
remaining = length
|
128
|
+
while remaining > 0
|
129
|
+
data = @body.read
|
130
|
+
@logger.debug("Read bytes", :length => data.size)
|
131
|
+
if data.size > remaining
|
132
|
+
# Read too much data, only wanted part of this. Push the rest back.
|
133
|
+
yield data[0..remaining]
|
134
|
+
remaining = 0
|
135
|
+
@body.pushback(data[remaining .. -1]) if remaining < 0
|
136
|
+
else
|
137
|
+
yield data
|
138
|
+
remaining -= data.size
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end # def read_http_body_length
|
142
|
+
|
143
|
+
# This is kind of messed, need to fix it.
|
144
|
+
def read_http_body_chunked(&block)
|
145
|
+
parser = HTTP::Parser.new
|
146
|
+
|
147
|
+
# Fake fill-in the response we've already read into the parser.
|
148
|
+
parser << to_s
|
149
|
+
parser << CRLF
|
150
|
+
parser.on_body = block
|
151
|
+
done = false
|
152
|
+
parser.on_message_complete = proc { done = true }
|
153
|
+
|
154
|
+
while !done # will break on special conditions below
|
155
|
+
data = @body.read
|
156
|
+
offset = parser << data
|
157
|
+
if offset != data.length
|
158
|
+
raise "Parser did not consume all data read?"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end # def read_http_body_chunked
|
60
162
|
end # module FTW::Protocol
|
data/lib/ftw/request.rb
CHANGED
@@ -56,6 +56,7 @@ class FTW::Request
|
|
56
56
|
@protocol = "http"
|
57
57
|
@version = 1.1
|
58
58
|
use_uri(uri) if !uri.nil?
|
59
|
+
@logger = Cabin::Channel.get
|
59
60
|
end # def initialize
|
60
61
|
|
61
62
|
# Execute this request on a given connection: Writes the request, returns a
|
@@ -72,18 +73,15 @@ class FTW::Request
|
|
72
73
|
tries = 3
|
73
74
|
begin
|
74
75
|
connection.write(to_s + CRLF)
|
75
|
-
|
76
76
|
if body?
|
77
|
-
|
78
|
-
|
79
|
-
else
|
80
|
-
connection.write(data) while data = body.read(16384)
|
81
|
-
end
|
77
|
+
write_http_body(body, connection,
|
78
|
+
headers["Transfer-Encoding"] == "chunked")
|
82
79
|
end
|
83
80
|
rescue => e
|
84
81
|
# TODO(sissel): Rescue specific exceptions, not just anything.
|
85
82
|
# Reconnect and retry
|
86
83
|
if tries > 0
|
84
|
+
tries -= 1
|
87
85
|
connection.connect
|
88
86
|
retry
|
89
87
|
else
|
data/lib/ftw/response.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "ftw/namespace"
|
2
|
+
require "ftw/protocol"
|
2
3
|
require "ftw/http/message"
|
3
4
|
require "cabin" # gem cabin
|
4
5
|
require "http/parser" # gem http_parser.rb
|
@@ -8,6 +9,7 @@ require "http/parser" # gem http_parser.rb
|
|
8
9
|
# See RFC2616 section 6: <http://tools.ietf.org/html/rfc2616#section-6>
|
9
10
|
class FTW::Response
|
10
11
|
include FTW::HTTP::Message
|
12
|
+
include FTW::Protocol
|
11
13
|
|
12
14
|
# The http status code (RFC2616 6.1.1)
|
13
15
|
# See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
|
@@ -45,8 +47,6 @@ class FTW::Response
|
|
45
47
|
406 => "Not Acceptable"
|
46
48
|
} # STATUS_REASON_MAP
|
47
49
|
|
48
|
-
attr_accessor :body
|
49
|
-
|
50
50
|
private
|
51
51
|
|
52
52
|
# Create a new Response.
|
@@ -90,66 +90,6 @@ class FTW::Response
|
|
90
90
|
# Define the Message's start_line as status_line
|
91
91
|
alias_method :start_line, :status_line
|
92
92
|
|
93
|
-
# Read the body of this Response. The block is called with chunks of the
|
94
|
-
# response as they are read in.
|
95
|
-
#
|
96
|
-
# This method is generally only called by http clients, not servers.
|
97
|
-
def read_body(&block)
|
98
|
-
if @body.respond_to?(:read)
|
99
|
-
if headers.include?("Content-Length") and headers["Content-Length"].to_i > 0
|
100
|
-
@logger.debug("Reading body with Content-Length")
|
101
|
-
read_body_length(headers["Content-Length"].to_i, &block)
|
102
|
-
elsif headers["Transfer-Encoding"] == "chunked"
|
103
|
-
@logger.debug("Reading body with chunked encoding")
|
104
|
-
read_body_chunked(&block)
|
105
|
-
end
|
106
|
-
|
107
|
-
# If this is a poolable resource, release it (like a FTW::Connection)
|
108
|
-
@body.release if @body.respond_to?(:release)
|
109
|
-
elsif !@body.nil?
|
110
|
-
yield @body
|
111
|
-
end
|
112
|
-
end # def read_body
|
113
|
-
|
114
|
-
# Read the length bytes from the body. Yield each chunk read to the block
|
115
|
-
# given. This method is generally only called by http clients, not servers.
|
116
|
-
def read_body_length(length, &block)
|
117
|
-
remaining = length
|
118
|
-
while remaining > 0
|
119
|
-
data = @body.read
|
120
|
-
@logger.debug("Read bytes", :length => data.size)
|
121
|
-
if data.size > remaining
|
122
|
-
# Read too much data, only wanted part of this. Push the rest back.
|
123
|
-
yield data[0..remaining]
|
124
|
-
remaining = 0
|
125
|
-
@body.pushback(data[remaining .. -1]) if remaining < 0
|
126
|
-
else
|
127
|
-
yield data
|
128
|
-
remaining -= data.size
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end # def read_body_length
|
132
|
-
|
133
|
-
# This is kind of messed, need to fix it.
|
134
|
-
def read_body_chunked(&block)
|
135
|
-
parser = HTTP::Parser.new
|
136
|
-
|
137
|
-
# Fake fill-in the response we've already read into the parser.
|
138
|
-
parser << to_s
|
139
|
-
parser << CRLF
|
140
|
-
parser.on_body = block
|
141
|
-
done = false
|
142
|
-
parser.on_message_complete = proc { done = true }
|
143
|
-
|
144
|
-
while !done # will break on special conditions below
|
145
|
-
data = @body.read
|
146
|
-
offset = parser << data
|
147
|
-
if offset != data.length
|
148
|
-
raise "Parser dis not consume all data read?"
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end # def read_body_chunked
|
152
|
-
|
153
93
|
# Is this Response the result of a successful Upgrade request?
|
154
94
|
def upgrade?
|
155
95
|
return false unless status == 101 # "Switching Protocols"
|
@@ -158,6 +98,6 @@ class FTW::Response
|
|
158
98
|
end # def upgrade?
|
159
99
|
|
160
100
|
public(:status=, :status, :reason, :initialize, :upgrade?, :redirect?,
|
161
|
-
:error?, :status_line
|
101
|
+
:error?, :status_line)
|
162
102
|
end # class FTW::Response
|
163
103
|
|
data/lib/ftw/version.rb
CHANGED
@@ -0,0 +1,105 @@
|
|
1
|
+
require "ftw"
|
2
|
+
require "ftw/protocol"
|
3
|
+
require "ftw/crlf"
|
4
|
+
require "socket"
|
5
|
+
require "cabin"
|
6
|
+
|
7
|
+
# An attempt to invent a simple FTW web server.
|
8
|
+
class FTW::WebServer
|
9
|
+
include FTW::Protocol
|
10
|
+
include FTW::CRLF
|
11
|
+
|
12
|
+
def initialize(host, port, &block)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
@handler = block
|
16
|
+
|
17
|
+
@logger = Cabin::Channel.get
|
18
|
+
@threads = []
|
19
|
+
end # def initialize
|
20
|
+
|
21
|
+
# Run the server.
|
22
|
+
#
|
23
|
+
# Connections are farmed out to threads.
|
24
|
+
def run
|
25
|
+
logger.info("Starting server", :config => @config)
|
26
|
+
@server = FTW::Server.new([@host, @port].join(":"))
|
27
|
+
@server.each_connection do |connection|
|
28
|
+
@threads << Thread.new do
|
29
|
+
handle_connection(connection)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end # def run
|
33
|
+
|
34
|
+
def stop
|
35
|
+
@server.stop unless @server.nil?
|
36
|
+
@threads.each(&:join)
|
37
|
+
end # def stop
|
38
|
+
|
39
|
+
# Handle a new connection.
|
40
|
+
#
|
41
|
+
# This method parses http requests and passes them on to #handle_request
|
42
|
+
#
|
43
|
+
# @param connection The FTW::Connection being handled.
|
44
|
+
def handle_connection(connection)
|
45
|
+
while true
|
46
|
+
begin
|
47
|
+
request = read_http_message(connection)
|
48
|
+
rescue EOFError, Errno::EPIPE, Errno::ECONNRESET, HTTP::Parser::Error, IOError
|
49
|
+
# Connection EOF'd or errored before we finished reading a full HTTP
|
50
|
+
# message, shut it down.
|
51
|
+
break
|
52
|
+
end
|
53
|
+
|
54
|
+
if request["Content-Length"] || request["Transfer-Encoding"]
|
55
|
+
request.body = connection
|
56
|
+
end
|
57
|
+
|
58
|
+
begin
|
59
|
+
handle_request(request, connection)
|
60
|
+
rescue => e
|
61
|
+
puts e.inspect
|
62
|
+
puts e.backtrace
|
63
|
+
raise e
|
64
|
+
end
|
65
|
+
end
|
66
|
+
connection.disconnect("Fun")
|
67
|
+
end # def handle_connection
|
68
|
+
|
69
|
+
# Handle a request. This will set up the rack 'env' and invoke the
|
70
|
+
# application associated with this handler.
|
71
|
+
def handle_request(request, connection)
|
72
|
+
response = FTW::Response.new
|
73
|
+
response.version = request.version
|
74
|
+
response["Connection"] = request.headers["Connection"] || "close"
|
75
|
+
|
76
|
+
# Process this request with the handler
|
77
|
+
@handler.call(request, response, connection)
|
78
|
+
|
79
|
+
# Write the response
|
80
|
+
begin
|
81
|
+
connection.write(response.to_s + CRLF)
|
82
|
+
if response.body?
|
83
|
+
write_http_body(response.body, connection,
|
84
|
+
response["Transfer-Encoding"] == "chunked")
|
85
|
+
end
|
86
|
+
rescue => e
|
87
|
+
@logger.error(e)
|
88
|
+
connection.disconnect(e.inspect)
|
89
|
+
end
|
90
|
+
|
91
|
+
if response["Connection"] == "close" or response["Connection"].nil?
|
92
|
+
connection.disconnect("'Connection' header was close or nil")
|
93
|
+
end
|
94
|
+
end # def handle_request
|
95
|
+
|
96
|
+
# Get the logger.
|
97
|
+
def logger
|
98
|
+
if @logger.nil?
|
99
|
+
@logger = Cabin::Channel.get
|
100
|
+
end
|
101
|
+
return @logger
|
102
|
+
end # def logger
|
103
|
+
|
104
|
+
public(:run, :initialize, :stop)
|
105
|
+
end # class FTW::WebServer
|
data/lib/ftw/websocket/writer.rb
CHANGED
@@ -116,8 +116,9 @@ class FTW::WebSocket::Writer
|
|
116
116
|
end
|
117
117
|
data << (maskbit | lengthbits)
|
118
118
|
pack << "C"
|
119
|
-
end
|
119
|
+
end # def pack_maskbit_and_length
|
120
120
|
|
121
|
+
# Pack the extended length. 16 bits or 64 bits
|
121
122
|
def pack_extended_length(data, pack, length)
|
122
123
|
data << length
|
123
124
|
if length >= (1 << 16)
|
data/lib/rack/handler/ftw.rb
CHANGED
@@ -202,9 +202,14 @@ class Rack::Handler::FTW
|
|
202
202
|
end
|
203
203
|
response.body = body
|
204
204
|
|
205
|
-
|
206
|
-
|
207
|
-
|
205
|
+
begin
|
206
|
+
connection.write(response.to_s + CRLF)
|
207
|
+
body.each do |chunk|
|
208
|
+
connection.write(chunk)
|
209
|
+
end
|
210
|
+
rescue => e
|
211
|
+
@logger.error(e)
|
212
|
+
connection.disconnect(e.inspect)
|
208
213
|
end
|
209
214
|
end # def handle_request
|
210
215
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ftw
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.14
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
@@ -141,6 +141,7 @@ files:
|
|
141
141
|
- lib/ftw/cookies.rb
|
142
142
|
- lib/ftw/singleton.rb
|
143
143
|
- lib/ftw/crlf.rb
|
144
|
+
- lib/ftw/webserver.rb
|
144
145
|
- test/testing.rb
|
145
146
|
- test/docs.rb
|
146
147
|
- test/ftw/http/dns.rb
|