ftw 0.0.13 → 0.0.14
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/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
|