ftw 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|