net-http-server 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|