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.
@@ -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
+