net-http-server 0.1.0

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,3 @@
1
+ -
2
+ ChangeLog.*
3
+ LICENSE.txt
File without changes
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
@@ -0,0 +1 @@
1
+ --markup markdown --title "net-http-server Documentation" --protected
@@ -0,0 +1,8 @@
1
+ ### 0.1.0 / 2011-01-26
2
+
3
+ * Initial release:
4
+ * Added {Net::HTTP::Server::Parser}.
5
+ * Added {Net::HTTP::Server::Requests}.
6
+ * Added {Net::HTTP::Server::Responses}.
7
+ * Added {Net::HTTP::Server::Daemon}.
8
+ * Added {Rack::Handler::HTTP}.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Hal Brodigan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,48 @@
1
+ # net-http-server
2
+
3
+ * [Homepage](http://github.com/postmodern/net-http-server)
4
+ * [Issues](http://github.com/postmodern/net-http-server/issues)
5
+ * [Documentation](http://rubydoc.info/gems/net-http-server)
6
+ * Postmodern (postmodern.mod3 at gmail.com)
7
+
8
+ ## Description
9
+
10
+ {Net::HTTP::Server} is a pure Ruby HTTP server.
11
+
12
+ ## Features
13
+
14
+ * Pure Ruby.
15
+ * Provides a [Rack](http://rack.rubyforge.org/) Handler.
16
+
17
+ ## Examples
18
+
19
+ Simple HTTP Server:
20
+
21
+ require 'net/http/server'
22
+ require 'pp'
23
+
24
+ Net::HTTP::Server.run(:port => 8080) do |request,socket|
25
+ pp request
26
+
27
+ [200, {'Content-Type' => 'text/html'}, ['Hello World']]
28
+ end
29
+
30
+ Use it with Rack:
31
+
32
+ require 'rack/handler/http'
33
+
34
+ Rack::Handler::HTTP.run app
35
+
36
+ ## Requirements
37
+
38
+ * [parslet](http://rubygems.org/gems/parslet) ~> 1.0
39
+
40
+ ## Install
41
+
42
+ $ gem install net-http-server
43
+
44
+ ## Copyright
45
+
46
+ Copyright (c) 2011 Hal Brodigan
47
+
48
+ See {file:LICENSE.txt} for details.
@@ -0,0 +1,36 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ gem 'ore-tasks', '~> 0.3.0'
6
+ require 'ore/tasks'
7
+
8
+ Ore::Tasks.new
9
+ rescue LoadError => e
10
+ STDERR.puts e.message
11
+ STDERR.puts "Run `gem install ore-tasks` to install 'ore/tasks'."
12
+ end
13
+
14
+ begin
15
+ gem 'rspec', '~> 2.4.0'
16
+ require 'rspec/core/rake_task'
17
+
18
+ RSpec::Core::RakeTask.new
19
+ rescue LoadError => e
20
+ task :spec do
21
+ abort "Please run `gem install rspec` to install RSpec."
22
+ end
23
+ end
24
+ task :test => :spec
25
+ task :default => :spec
26
+
27
+ begin
28
+ gem 'yard', '~> 0.6.0'
29
+ require 'yard'
30
+
31
+ YARD::Rake::YardocTask.new
32
+ rescue LoadError => e
33
+ task :yard do
34
+ abort "Please run `gem install yard` to install YARD."
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ name: net-http-server
2
+ summary: A pure Ruby HTTP Server
3
+ description: A Rack compatible pure Ruby HTTP Server.
4
+ license: MIT
5
+ authors: Postmodern
6
+ email: postmodern.mod3@gmail.com
7
+ homepage: http://github.com/postmodern/net-http-server
8
+ has_yard: true
9
+
10
+ dependencies:
11
+ parslet: ~> 1.0
12
+
13
+ development_dependencies:
14
+ ore-tasks: ~> 0.3.0
15
+ rspec: ~> 2.4.0
16
+ yard: ~> 0.6.0
@@ -0,0 +1,4 @@
1
+ require 'net/http/server/parser'
2
+ require 'net/http/server/daemon'
3
+ require 'net/http/server/server'
4
+ require 'net/http/server/version'
@@ -0,0 +1,123 @@
1
+ require 'net/http/server/parser'
2
+ require 'net/http/server/requests'
3
+ require 'net/http/server/responses'
4
+
5
+ require 'net/protocol'
6
+ require 'gserver'
7
+
8
+ module Net
9
+ class HTTP < Protocol
10
+ module Server
11
+ class Daemon < GServer
12
+
13
+ include Requests
14
+ include Responses
15
+
16
+ # Default host to bind to.
17
+ DEFAULT_HOST = '0.0.0.0'
18
+
19
+ # Default port to listen on.
20
+ DEFAULT_PORT = 8080
21
+
22
+ # Maximum number of simultaneous connections.
23
+ MAX_CONNECTIONS = 256
24
+
25
+ # Creates a new HTTP Daemon.
26
+ #
27
+ # @param [Hash] options
28
+ # Options for the daemon.
29
+ #
30
+ # @option options [String] :host (DEFAULT_HOST)
31
+ # The host to run on.
32
+ #
33
+ # @option options [String] :port (DEFAULT_PORT)
34
+ # The port to listen on.
35
+ #
36
+ # @option options [Integer] :max_connections (MAX_CONNECTIONS)
37
+ # The maximum number of simultaneous connections.
38
+ #
39
+ # @option options [IO] :log (STDERR)
40
+ # The log to write errors to.
41
+ #
42
+ # @option options [#call] :handler
43
+ # The HTTP Request Handler object.
44
+ #
45
+ # @yield [request, socket]
46
+ # If a block is given, it will be used to process HTTP Requests.
47
+ #
48
+ # @yieldparam [Hash{Symbol => String,Array,Hash}] request
49
+ # The HTTP Request.
50
+ #
51
+ # @yieldparam [TCPSocket] socket
52
+ # The TCP socket of the client.
53
+ #
54
+ def initialize(options={},&block)
55
+ host = options.fetch(:host,DEFAULT_HOST)
56
+ port = options.fetch(:port,DEFAULT_PORT).to_i
57
+ max_connections = options.fetch(:max_connections,MAX_CONNECTIONS)
58
+ log = options.fetch(:log,STDERR)
59
+
60
+ super(port,host,max_connections,log,false,true)
61
+
62
+ handler(options[:handler],&block)
63
+ end
64
+
65
+ #
66
+ # Sets the HTTP Request Handler.
67
+ #
68
+ # @param [#call, nil] object
69
+ # The HTTP Request Handler object.
70
+ #
71
+ # @yield [request, socket]
72
+ # If a block is given, it will be used to process HTTP Requests.
73
+ #
74
+ # @yieldparam [Hash{Symbol => String,Array,Hash}] request
75
+ # The HTTP Request.
76
+ #
77
+ # @yieldparam [TCPSocket] socket
78
+ # The TCP socket of the client.
79
+ #
80
+ # @raise [ArgumentError]
81
+ # The HTTP Request Handler must respond to `#call`.
82
+ #
83
+ def handler(object=nil,&block)
84
+ if object
85
+ unless object.respond_to?(:call)
86
+ raise(ArgumentError,"HTTP Request Handler must respond to #call")
87
+ end
88
+ elsif block.nil?
89
+ raise(ArgumentError,"no HTTP Request Handler block given")
90
+ end
91
+
92
+ @handler = (object || block)
93
+ end
94
+
95
+ #
96
+ # Receives HTTP Requests and handles them.
97
+ #
98
+ # @param [TCPSocket] socket
99
+ # A new TCP connection.
100
+ #
101
+ def serve(socket)
102
+ if (raw_request = read_request(socket))
103
+ parser = Parser.new
104
+
105
+ begin
106
+ request = parser.parse(raw_request)
107
+ rescue Parslet::ParseFailed => error
108
+ return Responses::BAD_REQUEST
109
+ end
110
+
111
+ normalize_request(request)
112
+
113
+ # rack compliant
114
+ status, headers, body = @handler.call(request,socket)
115
+
116
+ write_response(socket,status,headers,body)
117
+ end
118
+ end
119
+
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,151 @@
1
+ require 'net/protocol'
2
+ require 'parslet'
3
+
4
+ module Net
5
+ class HTTP < Protocol
6
+ module Server
7
+ #
8
+ # Inspired by:
9
+ #
10
+ # * [Thin](https://github.com/macournoyer/thin/blob/master/ext/thin_parser/common.rl)
11
+ # * [Unicorn](https://github.com/defunkt/unicorn/blob/master/ext/unicorn_http/unicorn_http_common.rl)
12
+ # * [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616.html)
13
+ #
14
+ class Parser < Parslet::Parser
15
+
16
+ #
17
+ # Character Classes
18
+ #
19
+ rule(:digit) { match['0-9'] }
20
+ rule(:digits) { digit.repeat(1) }
21
+ rule(:xdigit) { digit | match['a-fA-F'] }
22
+ rule(:upper) { match['A-Z'] }
23
+ rule(:lower) { match['a-z'] }
24
+ rule(:alpha) { upper | lower }
25
+ rule(:alnum) { alpha | digit }
26
+ rule(:cntrl) { match['\x00-\x1f'] }
27
+ rule(:ascii) { match['\x00-\x7f'] }
28
+
29
+ rule(:lws) { match[" \t"] }
30
+ rule(:crlf) { str("\r\n") }
31
+
32
+ rule(:ctl) { cntrl | str("\x7f") }
33
+ rule(:text) { lws | (ctl.absnt? >> ascii) }
34
+
35
+ rule(:safe) { charset('$', '-', '_', '.') }
36
+ rule(:extra) { charset('!', '*', "'", '(', ')', ',') }
37
+ rule(:reserved) { charset(';', '/', '?', ':', '@', '&', '=', '+') }
38
+ rule(:sorta_safe) { charset('"', '<', '>') }
39
+
40
+ rule(:unsafe) { ctl | charset(' ', '#', '%') | sorta_safe }
41
+ rule(:national) {
42
+ (alpha | digit | reserved | extra | safe | unsafe).absnt? >> any
43
+ }
44
+
45
+ rule(:unreserved) { alpha | digit | safe | extra | national }
46
+ rule(:escape) { str("%u").maybe >> xdigit >> xdigit }
47
+ rule(:uchar) { unreserved | escape | sorta_safe }
48
+ rule(:pchar) { uchar | charset(':', '@', '&', '=', '+') }
49
+ rule(:separators) {
50
+ lws | charset(
51
+ '(', ')', '<', '>', '@', ',', ';', ':', "\\", '"', '/', '[', ']',
52
+ '?', '=', '{', '}'
53
+ )
54
+ }
55
+
56
+ #
57
+ # Elements
58
+ #
59
+ rule(:token) { (ctl | separators).absnt? >> ascii }
60
+
61
+ rule(:comment_text) { (str('(') | str(')')).absnt? >> text }
62
+ rule(:comment) { str('(') >> comment_text.repeat >> str(')') }
63
+
64
+ rule(:quoted_pair) { str("\\") >> ascii }
65
+ rule(:quoted_text) { quoted_pair | str('"').absnt? >> text }
66
+ rule(:quoted_string) { str('"') >> quoted_text >> str('"') }
67
+
68
+ #
69
+ # URI Elements
70
+ #
71
+ rule(:scheme) {
72
+ (alpha | digit | charset('+', '-', '.')).repeat
73
+ }
74
+ rule(:host_name) {
75
+ (alnum | charset('-', '_', '.')).repeat(1)
76
+ }
77
+ rule(:user_info) {
78
+ (
79
+ unreserved | escape | charset(';', ':', '&', '=', '+')
80
+ ).repeat(1)
81
+ }
82
+
83
+ rule(:path) { pchar.repeat(1) >> (str('/') >> pchar.repeat).repeat }
84
+ rule(:query_string) { (uchar | reserved).repeat }
85
+ rule(:param) { (pchar | str('/')).repeat }
86
+ rule(:params) { param >> (str(';') >> param).repeat }
87
+ rule(:frag) { (uchar | reserved).repeat }
88
+
89
+ rule(:relative_path) {
90
+ path.maybe.as(:path) >>
91
+ (str(';') >> params.as(:params)).maybe >>
92
+ (str('?') >> query_string.as(:query)).maybe >>
93
+ (str('#') >> frag.as(:fragment)).maybe
94
+ }
95
+ rule(:absolute_path) { str('/').repeat(1) >> relative_path }
96
+
97
+ rule(:absolute_uri) {
98
+ scheme.as(:scheme) >> str(':') >> str('//').maybe >>
99
+ (user_info.as(:user_info) >> str('@')).maybe >>
100
+ host_name.as(:host) >>
101
+ (str(':') >> digits.as(:port)).maybe >>
102
+ absolute_path
103
+ }
104
+
105
+ rule(:request_uri) { str('*') | absolute_uri | absolute_path }
106
+
107
+ #
108
+ # HTTP Elements
109
+ #
110
+ rule(:request_method) { upper.repeat(1,20) | token.repeat(1) }
111
+
112
+ rule(:version_number) { digits >> str('.') >> digits }
113
+ rule(:http_version) { str('HTTP/') >> version_number.as(:version) }
114
+ rule(:request_line) {
115
+ request_method.as(:method) >>
116
+ str(' ') >> request_uri.as(:uri) >>
117
+ str(' ') >> http_version
118
+ }
119
+
120
+ rule(:header_name) { (str(':').absnt? >> token).repeat(1) }
121
+ rule(:header_value) {
122
+ (text | token | separators | quoted_string).repeat(1)
123
+ }
124
+
125
+ rule(:header) {
126
+ header_name.as(:name) >> str(':') >> lws.repeat(1) >>
127
+ header_value.as(:value) >> crlf
128
+ }
129
+ rule(:request) {
130
+ request_line >> crlf >>
131
+ header.repeat.as(:headers) >> crlf
132
+ }
133
+
134
+ root :request
135
+
136
+ protected
137
+
138
+ #
139
+ # Creates a matcher for the given characters.
140
+ #
141
+ # @param [Array<String>] chars
142
+ # The characters to match.
143
+ #
144
+ def charset(*chars)
145
+ match[chars.map { |c| Regexp.escape(c) }.join]
146
+ end
147
+
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,129 @@
1
+ require 'net/protocol'
2
+
3
+ module Net
4
+ class HTTP < Protocol
5
+ module Server
6
+ module Requests
7
+ # Default ports for common URI schemes
8
+ DEFAULT_PORTS = {
9
+ 'https' => 443,
10
+ 'http' => 80
11
+ }
12
+
13
+ protected
14
+
15
+ #
16
+ # Reads a HTTP Request from the stream.
17
+ #
18
+ # @param [IO] stream
19
+ # The stream to read from.
20
+ #
21
+ # @return [String, nil]
22
+ # The raw HTTP Request or `nil` if the Request was malformed.
23
+ #
24
+ def read_request(stream)
25
+ buffer = ''
26
+
27
+ request_line = stream.readline("\r\n")
28
+
29
+ # the request line must contain 'HTTP/'
30
+ return unless request_line.include?('HTTP/')
31
+
32
+ buffer << request_line
33
+
34
+ stream.each_line("\r\n") do |header|
35
+ buffer << header
36
+
37
+ # a header line must contain a ':' character followed by
38
+ # linear-white-space (either ' ' or "\t").
39
+ unless (header.include?(': ') || header.include?(":\t"))
40
+ # if this is not a header line, check if it is the end
41
+ # of the request
42
+ if header == "\r\n"
43
+ # end of the request
44
+ break
45
+ else
46
+ # invalid header line
47
+ return
48
+ end
49
+ end
50
+ end
51
+
52
+ return buffer
53
+ end
54
+
55
+ #
56
+ # Normalizes the `:uri` part of the request.
57
+ #
58
+ # @param [Hash] request
59
+ # The unnormalized HTTP request.
60
+ #
61
+ def normalize_uri(request)
62
+ uri = request[:uri]
63
+
64
+ if uri.kind_of?(Hash)
65
+ if uri[:scheme]
66
+ uri[:port] = unless uri[:port]
67
+ DEFAULT_PORTS[uri[:scheme]]
68
+ else
69
+ uri[:port].to_i
70
+ end
71
+ end
72
+
73
+ unless uri[:path]
74
+ uri[:path] = '/'
75
+ else
76
+ uri[:path].insert(0,'/')
77
+ end
78
+ elsif uri == '*'
79
+ request[:uri] = {}
80
+ end
81
+ end
82
+
83
+ #
84
+ # Normalizes the `:headers` part of the request.
85
+ #
86
+ # @param [Hash] request
87
+ # The unnormalized HTTP request.
88
+ #
89
+ def normalize_headers(request)
90
+ headers = request[:headers]
91
+ normalized_headers = {}
92
+
93
+ unless headers.empty?
94
+ headers.each do |header|
95
+ name = header[:name]
96
+ value = header[:value]
97
+
98
+ if normalized_headers.has_key?(name)
99
+ previous_value = normalized_headers[name]
100
+
101
+ if previous_value.kind_of?(Array)
102
+ previous_value << value
103
+ else
104
+ normalized_headers[name] = [previous_value, value]
105
+ end
106
+ else
107
+ normalized_headers[name] = value
108
+ end
109
+ end
110
+ end
111
+
112
+ request[:headers] = normalized_headers
113
+ end
114
+
115
+ #
116
+ # Normalizes a HTTP request.
117
+ #
118
+ # @param [Hash] request
119
+ # The unnormalized HTTP request.
120
+ #
121
+ def normalize_request(request)
122
+ normalize_uri(request)
123
+ normalize_headers(request)
124
+ end
125
+
126
+ end
127
+ end
128
+ end
129
+ end