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 CHANGED
@@ -3,3 +3,4 @@ require "ftw/connection"
3
3
  require "ftw/dns"
4
4
  require "ftw/version"
5
5
  require "ftw/server"
6
+ require "ftw/webserver"
@@ -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.read_body { |chunk| }
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
@@ -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[header]
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] = header
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.is_a?(IO)
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
@@ -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
- public(:read_http_message)
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
@@ -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
- if body.is_a?(String)
78
- connection.write(body)
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
@@ -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, :read_body)
101
+ :error?, :status_line)
162
102
  end # class FTW::Response
163
103
 
@@ -3,5 +3,5 @@ require "ftw/namespace"
3
3
  # :nodoc:
4
4
  module FTW
5
5
  # The version of this library
6
- VERSION = "0.0.13"
6
+ VERSION = "0.0.14"
7
7
  end
@@ -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
@@ -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)
@@ -202,9 +202,14 @@ class Rack::Handler::FTW
202
202
  end
203
203
  response.body = body
204
204
 
205
- connection.write(response.to_s + CRLF)
206
- body.each do |chunk|
207
- connection.write(chunk)
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.13
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-03 00:00:00.000000000 Z
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