jaws 0.5.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 +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
|