jaws 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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.
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
1
+ --color
@@ -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