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.
- data/.document +3 -0
- data/.gemtest +0 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +8 -0
- data/LICENSE.txt +20 -0
- data/README.md +48 -0
- data/Rakefile +36 -0
- data/gemspec.yml +16 -0
- data/lib/net/http/server.rb +4 -0
- data/lib/net/http/server/daemon.rb +123 -0
- data/lib/net/http/server/parser.rb +151 -0
- data/lib/net/http/server/requests.rb +129 -0
- data/lib/net/http/server/responses.rb +151 -0
- data/lib/net/http/server/server.rb +49 -0
- data/lib/net/http/server/version.rb +10 -0
- data/lib/rack/handler/http.rb +170 -0
- data/net-http-server.gemspec +10 -0
- data/spec/rack/handler/helpers/test_request.rb +69 -0
- data/spec/rack/handler/http_spec.rb +87 -0
- data/spec/rack/handler/images/image.jpg +0 -0
- data/spec/server/daemon_spec.rb +26 -0
- data/spec/server/parser_spec.rb +90 -0
- data/spec/server/requests_spec.rb +110 -0
- data/spec/server/responses_spec.rb +105 -0
- data/spec/server/server_spec.rb +5 -0
- data/spec/spec_helper.rb +2 -0
- metadata +155 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'net/protocol'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
class HTTP < Protocol
|
6
|
+
module Server
|
7
|
+
module Responses
|
8
|
+
# The supported HTTP Protocol.
|
9
|
+
HTTP_VERSION = '1.1'
|
10
|
+
|
11
|
+
# The known HTTP Status codes and messages
|
12
|
+
HTTP_STATUSES = {
|
13
|
+
# 1xx
|
14
|
+
100 => 'Continue',
|
15
|
+
101 => 'Switching Protocols',
|
16
|
+
102 => 'Processing',
|
17
|
+
# 2xx
|
18
|
+
200 => 'OK',
|
19
|
+
201 => 'Created',
|
20
|
+
202 => 'Accepted',
|
21
|
+
203 => 'Non-Authoritative Information',
|
22
|
+
204 => 'No Content',
|
23
|
+
205 => 'Reset Content',
|
24
|
+
206 => 'Partial Content',
|
25
|
+
# 3xx
|
26
|
+
300 => 'Multiple Choices',
|
27
|
+
301 => 'Moved Permanently',
|
28
|
+
302 => 'Found',
|
29
|
+
303 => 'See Other',
|
30
|
+
304 => 'Not Modified',
|
31
|
+
305 => 'Use Proxy',
|
32
|
+
307 => 'Temporary Redirect',
|
33
|
+
# 4xx
|
34
|
+
400 => 'Bad Request',
|
35
|
+
401 => 'Unauthorized',
|
36
|
+
402 => 'Payment Required',
|
37
|
+
403 => 'Forbidden',
|
38
|
+
404 => 'Not Found',
|
39
|
+
405 => 'Method Not Allowed',
|
40
|
+
406 => 'Not Acceptable',
|
41
|
+
407 => 'Proxy Authentication Required',
|
42
|
+
408 => 'Request Time-out',
|
43
|
+
409 => 'Conflict',
|
44
|
+
410 => 'Gone',
|
45
|
+
411 => 'Length Required',
|
46
|
+
412 => 'Precondition Failed',
|
47
|
+
413 => 'Request Entity Too Large',
|
48
|
+
414 => 'Request-URI Too Large',
|
49
|
+
415 => 'Unsupported Media Type',
|
50
|
+
416 => 'Requested range not satisfiable',
|
51
|
+
417 => 'Expectation Failed',
|
52
|
+
# 5xx
|
53
|
+
500 => 'Internal Server Error',
|
54
|
+
501 => 'Not Implemented',
|
55
|
+
502 => 'Bad Gateway',
|
56
|
+
503 => 'Service Unavailable',
|
57
|
+
504 => 'Gateway Time-out',
|
58
|
+
505 => 'HTTP Version not supported extension-code'
|
59
|
+
}
|
60
|
+
|
61
|
+
# Generic Bad Request response
|
62
|
+
BAD_REQUEST = [400, {}, ['Bad Request']]
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
#
|
67
|
+
# Writes the status of an HTTP Response to a stream.
|
68
|
+
#
|
69
|
+
# @param [IO] stream
|
70
|
+
# The stream to write the headers back to.
|
71
|
+
#
|
72
|
+
# @param [Integer] status
|
73
|
+
# The status of the HTTP Response.
|
74
|
+
#
|
75
|
+
def write_status(stream,status)
|
76
|
+
status = status.to_i
|
77
|
+
|
78
|
+
reason = HTTP_STATUSES[status]
|
79
|
+
stream.write("HTTP/#{HTTP_VERSION} #{status} #{reason}\r\n")
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Write the headers of an HTTP Response to a stream.
|
84
|
+
#
|
85
|
+
# @param [IO] stream
|
86
|
+
# The stream to write the headers back to.
|
87
|
+
#
|
88
|
+
# @param [Hash{String => [String, Time, Array<String>}] headers
|
89
|
+
# The headers of the HTTP Response.
|
90
|
+
#
|
91
|
+
def write_headers(stream,headers)
|
92
|
+
headers.each do |name,values|
|
93
|
+
case values
|
94
|
+
when String
|
95
|
+
values.each_line("\n") do |value|
|
96
|
+
stream.write("#{name}: #{value.chomp}\r\n")
|
97
|
+
end
|
98
|
+
when Time
|
99
|
+
stream.write("#{name}: #{values.httpdate}\r\n")
|
100
|
+
when Array
|
101
|
+
values.each do |value|
|
102
|
+
stream.write("#{name}: #{value}\r\n")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
stream.write("\r\n")
|
108
|
+
stream.flush
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Writes the body of a HTTP Response to a stream.
|
113
|
+
#
|
114
|
+
# @param [IO] stream
|
115
|
+
# The stream to write the headers back to.
|
116
|
+
#
|
117
|
+
# @param [#each] body
|
118
|
+
# The body of the HTTP Response.
|
119
|
+
#
|
120
|
+
def write_body(stream,body)
|
121
|
+
body.each do |chunk|
|
122
|
+
stream.write(chunk)
|
123
|
+
stream.flush
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Writes a HTTP Response to a stream.
|
129
|
+
#
|
130
|
+
# @param [IO] stream
|
131
|
+
# The stream to write the HTTP Response to.
|
132
|
+
#
|
133
|
+
# @param [Integer] status
|
134
|
+
# The status of the HTTP Response.
|
135
|
+
#
|
136
|
+
# @param [Hash{String => [String, Time, Array<String>}] headers
|
137
|
+
# The headers of the HTTP Response.
|
138
|
+
#
|
139
|
+
# @param [#each] body
|
140
|
+
# The body of the HTTP Response.
|
141
|
+
#
|
142
|
+
def write_response(stream,status,headers,body)
|
143
|
+
write_status stream, status
|
144
|
+
write_headers stream, headers
|
145
|
+
write_body stream, body
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'net/http/server/daemon'
|
2
|
+
|
3
|
+
require 'net/protocol'
|
4
|
+
|
5
|
+
module Net
|
6
|
+
class HTTP < Protocol
|
7
|
+
module Server
|
8
|
+
#
|
9
|
+
# Starts the HTTP Server.
|
10
|
+
#
|
11
|
+
# @param [Hash] options
|
12
|
+
# Options for the server.
|
13
|
+
#
|
14
|
+
# @option options [String] :host (DEFAULT_HOST)
|
15
|
+
# The host to run on.
|
16
|
+
#
|
17
|
+
# @option options [String] :port (DEFAULT_PORT)
|
18
|
+
# The port to listen on.
|
19
|
+
#
|
20
|
+
# @option options [Integer] :max_connections (MAX_CONNECTIONS)
|
21
|
+
# The maximum number of simultaneous connections.
|
22
|
+
#
|
23
|
+
# @option options [Boolean] :background (false)
|
24
|
+
# Specifies whether to run the server in the background or
|
25
|
+
# foreground.
|
26
|
+
#
|
27
|
+
# @option options [#call] :handler
|
28
|
+
# The HTTP Request Handler object.
|
29
|
+
#
|
30
|
+
# @yield [request, socket]
|
31
|
+
# If a block is given, it will be used to process HTTP Requests.
|
32
|
+
#
|
33
|
+
# @yieldparam [Hash{Symbol => String,Array,Hash}] request
|
34
|
+
# The HTTP Request.
|
35
|
+
#
|
36
|
+
# @yieldparam [TCPSocket] socket
|
37
|
+
# The TCP socket of the client.
|
38
|
+
#
|
39
|
+
def Server.run(options={},&block)
|
40
|
+
daemon = Daemon.new(options,&block)
|
41
|
+
|
42
|
+
daemon.start
|
43
|
+
daemon.join unless options[:background]
|
44
|
+
return daemon
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'net/http/server/daemon'
|
2
|
+
require 'net/http/server/version'
|
3
|
+
|
4
|
+
require 'rack'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
module Handler
|
9
|
+
#
|
10
|
+
# A Rack handler for {Net::HTTP::Server}.
|
11
|
+
#
|
12
|
+
class HTTP
|
13
|
+
|
14
|
+
# The default environment settings.
|
15
|
+
DEFAULT_ENV = {
|
16
|
+
'rack.version' => Rack::VERSION,
|
17
|
+
'rack.errors' => STDERR,
|
18
|
+
'rack.multithread' => true,
|
19
|
+
'rack.multiprocess' => false,
|
20
|
+
'rack.run_once' => false,
|
21
|
+
'rack.url_scheme' => 'http',
|
22
|
+
|
23
|
+
'SERVER_SOFTWARE' => "Net::HTTP::Server/#{Net::HTTP::Server::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})",
|
24
|
+
'SCRIPT_NAME' => ''
|
25
|
+
}
|
26
|
+
|
27
|
+
# Special HTTP Headers used by Rack::Request
|
28
|
+
SPECIAL_HEADERS = Set[
|
29
|
+
'Content-Type',
|
30
|
+
'Content-Length',
|
31
|
+
]
|
32
|
+
|
33
|
+
#
|
34
|
+
# Initializes the handler.
|
35
|
+
#
|
36
|
+
# @param [#call] app
|
37
|
+
# The application the handler will be passing requests to.
|
38
|
+
#
|
39
|
+
# @param [Hash] options
|
40
|
+
# Additional options.
|
41
|
+
#
|
42
|
+
# @option options [String] :Host
|
43
|
+
# The host to bind to.
|
44
|
+
#
|
45
|
+
# @option options [Integer] :Port
|
46
|
+
# The port to listen on.
|
47
|
+
#
|
48
|
+
def initialize(app,options={})
|
49
|
+
@app = app
|
50
|
+
@options = options
|
51
|
+
|
52
|
+
@server = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Creates a new handler and begins handling HTTP Requests.
|
57
|
+
#
|
58
|
+
# @see #initialize
|
59
|
+
#
|
60
|
+
def self.run(app,options={})
|
61
|
+
new(app,options).run
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Starts {Net::HTTP::Server} and begins handling HTTP Requests.
|
66
|
+
#
|
67
|
+
def run
|
68
|
+
@server = Net::HTTP::Server::Daemon.new(
|
69
|
+
:host => @options[:Host],
|
70
|
+
:port => @options[:Port],
|
71
|
+
:handler => self
|
72
|
+
)
|
73
|
+
|
74
|
+
@server.start
|
75
|
+
@server.join
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Handles an HTTP Request.
|
80
|
+
#
|
81
|
+
# @param [Hash] request
|
82
|
+
# An HTTP Request received from {Net::HTTP::Server}.
|
83
|
+
#
|
84
|
+
# @param [TCPSocket] socket
|
85
|
+
# The socket that the request was received from.
|
86
|
+
#
|
87
|
+
# @return [Array<Integer, Hash, Array>]
|
88
|
+
# The response status, headers and body.
|
89
|
+
#
|
90
|
+
def call(request,socket)
|
91
|
+
request_uri = request[:uri]
|
92
|
+
remote_address = socket.remote_address
|
93
|
+
local_address = socket.local_address
|
94
|
+
|
95
|
+
env = {}
|
96
|
+
|
97
|
+
# add the default values
|
98
|
+
env.merge!(DEFAULT_ENV)
|
99
|
+
|
100
|
+
# populate
|
101
|
+
env['rack.input'] = socket
|
102
|
+
|
103
|
+
if request_uri[:scheme]
|
104
|
+
env['rack.url_scheme'] = request_uri[:scheme]
|
105
|
+
end
|
106
|
+
|
107
|
+
env['SERVER_NAME'] = local_address.getnameinfo[0]
|
108
|
+
env['SERVER_PORT'] = local_address.ip_port.to_s
|
109
|
+
env['SERVER_PROTOCOL'] = "HTTP/#{request[:http_version]}"
|
110
|
+
|
111
|
+
env['REMOTE_ADDR'] = remote_address.ip_address
|
112
|
+
env['REMOTE_PORT'] = remote_address.ip_port.to_s
|
113
|
+
|
114
|
+
env['REQUEST_METHOD'] = request[:method]
|
115
|
+
env['PATH_INFO'] = request_uri.fetch(:path,'*')
|
116
|
+
env['QUERY_STRING'] = request_uri[:query_string].to_s
|
117
|
+
|
118
|
+
# add the headers
|
119
|
+
request[:headers].each do |name,value|
|
120
|
+
key = name.dup
|
121
|
+
|
122
|
+
key.upcase!
|
123
|
+
key.tr!('-','_')
|
124
|
+
|
125
|
+
# if the header is not special, prepend 'HTTP_'
|
126
|
+
unless SPECIAL_HEADERS.include?(name)
|
127
|
+
key.insert(0,'HTTP_')
|
128
|
+
end
|
129
|
+
|
130
|
+
env[key] = case value
|
131
|
+
when Array
|
132
|
+
value.join("\n")
|
133
|
+
else
|
134
|
+
value.to_s
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
@app.call(env)
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Determines if the handler is running.
|
143
|
+
#
|
144
|
+
# @return [Boolean]
|
145
|
+
# Specifies whether the handler is still running.
|
146
|
+
#
|
147
|
+
def running?
|
148
|
+
@server && !(@server.stopped?)
|
149
|
+
end
|
150
|
+
|
151
|
+
#
|
152
|
+
# Determines whether the handler was stopped.
|
153
|
+
#
|
154
|
+
# @return [Boolean]
|
155
|
+
# Specifies whether the handler was previously stopped.
|
156
|
+
#
|
157
|
+
def stopped?
|
158
|
+
@server.nil? || @server.stopped?
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Stops the handler.
|
163
|
+
#
|
164
|
+
def stop
|
165
|
+
@server.stop if @server
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
# Borrowed from Rack's own specs.
|
5
|
+
class TestRequest
|
6
|
+
def call(env)
|
7
|
+
status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200
|
8
|
+
env["test.postdata"] = env["rack.input"].read
|
9
|
+
body = env.to_yaml
|
10
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
11
|
+
[status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]]
|
12
|
+
end
|
13
|
+
|
14
|
+
module Helpers
|
15
|
+
attr_reader :status, :response
|
16
|
+
|
17
|
+
ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
|
18
|
+
ENV["RUBYOPT"] = "-I#{ROOT}/lib -rubygems"
|
19
|
+
|
20
|
+
def root
|
21
|
+
ROOT
|
22
|
+
end
|
23
|
+
|
24
|
+
def rackup
|
25
|
+
"#{ROOT}/bin/rackup"
|
26
|
+
end
|
27
|
+
|
28
|
+
def GET(path, header={})
|
29
|
+
Net::HTTP.start(@host, @port) { |http|
|
30
|
+
user = header.delete(:user)
|
31
|
+
passwd = header.delete(:passwd)
|
32
|
+
|
33
|
+
get = Net::HTTP::Get.new(path, header)
|
34
|
+
get.basic_auth user, passwd if user && passwd
|
35
|
+
http.request(get) { |response|
|
36
|
+
@status = response.code.to_i
|
37
|
+
if response.content_type == "text/yaml"
|
38
|
+
load_yaml(response)
|
39
|
+
else
|
40
|
+
@response = response
|
41
|
+
end
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def POST(path, formdata={}, header={})
|
47
|
+
Net::HTTP.start(@host, @port) { |http|
|
48
|
+
user = header.delete(:user)
|
49
|
+
passwd = header.delete(:passwd)
|
50
|
+
|
51
|
+
post = Net::HTTP::Post.new(path, header)
|
52
|
+
post.form_data = formdata
|
53
|
+
post.basic_auth user, passwd if user && passwd
|
54
|
+
http.request(post) { |response|
|
55
|
+
@status = response.code.to_i
|
56
|
+
@response = YAML.load(response.body)
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_yaml(response)
|
62
|
+
begin
|
63
|
+
@response = YAML.load(response.body)
|
64
|
+
rescue ArgumentError
|
65
|
+
@response = nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|