ftw 0.0.1
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 +49 -0
- data/lib/ftw/agent.rb +40 -0
- data/lib/ftw/connection.rb +231 -0
- data/lib/ftw/crlf.rb +6 -0
- data/lib/ftw/dns.rb +62 -0
- data/lib/ftw/http/headers.rb +122 -0
- data/lib/ftw/http/message.rb +92 -0
- data/lib/ftw/namespace.rb +3 -0
- data/lib/ftw/request.rb +102 -0
- data/lib/ftw/version.rb +5 -0
- data/lib/net-ftw.rb +1 -0
- data/lib/net/ftw.rb +5 -0
- data/lib/net/ftw/agent.rb +10 -0
- data/lib/net/ftw/connection.rb +296 -0
- data/lib/net/ftw/connection2.rb +247 -0
- data/lib/net/ftw/crlf.rb +6 -0
- data/lib/net/ftw/dns.rb +57 -0
- data/lib/net/ftw/http.rb +2 -0
- data/lib/net/ftw/http/client.rb +116 -0
- data/lib/net/ftw/http/client2.rb +80 -0
- data/lib/net/ftw/http/connection.rb +42 -0
- data/lib/net/ftw/http/headers.rb +122 -0
- data/lib/net/ftw/http/machine.rb +38 -0
- data/lib/net/ftw/http/message.rb +91 -0
- data/lib/net/ftw/http/request.rb +80 -0
- data/lib/net/ftw/http/response.rb +80 -0
- data/lib/net/ftw/http/server.rb +5 -0
- data/lib/net/ftw/machine.rb +59 -0
- data/lib/net/ftw/namespace.rb +6 -0
- data/lib/net/ftw/protocol/tls.rb +12 -0
- data/lib/net/ftw/websocket.rb +139 -0
- data/test/net/ftw/crlf.rb +12 -0
- data/test/net/ftw/http/dns.rb +6 -0
- data/test/net/ftw/http/headers.rb +50 -0
- data/test/testing.rb +23 -0
- metadata +82 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require "net/ftw/namespace"
|
2
|
+
require "net/ftw/connection"
|
3
|
+
|
4
|
+
class Net::FTW::HTTP::Connection < Net::FTW::Connection
|
5
|
+
HEADERS_COMPLETE = :headers_complete
|
6
|
+
MESSAGE_BODY = :message_body
|
7
|
+
|
8
|
+
def run
|
9
|
+
# TODO(sissel): Implement retries on certain failures like DNS, connect
|
10
|
+
# timeouts, or connection resets?
|
11
|
+
# TODO(sissel): use HTTPS if the uri.scheme == "https"
|
12
|
+
# TODO(sissel): Resolve the hostname
|
13
|
+
# TODO(sissel): Start a new connection, or reuse an existing one.
|
14
|
+
#
|
15
|
+
# TODO(sissel): This suff belongs in a new class, like HTTP::Connection or something.
|
16
|
+
parser = HTTP::Parser.new
|
17
|
+
|
18
|
+
# Only parse the header of the response
|
19
|
+
state = :headers
|
20
|
+
parser.on_headers_complete = proc { state = :body; :stop }
|
21
|
+
|
22
|
+
on(DATA) do |data|
|
23
|
+
# TODO(sissel): Implement this better. Should be able to swap out the
|
24
|
+
# DATA handler at run-time
|
25
|
+
if state == :headers
|
26
|
+
offset = parser << data
|
27
|
+
if state == :body
|
28
|
+
# headers done parsing.
|
29
|
+
version = "#{parser.http_major}.#{parser.http_minor}".to_f
|
30
|
+
trigger(HEADERS_COMPLETE, version, parser.status_code, parser.headers)
|
31
|
+
|
32
|
+
# Re-call 'data' with the remaining non-header portion of data.
|
33
|
+
trigger(DATA, data[offset..-1])
|
34
|
+
end
|
35
|
+
else
|
36
|
+
trigger(MESSAGE_BODY, data)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
super()
|
41
|
+
end # def run
|
42
|
+
end # class Net::FTW::HTTP::Connection
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require "net/ftw/namespace"
|
2
|
+
require "net/ftw/crlf"
|
3
|
+
|
4
|
+
# HTTP Headers
|
5
|
+
#
|
6
|
+
# See RFC2616 section 4.2: <http://tools.ietf.org/html/rfc2616#section-4.2>
|
7
|
+
#
|
8
|
+
# Section 14.44 says Field Names in the header are case-insensitive, so
|
9
|
+
# this library always forces field names to be lowercase. This includes
|
10
|
+
# get() calls.
|
11
|
+
#
|
12
|
+
# headers.set("HELLO", "world")
|
13
|
+
# headers.get("hello") # ===> "world"
|
14
|
+
#
|
15
|
+
class Net::FTW::HTTP::Headers
|
16
|
+
include Enumerable
|
17
|
+
include Net::FTW::CRLF
|
18
|
+
|
19
|
+
# Make a new headers container. You can pass a hash of
|
20
|
+
public
|
21
|
+
def initialize(headers={})
|
22
|
+
super()
|
23
|
+
@version = 1.1
|
24
|
+
@headers = headers
|
25
|
+
end # def initialize
|
26
|
+
|
27
|
+
# Set a header field to a specific value.
|
28
|
+
# Any existing value(s) for this field are destroyed.
|
29
|
+
def set(field, value)
|
30
|
+
@headers[field.downcase] = value
|
31
|
+
end # def set
|
32
|
+
|
33
|
+
# Set a header field to a specific value.
|
34
|
+
# Any existing value(s) for this field are destroyed.
|
35
|
+
def include?(field)
|
36
|
+
@headers.include?(field.downcase)
|
37
|
+
end # def include?
|
38
|
+
|
39
|
+
# Add a header field with a value.
|
40
|
+
#
|
41
|
+
# If this field already exists, another value is added.
|
42
|
+
# If this field does not already exist, it is set.
|
43
|
+
def add(field, value)
|
44
|
+
field = field.downcase
|
45
|
+
if @headers.include?(field)
|
46
|
+
if @headers[field].is_a?(Array)
|
47
|
+
@headers[field] << value
|
48
|
+
else
|
49
|
+
@headers[field] = [@headers[field], value]
|
50
|
+
end
|
51
|
+
else
|
52
|
+
set(field, value)
|
53
|
+
end
|
54
|
+
end # def add
|
55
|
+
|
56
|
+
# Removes a header entry. If the header has multiple values
|
57
|
+
# (like X-Forwarded-For can), you can delete a specific entry
|
58
|
+
# by passing the value of the header field to remove.
|
59
|
+
#
|
60
|
+
# # Remove all X-Forwarded-For entries
|
61
|
+
# headers.remove("X-Forwarded-For")
|
62
|
+
# # Remove a specific X-Forwarded-For entry
|
63
|
+
# headers.remove("X-Forwarded-For", "1.2.3.4")
|
64
|
+
#
|
65
|
+
# * If you remove a field that doesn't exist, no error will occur.
|
66
|
+
# * If you remove a field value that doesn't exist, no error will occur.
|
67
|
+
# * If you remove a field value that is the only value, it is the same as
|
68
|
+
# removing that field by name.
|
69
|
+
def remove(field, value=nil)
|
70
|
+
field = field.downcase
|
71
|
+
if value.nil?
|
72
|
+
# no value, given, remove the entire field.
|
73
|
+
@headers.delete(field)
|
74
|
+
else
|
75
|
+
field_value = @headers[field]
|
76
|
+
if field_value.is_a?(Array)
|
77
|
+
# remove a specific value
|
78
|
+
field_value.delete(value)
|
79
|
+
# Down to a String again if there's only one value.
|
80
|
+
if field_value.size == 1
|
81
|
+
set(field, field_value.first)
|
82
|
+
end
|
83
|
+
else
|
84
|
+
# Remove this field if the value matches
|
85
|
+
if field_value == value
|
86
|
+
remove(field)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end # def remove
|
91
|
+
|
92
|
+
# Get a field value.
|
93
|
+
#
|
94
|
+
# This will return:
|
95
|
+
# * String if there is only a single value for this field
|
96
|
+
# * Array of String if there are multiple values for this field
|
97
|
+
def get(field)
|
98
|
+
field = field.downcase
|
99
|
+
return @headers[field]
|
100
|
+
end # def get
|
101
|
+
|
102
|
+
# Iterate over headers. Given to the block are two arguments, the field name
|
103
|
+
# and the field value. For fields with multiple values, you will receive
|
104
|
+
# that same field name multiple times, like:
|
105
|
+
# yield "Host", "www.example.com"
|
106
|
+
# yield "X-Forwarded-For", "1.2.3.4"
|
107
|
+
# yield "X-Forwarded-For", "1.2.3.5"
|
108
|
+
def each(&block)
|
109
|
+
@headers.each do |field_name, field_value|
|
110
|
+
if field_value.is_a?(Array)
|
111
|
+
field_value.map { |value| yield field_name, v }
|
112
|
+
else
|
113
|
+
yield field_name, field_value
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end # end each
|
117
|
+
|
118
|
+
public
|
119
|
+
def to_s
|
120
|
+
return @headers.collect { |name, value| "#{name}: #{value}" }.join(CRLF) + CRLF
|
121
|
+
end # def to_s
|
122
|
+
end # class Net::FTW::HTTP::Request < Message
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "net/ftw/namespace"
|
2
|
+
require "net/ftw/machine"
|
3
|
+
require "http/parser" # gem http_parser.rb
|
4
|
+
|
5
|
+
class Net::FTW::HTTP::Machine
|
6
|
+
# States
|
7
|
+
HEADERS = :headers
|
8
|
+
MESSAGE = :message
|
9
|
+
|
10
|
+
# Valid transitions
|
11
|
+
TRANSITIONS = {
|
12
|
+
START => HEADERS
|
13
|
+
HEADERS => [MESSAGE, ERROR]
|
14
|
+
MESSAGE => [START, ERROR]
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super
|
19
|
+
transition(HEADERS)
|
20
|
+
@parser = HTTP::Parser.new
|
21
|
+
@parser.on_headers_complete = proc { transition(MESSAGE) }
|
22
|
+
end # def initialize
|
23
|
+
|
24
|
+
def state_headers(data)
|
25
|
+
offset = parser << data
|
26
|
+
if state?(MESSAGE)
|
27
|
+
# We finished headers and transitioned to message body.
|
28
|
+
yield version, parser.status_code, parser.headers
|
29
|
+
|
30
|
+
# Re-feed any body part we were fed that wasn't part of the headers
|
31
|
+
feed(data[offset..-1])
|
32
|
+
end
|
33
|
+
end # def state_headers
|
34
|
+
|
35
|
+
def state_message(data)
|
36
|
+
yield data
|
37
|
+
end # def state_message
|
38
|
+
end # class Net::FTW::HTTP::Connection
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "net/ftw/namespace"
|
2
|
+
require "net/ftw/http/headers"
|
3
|
+
|
4
|
+
# HTTP Message, RFC2616
|
5
|
+
class Net::FTW::HTTP::Message
|
6
|
+
include Net::FTW::CRLF
|
7
|
+
|
8
|
+
# The HTTP headers. See Net::FTW::HTTP::Headers
|
9
|
+
# RFC2616 5.3 - <http://tools.ietf.org/html/rfc2616#section-5.3>
|
10
|
+
attr_reader :headers
|
11
|
+
|
12
|
+
# The HTTP version. See VALID_VERSIONS for valid versions.
|
13
|
+
# This will always be a Numeric object.
|
14
|
+
# Both Request and Responses have version, so put it in the parent class.
|
15
|
+
attr_accessor :version
|
16
|
+
VALID_VERSIONS = [1.0, 1.1]
|
17
|
+
|
18
|
+
# A new HTTP Message. You probably won't use this class much.
|
19
|
+
# See RFC2616 section 4: <http://tools.ietf.org/html/rfc2616#section-4>
|
20
|
+
# See Request and Response.
|
21
|
+
public
|
22
|
+
def initialize
|
23
|
+
@headers = Net::FTW::HTTP::Headers.new
|
24
|
+
@body = nil
|
25
|
+
end # def initialize
|
26
|
+
|
27
|
+
# get a header value
|
28
|
+
public
|
29
|
+
def [](header)
|
30
|
+
return @headers[header]
|
31
|
+
end # def []
|
32
|
+
|
33
|
+
public
|
34
|
+
def []=(header, value)
|
35
|
+
@headers[header] = header
|
36
|
+
end # def []=
|
37
|
+
|
38
|
+
# See RFC2616 section 4.3: <http://tools.ietf.org/html/rfc2616#section-4.3>
|
39
|
+
public
|
40
|
+
def body=(message_body)
|
41
|
+
# TODO(sissel): if message_body is a string, set Content-Length header
|
42
|
+
# TODO(sissel): if it's an IO object, set Transfer-Encoding to chunked
|
43
|
+
# TODO(sissel): if it responds to each or appears to be Enumerable, then
|
44
|
+
# set Transfer-Encoding to chunked.
|
45
|
+
@body = message_body
|
46
|
+
end # def body=
|
47
|
+
|
48
|
+
public
|
49
|
+
def body
|
50
|
+
# TODO(sissel): verification todos follow...
|
51
|
+
# TODO(sissel): RFC2616 section 4.3 - if there is a message body
|
52
|
+
# then one of "Transfer-Encoding" *or* "Content-Length" MUST be present.
|
53
|
+
# otherwise, if neither header is present, no body is present.
|
54
|
+
# TODO(sissel): Responses to HEAD requests or those with status 1xx, 204,
|
55
|
+
# or 304 MUST NOT have a body. All other requests have a message body,
|
56
|
+
# even if that body is of zero length.
|
57
|
+
return @body
|
58
|
+
end # def body
|
59
|
+
|
60
|
+
# Does this message have a message body?
|
61
|
+
public
|
62
|
+
def body?
|
63
|
+
return @body.nil?
|
64
|
+
end # def body?
|
65
|
+
|
66
|
+
# Set the HTTP version. Must be a valid version. See VALID_VERSIONS.
|
67
|
+
public
|
68
|
+
def version=(ver)
|
69
|
+
# Accept string "1.0" or simply "1", etc.
|
70
|
+
ver = ver.to_f if !ver.is_a?(Float)
|
71
|
+
|
72
|
+
if !VALID_VERSIONS.include?(ver)
|
73
|
+
raise ArgumentError.new("#{self.class.name}#version = #{ver.inspect} is" \
|
74
|
+
"invalid. It must be a number, one of #{VALID_VERSIONS.join(", ")}")
|
75
|
+
end
|
76
|
+
@version = ver
|
77
|
+
end # def version=
|
78
|
+
|
79
|
+
# Serialize this Request according to RFC2616
|
80
|
+
# Note: There is *NO* trailing CRLF. This is intentional.
|
81
|
+
# The RFC defines:
|
82
|
+
# generic-message = start-line
|
83
|
+
# *(message-header CRLF)
|
84
|
+
# CRLF
|
85
|
+
# [ message-body ]
|
86
|
+
# Thus, the CRLF between header and body is not part of the header.
|
87
|
+
public
|
88
|
+
def to_s
|
89
|
+
return [start_line, @headers].join(CRLF)
|
90
|
+
end
|
91
|
+
end # class Net::FTW::HTTP::Message
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "net/ftw/namespace"
|
2
|
+
require "net/ftw/http/message"
|
3
|
+
require "addressable/uri" # gem addressable
|
4
|
+
require "uri" # ruby stdlib
|
5
|
+
require "http/parser" # gem http_parser.rb
|
6
|
+
|
7
|
+
# An HTTP Request.
|
8
|
+
#
|
9
|
+
# See RFC2616 section 5: <http://tools.ietf.org/html/rfc2616#section-5>
|
10
|
+
class Net::FTW::HTTP::Request < Net::FTW::HTTP::Message
|
11
|
+
include Net::FTW::CRLF
|
12
|
+
|
13
|
+
# The http method. Like GET, PUT, POST, etc..
|
14
|
+
# RFC2616 5.1.1 - <http://tools.ietf.org/html/rfc2616#section-5.1.1>
|
15
|
+
#
|
16
|
+
# Warning: this accessor obscures the ruby Kernel#method() method.
|
17
|
+
# I would like to call this 'verb', but my preference is first to adhere to
|
18
|
+
# RFC terminology. Further, ruby's stdlib Net::HTTP calls this 'method' as
|
19
|
+
# well (See Net::HTTPGenericRequest).
|
20
|
+
attr_accessor :method
|
21
|
+
|
22
|
+
# This is the Request-URI. Many people call this the 'path' of the request.
|
23
|
+
# RFC2616 5.1.2 - <http://tools.ietf.org/html/rfc2616#section-5.1.2>
|
24
|
+
attr_accessor :request_uri
|
25
|
+
|
26
|
+
# Lemmings. Everyone else calls Request-URI the 'path' - so I should too.
|
27
|
+
alias_method :path, :request_uri
|
28
|
+
|
29
|
+
public
|
30
|
+
def initialize(uri=nil)
|
31
|
+
super()
|
32
|
+
use_uri(uri) if !uri.nil?
|
33
|
+
@version = 1.1
|
34
|
+
end # def initialize
|
35
|
+
|
36
|
+
public
|
37
|
+
def use_uri(uri)
|
38
|
+
# Convert URI objects to Addressable::URI
|
39
|
+
uri = Addressable::URI.parse(uri.to_s) if uri.is_a?(URI)
|
40
|
+
|
41
|
+
# TODO(sissel): Use normalized versions of these fields?
|
42
|
+
# uri.host
|
43
|
+
# uri.port
|
44
|
+
# uri.scheme
|
45
|
+
# uri.path
|
46
|
+
# uri.password
|
47
|
+
# uri.user
|
48
|
+
@request_uri = uri.path
|
49
|
+
@headers.set("Host", uri.host)
|
50
|
+
|
51
|
+
# TODO(sissel): support authentication
|
52
|
+
end # def use_uri
|
53
|
+
|
54
|
+
# Set the method for this request. Usually something like "GET" or "PUT"
|
55
|
+
# etc. See <http://tools.ietf.org/html/rfc2616#section-5.1.1>
|
56
|
+
public
|
57
|
+
def method=(method)
|
58
|
+
# RFC2616 5.1.1 doesn't say the method has to be uppercase.
|
59
|
+
# It can be any 'token' besides the ones defined in section 5.1.1:
|
60
|
+
# The grammar for 'token' is:
|
61
|
+
# token = 1*<any CHAR except CTLs or separators>
|
62
|
+
# TODO(sissel): support section 5.1.1 properly. Don't upcase, but
|
63
|
+
# maybe upcase things that are defined in 5.1.1 like GET, etc.
|
64
|
+
@method = method.upcase
|
65
|
+
end # def method=
|
66
|
+
|
67
|
+
# Get the request line (first line of the http request)
|
68
|
+
# From the RFC: Request-Line = Method SP Request-URI SP HTTP-Version CRLF
|
69
|
+
#
|
70
|
+
# Note: I skip the trailing CRLF. See the to_s method where it is provided.
|
71
|
+
def request_line
|
72
|
+
return "#{method} #{request_uri} HTTP/#{version}"
|
73
|
+
end # def request_line
|
74
|
+
|
75
|
+
# Define the Message's start_line as request_line
|
76
|
+
alias_method :start_line, :request_line
|
77
|
+
# TODO(sissel): Methods to write:
|
78
|
+
# 1. Parsing a request, use HTTP::Parser from http_parser.rb
|
79
|
+
# 2. Building a request from a URI or Addressable::URI
|
80
|
+
end # class Net::FTW::HTTP::Request < Message
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "net/ftw/namespace"
|
2
|
+
require "net/ftw/http/message"
|
3
|
+
require "http/parser" # gem http_parser.rb
|
4
|
+
|
5
|
+
class Net::FTW::HTTP::Response < Net::FTW::HTTP::Message
|
6
|
+
# The HTTP version number
|
7
|
+
# See RFC2616 section 6.1: <http://tools.ietf.org/html/rfc2616#section-6.1>
|
8
|
+
attr_reader :version
|
9
|
+
|
10
|
+
# The http status code (RFC2616 6.1.1)
|
11
|
+
# See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
|
12
|
+
attr_reader :status
|
13
|
+
|
14
|
+
# The reason phrase (RFC2616 6.1.1)
|
15
|
+
# See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
|
16
|
+
attr_reader :reason
|
17
|
+
|
18
|
+
# Translated from the recommendations listed in RFC2616 section 6.1.1
|
19
|
+
# See RFC2616 section 6.1.1: <http://tools.ietf.org/html/rfc2616#section-6.1.1>
|
20
|
+
STATUS_REASON_MAP = {
|
21
|
+
100 => "Continue",
|
22
|
+
101 => "Switching Protocols",
|
23
|
+
200 => "OK",
|
24
|
+
201 => "Created",
|
25
|
+
202 => "Accepted",
|
26
|
+
203 => "Non-Authoritative Information",
|
27
|
+
204 => "No Content",
|
28
|
+
205 => "Reset Content",
|
29
|
+
206 => "Partial Content",
|
30
|
+
300 => "Multiple Choices",
|
31
|
+
301 => "Moved Permanently",
|
32
|
+
302 => "Found",
|
33
|
+
303 => "See Other",
|
34
|
+
304 => "Not Modified",
|
35
|
+
305 => "Use Proxy",
|
36
|
+
307 => "Temporary Redirect",
|
37
|
+
400 => "Bad Request",
|
38
|
+
401 => "Unauthorized",
|
39
|
+
402 => "Payment Required",
|
40
|
+
403 => "Forbidden",
|
41
|
+
404 => "Not Found",
|
42
|
+
405 => "Method Not Allowed",
|
43
|
+
406 => "Not Acceptable"
|
44
|
+
} # STATUS_REASON_MAP
|
45
|
+
|
46
|
+
public
|
47
|
+
def initialize
|
48
|
+
super
|
49
|
+
@reason = "" # Empty reason string by default. It is not required.
|
50
|
+
end # def initialize
|
51
|
+
|
52
|
+
# Set the status code
|
53
|
+
public
|
54
|
+
def status=(code)
|
55
|
+
code = code.to_i if !code.is_a?(Fixnum)
|
56
|
+
# TODO(sissel): Validate that 'code' is a 3 digit number
|
57
|
+
@status = code
|
58
|
+
|
59
|
+
# Attempt to set the reason if the status code has a known reason
|
60
|
+
# recommendation. If one is not found, default to the current reason.
|
61
|
+
@reason = STATUS_REASON_MAP.fetch(@status, @reason)
|
62
|
+
end # def status=
|
63
|
+
|
64
|
+
# Get the status-line string, like "HTTP/1.0 200 OK"
|
65
|
+
public
|
66
|
+
def status_line
|
67
|
+
# First line is 'Status-Line' from RFC2616 section 6.1
|
68
|
+
# Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
|
69
|
+
# etc...
|
70
|
+
return "HTTP-#{version} #{status} #{reason}"
|
71
|
+
end # def status_line
|
72
|
+
|
73
|
+
# Define the Message's start_line as status_line
|
74
|
+
alias_method :start_line, :status_line
|
75
|
+
|
76
|
+
# TODO(sissel): Methods to write:
|
77
|
+
# 1. Parsing a request, use HTTP::Parser from http_parser.rb
|
78
|
+
# 2. Building a request from a URI or Addressable::URI
|
79
|
+
end # class Net::FTW::HTTP::Response
|
80
|
+
|