ftw 0.0.1

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