jaws 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +35 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/jaws/server.rb +324 -0
- data/lib/rack/handler/jaws.rb +13 -0
- data/spec/jaws_spec.rb +63 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +69 -0
- metadata +115 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Graham Batty
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
= JAWS
|
2
|
+
|
3
|
+
Just Another Web Server. Obviously there are already a few in the Ruby eco-system, so I'll just list the design goals:
|
4
|
+
|
5
|
+
* Have a concurrency model that's predictable and easy to understand
|
6
|
+
It doesn't use EventMachine and uses a small and manageable thread pool to serve requests.
|
7
|
+
Specifically, it can handle a concurrency of N with N threads, but doesn't accept any connections
|
8
|
+
above that concurrency. This is so that it doesn't drop connections if it gets overloaded, nor
|
9
|
+
does it advertise a concurrency level (like 950) that in a practical web app is just impossible.
|
10
|
+
* Have a pluggable listen/accept system
|
11
|
+
Things like Swiftiply and CloudBridge, which have unusual means of accepting incoming connections,
|
12
|
+
currently monkey patch mongrel to enable their behaviour. This server allows you to override the
|
13
|
+
standard accept behaviour so these systems can work without fragile object surgery.
|
14
|
+
* Be built for rack right from the start
|
15
|
+
This server talks rack and only rack. It doesn't expect to be used as a standalone server.
|
16
|
+
* Be capable of being run in pure ruby, but provide better performance if possible
|
17
|
+
As things move forward, having a pure ruby implementation is important for enabling people
|
18
|
+
to work on and improve ruby software. This server uses the http_parser gem for http parsing,
|
19
|
+
which provides a first class implementation in ruby and (will eventually) support using a
|
20
|
+
C extension that conforms to the same interface, as set out by the specs in that gem,
|
21
|
+
so that it can achieve the same or better parsing performance as mongrel.
|
22
|
+
|
23
|
+
== Note on Patches/Pull Requests
|
24
|
+
|
25
|
+
* Fork the project.
|
26
|
+
* Make your feature addition or bug fix.
|
27
|
+
* Add tests for it. This is important so I don't break it in a
|
28
|
+
future version unintentionally.
|
29
|
+
* Commit, do not mess with rakefile, version, or history.
|
30
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
31
|
+
* Send me a pull request. Bonus points for topic branches.
|
32
|
+
|
33
|
+
== Copyright
|
34
|
+
|
35
|
+
Copyright (c) 2010 Graham Batty. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "jaws"
|
8
|
+
gem.summary = %Q{Just Another Web Server}
|
9
|
+
gem.description = %Q{A Ruby web server designed to have a predictable and simple concurrency model, and to be capable of running in a pure-ruby environment.}
|
10
|
+
gem.email = "graham@stormbrew.ca"
|
11
|
+
gem.homepage = "http://github.com/stormbrew/jaws"
|
12
|
+
gem.authors = ["Graham Batty"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_dependency "http_parser", ">= 0.1.2"
|
15
|
+
gem.add_dependency "rack", ">= 1.1.0"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'spec/rake/spectask'
|
24
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
32
|
+
spec.rcov = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :spec => :check_dependencies
|
36
|
+
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
require 'rake/rdoctask'
|
40
|
+
Rake::RDocTask.new do |rdoc|
|
41
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "jaws #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.0
|
data/lib/jaws/server.rb
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'http/parser'
|
3
|
+
require 'mutex_m'
|
4
|
+
require 'socket'
|
5
|
+
|
6
|
+
module Jaws
|
7
|
+
def self.decapse_name(name)
|
8
|
+
name.gsub(%r{^([A-Z])}) { $1.downcase }.gsub(%r{([a-z])([A-Z])}) { $1 + "_" + $2.downcase }
|
9
|
+
end
|
10
|
+
def self.encapse_name(name)
|
11
|
+
name.gsub(%r{(^|_)([a-z])}) { $2.upcase }
|
12
|
+
end
|
13
|
+
|
14
|
+
class Server
|
15
|
+
DefaultOptions = {
|
16
|
+
:Host => '0.0.0.0',
|
17
|
+
:Port => 8080,
|
18
|
+
:MaxClients => 20,
|
19
|
+
:SystemCores => nil,
|
20
|
+
:ReadTimeout => 2,
|
21
|
+
}
|
22
|
+
|
23
|
+
# The default values for most of the rack environment variables
|
24
|
+
DefaultRackEnv = {
|
25
|
+
"rack.version" => [1,1],
|
26
|
+
"rack.url_scheme" => "http",
|
27
|
+
"rack.input" => StringIO.new,
|
28
|
+
"rack.errors" => $stderr,
|
29
|
+
"rack.multithread" => true,
|
30
|
+
"rack.multiprocess" => false,
|
31
|
+
"rack.run_once" => false,
|
32
|
+
"SCRIPT_NAME" => "",
|
33
|
+
"PATH_INFO" => "",
|
34
|
+
"QUERY_STRING" => "",
|
35
|
+
"SERVER_SOFTWARE" => "Rack+Jaws",
|
36
|
+
}
|
37
|
+
|
38
|
+
StatusStrings = Rack::Utils::HTTP_STATUS_CODES
|
39
|
+
CodesWithoutBody = Rack::Utils::STATUS_WITH_NO_ENTITY_BODY
|
40
|
+
|
41
|
+
# The host to listen on when run(app) is called. Also set with options[:Host]
|
42
|
+
attr_accessor :host
|
43
|
+
# The port to listen on when run(app) is called. Also set with options[:Port]
|
44
|
+
attr_accessor :port
|
45
|
+
# The maximum number of requests this server should handle concurrently. Also set with options[:MaxClients]
|
46
|
+
# Note that you should set this legitimately to the number of clients you can actually handle and not
|
47
|
+
# some arbitrary high number like with Mongrel. This server will simply not accept more connections
|
48
|
+
# than it can handle, which allows you to run other server instances on other machines to take up the slack.
|
49
|
+
# A really really good rule of thumb for a database driven site is to have it be less than the number
|
50
|
+
# of database connections your (hopefuly properly tuned) database server can handle. If you run
|
51
|
+
# more than one web server machine, the TOTAL max_clients from all those servers should be less than what
|
52
|
+
# the database can handle.
|
53
|
+
attr_accessor :max_clients
|
54
|
+
# The number of cores the system has. This may eventually be used to determine if the process should fork
|
55
|
+
# if it's running on a ruby implementation that doesn't support multiprocessing. If set to nil,
|
56
|
+
# it'll auto-detect, and failing that just assume it shouldn't fork at all. If you want it to never
|
57
|
+
# fork, you should set it to 1 (1 core means 1 process).
|
58
|
+
# Also set with options[:SystemCores]
|
59
|
+
attr_accessor :system_cores
|
60
|
+
# The amount of time, in seconds, the server will wait without input before disconnecting the client.
|
61
|
+
# Also set with options[:Timeout]
|
62
|
+
attr_accessor :read_timeout
|
63
|
+
|
64
|
+
# Initializes a new Jaws server object. Pass it a hash of options (:Host, :Port, :MaxClients, and :SystemCores valid)
|
65
|
+
def initialize(options = DefaultOptions)
|
66
|
+
@options = DefaultOptions.merge(options)
|
67
|
+
self.class::DefaultOptions.each do |k,v|
|
68
|
+
send(:"#{Jaws.decapse_name(k.to_s)}=", @options[k])
|
69
|
+
end
|
70
|
+
self.extend Mutex_m
|
71
|
+
end
|
72
|
+
|
73
|
+
# You can re-implement this in a derived class in order to use a different
|
74
|
+
# mechanism to listen for connections. It should return
|
75
|
+
# an object that responds to accept() by returning an open connection to a
|
76
|
+
# client. It also has to respond to synchronize and yield to the block
|
77
|
+
# given to that method and be thread safe in that block. It must also
|
78
|
+
# respond to close() by immediately terminating any waiting accept() calls
|
79
|
+
# and responding to closed? with true thereafter. Close may be called
|
80
|
+
# from outside the object's synchronize block.
|
81
|
+
def create_listener(options)
|
82
|
+
l = TCPServer.new(@host, @port)
|
83
|
+
# let 10 requests back up for each request we can handle concurrently.
|
84
|
+
# note that this value is often truncated by the OS to numbers like 128
|
85
|
+
# or even 5. You may be able to raise this maximum using sysctl (on BSD/OSX)
|
86
|
+
# or /proc/sys/net/core/somaxconn on linux 2.6.
|
87
|
+
l.listen(@max_clients * 10)
|
88
|
+
l.extend Mutex_m # lock around use of the listener object.
|
89
|
+
return l
|
90
|
+
end
|
91
|
+
protected :create_listener
|
92
|
+
|
93
|
+
# Builds an env object from the information provided. Derived handlers
|
94
|
+
# can override this to provide additional information.
|
95
|
+
def build_env(client, req)
|
96
|
+
rack_env = DefaultRackEnv.dup
|
97
|
+
req.fill_rack_env(rack_env)
|
98
|
+
rack_env["SERVER_PORT"] ||= @port.to_s
|
99
|
+
|
100
|
+
if (rack_env["rack.input"].respond_to? :set_encoding)
|
101
|
+
rack_env["rack.input"].set_encoding "ASCII-8BIT"
|
102
|
+
end
|
103
|
+
|
104
|
+
rack_env["REMOTE_PORT"], rack_env["REMOTE_ADDR"] = Socket::unpack_sockaddr_in(client.getpeername)
|
105
|
+
rack_env["REMOTE_PORT"] &&= rack_env["REMOTE_PORT"].to_s
|
106
|
+
rack_env["SERVER_PROTOCOL"] = "HTTP/" << req.version.join('.')
|
107
|
+
|
108
|
+
return rack_env
|
109
|
+
end
|
110
|
+
protected :build_env
|
111
|
+
|
112
|
+
# Reads from a connection, yielding chunks of data as it goes,
|
113
|
+
# until the connection closes. Once the connection closes, it returns.
|
114
|
+
def chunked_read(io, timeout)
|
115
|
+
begin
|
116
|
+
loop do
|
117
|
+
list = IO.select([io], [], [], @read_timeout)
|
118
|
+
if (list.nil? || list.empty?)
|
119
|
+
# IO.select tells us we timed out by giving us nil,
|
120
|
+
# disconnect the non-talkative client.
|
121
|
+
return
|
122
|
+
end
|
123
|
+
data = io.recv(4096)
|
124
|
+
if (data == "")
|
125
|
+
# If recv returns an empty string, that means the other
|
126
|
+
# end closed the connection (either in response to our
|
127
|
+
# end closing the write pipe or because they just felt
|
128
|
+
# like it) so we close the connection from our end too.
|
129
|
+
return
|
130
|
+
end
|
131
|
+
yield data
|
132
|
+
end
|
133
|
+
ensure
|
134
|
+
io.close if (!io.closed?)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
private :read_timeout
|
138
|
+
|
139
|
+
def process_request(client, req, app)
|
140
|
+
rack_env = build_env(client, req)
|
141
|
+
|
142
|
+
# call the app
|
143
|
+
begin
|
144
|
+
status, headers, body = app.call(rack_env)
|
145
|
+
|
146
|
+
# headers
|
147
|
+
match = %r{^([0-9]{3,3})( +([[:graph:] ]+))?}.match(status.to_s)
|
148
|
+
code = match[1].to_i
|
149
|
+
response = "HTTP/1.1 #{match[1]} #{match[3] || StatusStrings[code] || "Unknown"}\r\n"
|
150
|
+
|
151
|
+
if (!headers["Transfer-Encoding"] || headers["Transfer-Encoding"] == "identity")
|
152
|
+
body_len = headers["Content-Length"] && headers["Content-Length"].to_i
|
153
|
+
if (!body_len)
|
154
|
+
headers["Transfer-Encoding"] = "chunked"
|
155
|
+
end
|
156
|
+
else
|
157
|
+
headers.delete("Content-Length")
|
158
|
+
end
|
159
|
+
|
160
|
+
headers.each do |key, vals|
|
161
|
+
vals.each_line do |val|
|
162
|
+
response << "#{key}: #{val}\r\n"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
response << "\r\n"
|
166
|
+
|
167
|
+
client.write(response)
|
168
|
+
|
169
|
+
# only output a body if the request wants one and the status code
|
170
|
+
# should have one.
|
171
|
+
if (req.method != "HEAD" && !CodesWithoutBody.include?(code))
|
172
|
+
if (body_len)
|
173
|
+
# If the app set a content length, we output that length
|
174
|
+
written = 0
|
175
|
+
body.each do |chunk|
|
176
|
+
remain = body_len - written
|
177
|
+
if (chunk.size > remain)
|
178
|
+
chunk[remain, chunk.size] = ""
|
179
|
+
end
|
180
|
+
client.write(chunk)
|
181
|
+
written += chunk.size
|
182
|
+
if (written >= body_len)
|
183
|
+
break
|
184
|
+
end
|
185
|
+
end
|
186
|
+
if (written < body_len)
|
187
|
+
$stderr.puts("Request gave Content-Length(#{body_len}) but gave less data(#{written}). Aborting connection.")
|
188
|
+
return
|
189
|
+
end
|
190
|
+
else
|
191
|
+
# If the app didn't set a length, we do it chunked.
|
192
|
+
body.each do |chunk|
|
193
|
+
client.write(chunk.size.to_s(16) + "\r\n")
|
194
|
+
client.write(chunk)
|
195
|
+
client.write("\r\n")
|
196
|
+
end
|
197
|
+
client.write("0\r\n")
|
198
|
+
client.write("\r\n")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# if the conditions are right, close the connection
|
203
|
+
if ((req.headers["CONNECTION"] && req.headers["CONNECTION"] =~ /close/) ||
|
204
|
+
(headers["Connection"] && headers["Connection"] =~ /close/) ||
|
205
|
+
(req.version == [1,0]))
|
206
|
+
client.close_write
|
207
|
+
end
|
208
|
+
rescue Errno::EPIPE
|
209
|
+
raise # pass the buck up.
|
210
|
+
rescue Object => e
|
211
|
+
err_str = "<h2>500 Internal Server Error</h2>"
|
212
|
+
err_str << "<p>#{e}: #{e.backtrace.first}</p>"
|
213
|
+
client.write("HTTP/1.1 500 Internal Server Error\r\n")
|
214
|
+
client.write("Connection: close\r\n")
|
215
|
+
client.write("Content-Length: #{err_str.length}\r\n")
|
216
|
+
client.write("Content-Type: text/html\r\n")
|
217
|
+
client.write("\r\n")
|
218
|
+
client.write(err_str)
|
219
|
+
client.close_write
|
220
|
+
return
|
221
|
+
ensure
|
222
|
+
body.close if (body.respond_to? :close)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
private :process_request
|
226
|
+
|
227
|
+
# Accepts a connection from a client and handles requests on it until
|
228
|
+
# the connection closes.
|
229
|
+
def process_client(app)
|
230
|
+
loop do
|
231
|
+
begin
|
232
|
+
client = @listener.synchronize do
|
233
|
+
begin
|
234
|
+
@listener && @listener.accept()
|
235
|
+
rescue => e
|
236
|
+
return # this means we've been turned off, so exit the loop.
|
237
|
+
end
|
238
|
+
end
|
239
|
+
if (!client)
|
240
|
+
return # nil return means we're quitting, exit loop.
|
241
|
+
end
|
242
|
+
|
243
|
+
req = Http::Parser.new()
|
244
|
+
buf = ""
|
245
|
+
chunked_read(client, @timeout) do |data|
|
246
|
+
begin
|
247
|
+
buf << data
|
248
|
+
req.parse!(buf)
|
249
|
+
if (req.done?)
|
250
|
+
process_request(client, req, app)
|
251
|
+
req = Http::Parser.new()
|
252
|
+
if (@listener.closed?)
|
253
|
+
return # ignore any more requests from this client if we're shutting down.
|
254
|
+
end
|
255
|
+
end
|
256
|
+
rescue Http::ParserError => e
|
257
|
+
err_str = "<h2>#{e.code} #{e.message}</h2>"
|
258
|
+
client.write("HTTP/1.1 #{e.code} #{e.message}\r\n")
|
259
|
+
client.write("Connection: close\r\n")
|
260
|
+
client.write("Content-Length: #{err_str.length}\r\n")
|
261
|
+
client.write("Content-Type: text/html\r\n")
|
262
|
+
client.write("\r\n")
|
263
|
+
client.write(err_str)
|
264
|
+
client.close_write
|
265
|
+
end
|
266
|
+
end
|
267
|
+
rescue Errno::EPIPE
|
268
|
+
# do nothing, just let the connection close.
|
269
|
+
rescue Object => e
|
270
|
+
$stderr.puts("Unhandled error #{e}:")
|
271
|
+
e.backtrace.each do |line|
|
272
|
+
$stderr.puts(line)
|
273
|
+
end
|
274
|
+
ensure
|
275
|
+
client.close if (client && !client.closed?)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
private :process_client
|
280
|
+
|
281
|
+
# Runs the application through the configured handler.
|
282
|
+
# Can only be run once at a time. If you try to run it more than
|
283
|
+
# once, the second run will block until the first finishes.
|
284
|
+
def run(app)
|
285
|
+
synchronize do
|
286
|
+
begin
|
287
|
+
@listener = create_listener(@options)
|
288
|
+
if (@max_clients > 1)
|
289
|
+
@master = Thread.current
|
290
|
+
@workers = (0...@max_clients).collect do
|
291
|
+
Thread.new do
|
292
|
+
process_client(app)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
@workers.each do |worker|
|
296
|
+
worker.join
|
297
|
+
end
|
298
|
+
else
|
299
|
+
@master = Thread.current
|
300
|
+
@workers = [Thread.current]
|
301
|
+
process_client(app)
|
302
|
+
end
|
303
|
+
ensure
|
304
|
+
@listener.close if (@listener && !@listener.closed?)
|
305
|
+
@listener = @master = @workers = nil
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def stop()
|
311
|
+
# close the connection, the handler threads will exit
|
312
|
+
# the next time they try to load.
|
313
|
+
# TODO: Make it force them to exit after a timeout.
|
314
|
+
@listener.close if !@listener.closed?
|
315
|
+
end
|
316
|
+
|
317
|
+
def running?
|
318
|
+
!@workers.nil?
|
319
|
+
end
|
320
|
+
def stopped?
|
321
|
+
@workers.nil?
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'jaws/server'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Handler
|
5
|
+
# This class is here for rackup-style automatic server detection.
|
6
|
+
# See Jaws::Server for more details.
|
7
|
+
class Jaws
|
8
|
+
def self.run(app, options = Jaws::Server::DefaultOptions)
|
9
|
+
::Jaws::Server.new(options).run(app)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/spec/jaws_spec.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'jaws/server'
|
4
|
+
require 'rack/lint'
|
5
|
+
|
6
|
+
describe Jaws::Server do
|
7
|
+
include TestRequest::Helpers
|
8
|
+
|
9
|
+
before :all do
|
10
|
+
@server = Jaws::Server.new(:Host => @host='0.0.0.0',:Port => @port=9204)
|
11
|
+
@thread = Thread.new do
|
12
|
+
@server.run(Rack::Lint.new(TestRequest.new))
|
13
|
+
end
|
14
|
+
Thread.pass until @server.running?
|
15
|
+
end
|
16
|
+
|
17
|
+
after :all do
|
18
|
+
@server.stop
|
19
|
+
Thread.pass until @server.stopped?
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should respond to a simple get request" do
|
23
|
+
GET "/"
|
24
|
+
status.should == 200
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should have CGI headers on GET" do
|
28
|
+
GET("/")
|
29
|
+
response["REQUEST_METHOD"].should == "GET"
|
30
|
+
response["SCRIPT_NAME"].should == ''
|
31
|
+
response["PATH_INFO"].should == "/"
|
32
|
+
response["QUERY_STRING"].should == ""
|
33
|
+
response["test.postdata"].should == ""
|
34
|
+
|
35
|
+
GET("/test/foo?quux=1")
|
36
|
+
response["REQUEST_METHOD"].should == "GET"
|
37
|
+
response["SCRIPT_NAME"].should == ''
|
38
|
+
response["REQUEST_URI"].should == "/test/foo?quux=1"
|
39
|
+
response["PATH_INFO"].should == "/test/foo"
|
40
|
+
response["QUERY_STRING"].should == "quux=1"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should have CGI headers on POST" do
|
44
|
+
POST("/", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
|
45
|
+
status.should == 200
|
46
|
+
response["REQUEST_METHOD"].should == "POST"
|
47
|
+
response["REQUEST_URI"].should == "/"
|
48
|
+
response["QUERY_STRING"].should == ""
|
49
|
+
response["HTTP_X_TEST_HEADER"].should == "42"
|
50
|
+
response["test.postdata"].should == "rack-form-data=23"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should support HTTP auth" do
|
54
|
+
GET("/test", {:user => "ruth", :passwd => "secret"})
|
55
|
+
response["HTTP_AUTHORIZATION"].should == "Basic cnV0aDpzZWNyZXQ="
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should set status" do
|
59
|
+
GET("/test?secret")
|
60
|
+
status.should == 403
|
61
|
+
response["rack.url_scheme"].should == "http"
|
62
|
+
end
|
63
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'spec'
|
4
|
+
require 'spec/autorun'
|
5
|
+
require 'yaml'
|
6
|
+
require 'net/http'
|
7
|
+
|
8
|
+
Spec::Runner.configure do |config|
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
# Borrowed from Rack's own specs.
|
13
|
+
class TestRequest
|
14
|
+
def call(env)
|
15
|
+
status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200
|
16
|
+
env["test.postdata"] = env["rack.input"].read
|
17
|
+
body = env.to_yaml
|
18
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
19
|
+
[status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]]
|
20
|
+
end
|
21
|
+
|
22
|
+
module Helpers
|
23
|
+
attr_reader :status, :response
|
24
|
+
|
25
|
+
ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
|
26
|
+
ENV["RUBYOPT"] = "-I#{ROOT}/lib -rubygems"
|
27
|
+
|
28
|
+
def root
|
29
|
+
ROOT
|
30
|
+
end
|
31
|
+
|
32
|
+
def rackup
|
33
|
+
"#{ROOT}/bin/rackup"
|
34
|
+
end
|
35
|
+
|
36
|
+
def GET(path, header={})
|
37
|
+
Net::HTTP.start(@host, @port) { |http|
|
38
|
+
user = header.delete(:user)
|
39
|
+
passwd = header.delete(:passwd)
|
40
|
+
|
41
|
+
get = Net::HTTP::Get.new(path, header)
|
42
|
+
get.basic_auth user, passwd if user && passwd
|
43
|
+
http.request(get) { |response|
|
44
|
+
@status = response.code.to_i
|
45
|
+
begin
|
46
|
+
@response = YAML.load(response.body)
|
47
|
+
rescue ArgumentError
|
48
|
+
@response = nil
|
49
|
+
end
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def POST(path, formdata={}, header={})
|
55
|
+
Net::HTTP.start(@host, @port) { |http|
|
56
|
+
user = header.delete(:user)
|
57
|
+
passwd = header.delete(:passwd)
|
58
|
+
|
59
|
+
post = Net::HTTP::Post.new(path, header)
|
60
|
+
post.form_data = formdata
|
61
|
+
post.basic_auth user, passwd if user && passwd
|
62
|
+
http.request(post) { |response|
|
63
|
+
@status = response.code.to_i
|
64
|
+
@response = YAML.load(response.body)
|
65
|
+
}
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jaws
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
- 0
|
9
|
+
version: 0.5.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Graham Batty
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-16 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 2
|
30
|
+
- 9
|
31
|
+
version: 1.2.9
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: http_parser
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
- 1
|
44
|
+
- 2
|
45
|
+
version: 0.1.2
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rack
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 1
|
57
|
+
- 1
|
58
|
+
- 0
|
59
|
+
version: 1.1.0
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
description: A Ruby web server designed to have a predictable and simple concurrency model, and to be capable of running in a pure-ruby environment.
|
63
|
+
email: graham@stormbrew.ca
|
64
|
+
executables: []
|
65
|
+
|
66
|
+
extensions: []
|
67
|
+
|
68
|
+
extra_rdoc_files:
|
69
|
+
- LICENSE
|
70
|
+
- README.rdoc
|
71
|
+
files:
|
72
|
+
- .document
|
73
|
+
- .gitignore
|
74
|
+
- LICENSE
|
75
|
+
- README.rdoc
|
76
|
+
- Rakefile
|
77
|
+
- VERSION
|
78
|
+
- lib/jaws/server.rb
|
79
|
+
- lib/rack/handler/jaws.rb
|
80
|
+
- spec/jaws_spec.rb
|
81
|
+
- spec/spec.opts
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: http://github.com/stormbrew/jaws
|
85
|
+
licenses: []
|
86
|
+
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options:
|
89
|
+
- --charset=UTF-8
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
segments:
|
97
|
+
- 0
|
98
|
+
version: "0"
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.3.6
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Just Another Web Server
|
113
|
+
test_files:
|
114
|
+
- spec/jaws_spec.rb
|
115
|
+
- spec/spec_helper.rb
|