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 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