fishwife 1.1.1-java
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/.gemtest +0 -0
- data/History.rdoc +23 -0
- data/LICENSE +202 -0
- data/Manifest.txt +17 -0
- data/README.rdoc +96 -0
- data/Rakefile +38 -0
- data/bin/fishwife +35 -0
- data/example/config.ru +14 -0
- data/lib/fishwife.rb +29 -0
- data/lib/fishwife/base.rb +19 -0
- data/lib/fishwife/http_server.rb +100 -0
- data/lib/fishwife/rack_servlet.rb +246 -0
- data/lib/rack/handler/fishwife.rb +38 -0
- data/spec/data/hello.txt +1 -0
- data/spec/data/reddit-icon.png +0 -0
- data/spec/fishwife_spec.rb +183 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/test_app.rb +120 -0
- metadata +153 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011 David Kellum
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
5
|
+
# may not use this file except in compliance with the License. You may
|
|
6
|
+
# obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
13
|
+
# implied. See the License for the specific language governing
|
|
14
|
+
# permissions and limitations under the License.
|
|
15
|
+
#++
|
|
16
|
+
|
|
17
|
+
module Fishwife
|
|
18
|
+
VERSION = '1.1.1'
|
|
19
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011 David Kellum
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
5
|
+
# may not use this file except in compliance with the License. You may
|
|
6
|
+
# obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
13
|
+
# implied. See the License for the specific language governing
|
|
14
|
+
# permissions and limitations under the License.
|
|
15
|
+
#++
|
|
16
|
+
|
|
17
|
+
module Fishwife
|
|
18
|
+
class HttpServer < RJack::Jetty::ServerFactory
|
|
19
|
+
|
|
20
|
+
attr_accessor :host
|
|
21
|
+
|
|
22
|
+
# Create the server with specified options:
|
|
23
|
+
#
|
|
24
|
+
# :host::
|
|
25
|
+
# String specifying the IP address to bind to (default: 0.0.0.0)
|
|
26
|
+
#
|
|
27
|
+
# :port::
|
|
28
|
+
# String or integer with the port to bind to (default: 9292).
|
|
29
|
+
# Jetty picks if given port 0 (and port can be read on return
|
|
30
|
+
# from start.)
|
|
31
|
+
#
|
|
32
|
+
# :min_threads::
|
|
33
|
+
# Minimum number of threads to keep in pool (default: 5)
|
|
34
|
+
#
|
|
35
|
+
# :max_threads::
|
|
36
|
+
# Maximum threads to create in pool (default: 50)
|
|
37
|
+
#
|
|
38
|
+
# :max_idle_time_ms::
|
|
39
|
+
# Maximum idle time for a connection in milliseconds (default: 10_000)
|
|
40
|
+
#
|
|
41
|
+
# :request_log_file::
|
|
42
|
+
# Request log to file name or :stderr (default: nil, no log)
|
|
43
|
+
def initialize( options = {} )
|
|
44
|
+
super()
|
|
45
|
+
|
|
46
|
+
@server = nil
|
|
47
|
+
@host = nil
|
|
48
|
+
|
|
49
|
+
self.min_threads = 5
|
|
50
|
+
self.max_threads = 50
|
|
51
|
+
self.port = 9292
|
|
52
|
+
|
|
53
|
+
options = Hash[ options.map { |o| [ o[0].to_s.downcase.to_sym, o[1] ] } ]
|
|
54
|
+
|
|
55
|
+
# Translate option values from possible Strings
|
|
56
|
+
[:port, :min_threads, :max_threads, :max_idle_time_ms].each do |k|
|
|
57
|
+
v = options[k]
|
|
58
|
+
options[k] = v.to_i if v
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
v = options[ :request_log_file ]
|
|
62
|
+
options[ :request_log_file ] = v.to_sym if v == 'stderr'
|
|
63
|
+
|
|
64
|
+
# Apply options as setters
|
|
65
|
+
options.each do |k,v|
|
|
66
|
+
setter = "#{k}=".to_sym
|
|
67
|
+
send( setter, v ) if respond_to?( setter )
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Start the server, given rack app to run
|
|
72
|
+
def start( app )
|
|
73
|
+
set_context_servlets( '/', { '/*' => RackServlet.new( app ) } )
|
|
74
|
+
|
|
75
|
+
@server = create
|
|
76
|
+
@server.start
|
|
77
|
+
# Recover the server port in case 0 was given.
|
|
78
|
+
self.port = @server.connectors[0].local_port
|
|
79
|
+
|
|
80
|
+
@server
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Join with started server so main thread doesn't exit.
|
|
84
|
+
def join
|
|
85
|
+
@server.join if @server
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Stop the server to allow graceful shutdown
|
|
89
|
+
def stop
|
|
90
|
+
@server.stop if @server
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def create_connectors
|
|
94
|
+
super.tap do |ctrs|
|
|
95
|
+
ctrs.first.host = @host if ctrs.first
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011 David Kellum
|
|
3
|
+
# Copyright (c) 2010-2011 Don Werve
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
6
|
+
# may not use this file except in compliance with the License. You may
|
|
7
|
+
# obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
14
|
+
# implied. See the License for the specific language governing
|
|
15
|
+
# permissions and limitations under the License.
|
|
16
|
+
#++
|
|
17
|
+
|
|
18
|
+
#
|
|
19
|
+
# Wraps a Rack application in a Java servlet.
|
|
20
|
+
#
|
|
21
|
+
# Relevant documentation:
|
|
22
|
+
#
|
|
23
|
+
# http://rack.rubyforge.org/doc/SPEC.html
|
|
24
|
+
# http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax
|
|
25
|
+
# /servlet/http/HttpServlet.html
|
|
26
|
+
#
|
|
27
|
+
module Fishwife
|
|
28
|
+
java_import 'javax.servlet.http.HttpServlet'
|
|
29
|
+
|
|
30
|
+
class RackServlet < HttpServlet
|
|
31
|
+
java_import 'java.io.FileInputStream'
|
|
32
|
+
java_import 'org.eclipse.jetty.continuation.ContinuationSupport'
|
|
33
|
+
|
|
34
|
+
def initialize( app )
|
|
35
|
+
super()
|
|
36
|
+
@log = RJack::SLF4J[ self.class ]
|
|
37
|
+
@app = app
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Takes an incoming request (as a Java Servlet) and dispatches it
|
|
41
|
+
# to the rack application setup via [rackup]. All this really
|
|
42
|
+
# involves is translating the various bits of the Servlet API into
|
|
43
|
+
# the Rack API on the way in, and translating the response back on
|
|
44
|
+
# the way out.
|
|
45
|
+
#
|
|
46
|
+
# Also, we implement a common extension to the Rack api for
|
|
47
|
+
# asynchronous request processing. We supply an 'async.callback'
|
|
48
|
+
# parameter in env to the Rack application. If we catch an :async
|
|
49
|
+
# symbol thrown by the app, we initiate a Jetty continuation.
|
|
50
|
+
#
|
|
51
|
+
# When 'async.callback' gets a response with empty headers and an
|
|
52
|
+
# empty body, we declare the async response finished.
|
|
53
|
+
def service(request, response)
|
|
54
|
+
# Turn the ServletRequest into a Rack env hash
|
|
55
|
+
env = servlet_to_rack(request)
|
|
56
|
+
|
|
57
|
+
# Handle asynchronous responses via Servlet continuations.
|
|
58
|
+
continuation = ContinuationSupport.getContinuation(request)
|
|
59
|
+
|
|
60
|
+
# If this is an expired connection, do nothing.
|
|
61
|
+
return if continuation.isExpired
|
|
62
|
+
|
|
63
|
+
# We should never be re-dispatched.
|
|
64
|
+
raise("Request re-dispatched.") unless continuation.isInitial
|
|
65
|
+
|
|
66
|
+
# Add our own special bits to the rack environment so that Rack
|
|
67
|
+
# middleware can have access to the Java internals.
|
|
68
|
+
env['rack.java.servlet'] = true
|
|
69
|
+
env['rack.java.servlet.request'] = request
|
|
70
|
+
env['rack.java.servlet.response'] = response
|
|
71
|
+
env['rack.java.servlet.continuation'] = continuation
|
|
72
|
+
|
|
73
|
+
# Add an callback that can be used to add results to the
|
|
74
|
+
# response asynchronously.
|
|
75
|
+
env['async.callback'] = lambda do |rack_response|
|
|
76
|
+
servlet_response = continuation.getServletResponse
|
|
77
|
+
rack_to_servlet(rack_response, servlet_response) and
|
|
78
|
+
continuation.complete
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Execute the Rack request.
|
|
82
|
+
catch(:async) do
|
|
83
|
+
rack_response = @app.call(env)
|
|
84
|
+
|
|
85
|
+
# For apps that don't throw :async.
|
|
86
|
+
unless(rack_response[0] == -1)
|
|
87
|
+
# Nope, nothing asynchronous here.
|
|
88
|
+
rack_to_servlet(rack_response, response)
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# If we got here, this is a continuation.
|
|
94
|
+
continuation.suspend(response)
|
|
95
|
+
|
|
96
|
+
rescue NativeException => n
|
|
97
|
+
@log.warn( "On service (native): #{n.cause.to_string}" )
|
|
98
|
+
raise n.cause
|
|
99
|
+
rescue Exception => e
|
|
100
|
+
@log.error( "On service: #{e}" )
|
|
101
|
+
raise e
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Turns a Servlet request into a Rack request hash.
|
|
107
|
+
def servlet_to_rack(request)
|
|
108
|
+
# The Rack request that we will pass on.
|
|
109
|
+
env = Hash.new
|
|
110
|
+
|
|
111
|
+
# Map Servlet bits to Rack bits.
|
|
112
|
+
env['REQUEST_METHOD'] = request.getMethod
|
|
113
|
+
qstring = request.getQueryString.to_s #or empty string
|
|
114
|
+
env['QUERY_STRING'] = qstring
|
|
115
|
+
env['SERVER_NAME'] = request.getServerName
|
|
116
|
+
env['SERVER_PORT'] = request.getServerPort.to_s
|
|
117
|
+
env['rack.version'] = Rack::VERSION
|
|
118
|
+
env['rack.url_scheme'] = request.getScheme
|
|
119
|
+
env['HTTP_VERSION'] = request.getProtocol
|
|
120
|
+
env["SERVER_PROTOCOL"] = request.getProtocol
|
|
121
|
+
env['REMOTE_ADDR'] = request.getRemoteAddr
|
|
122
|
+
env['REMOTE_HOST'] = request.getRemoteHost
|
|
123
|
+
|
|
124
|
+
# request.getPathInfo seems to be blank, so we're using the URI.
|
|
125
|
+
env['REQUEST_PATH'] = request.getRequestURI
|
|
126
|
+
env['PATH_INFO'] = request.getRequestURI
|
|
127
|
+
env['SCRIPT_NAME'] = ""
|
|
128
|
+
|
|
129
|
+
# Rack says URI, but it hands off a URL.
|
|
130
|
+
req_uri = request.getRequestURL.toString
|
|
131
|
+
|
|
132
|
+
# Java chops off the query string, but a Rack application will
|
|
133
|
+
# expect it, so we'll add it back if present
|
|
134
|
+
req_uri << '?' << qstring unless qstring.empty?
|
|
135
|
+
env['REQUEST_URI'] = req_uri
|
|
136
|
+
|
|
137
|
+
# CONTENT_TYPE/LENGTH are handled specifically, not in headers.
|
|
138
|
+
ctype = request.getContentType
|
|
139
|
+
env['CONTENT_TYPE'] = ctype if ctype && !ctype.empty?
|
|
140
|
+
clength = request.getContentLength
|
|
141
|
+
env['CONTENT_LENGTH'] = clength.to_s if clength != -1
|
|
142
|
+
|
|
143
|
+
# JRuby is like the matrix, only there's no spoon or fork().
|
|
144
|
+
env['rack.multiprocess'] = false
|
|
145
|
+
env['rack.multithread'] = true
|
|
146
|
+
env['rack.run_once'] = false
|
|
147
|
+
|
|
148
|
+
# Populate the HTTP headers.
|
|
149
|
+
hn = request.getHeaderNames
|
|
150
|
+
if hn.respond_to?( :each )
|
|
151
|
+
hn.each do |header_name|
|
|
152
|
+
header = header_name.upcase.tr('-', '_')
|
|
153
|
+
next if header == 'CONTENT_TYPE' || header == 'CONTENT_LENGTH'
|
|
154
|
+
env[ 'HTTP_' + header ] = request.getHeader( header_name )
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
@log.warn( "Weird headers: [#{ hn.to_s }]" )
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The input stream is a wrapper around the Java InputStream.
|
|
161
|
+
env['rack.input'] = request.getInputStream.to_io
|
|
162
|
+
|
|
163
|
+
# The output stream defaults to stderr.
|
|
164
|
+
env['rack.errors'] ||= $stderr
|
|
165
|
+
|
|
166
|
+
# All done, hand back the Rack request.
|
|
167
|
+
env
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Turns a Rack response into a Servlet response; can be called
|
|
171
|
+
# multiple times. Returns true if this is the full request (either
|
|
172
|
+
# a synchronous request or the last part of an async request),
|
|
173
|
+
# false otherwise.
|
|
174
|
+
#
|
|
175
|
+
# Note that keep-alive *only* happens if we get either a pathname
|
|
176
|
+
# (because we can find the length ourselves), or if we get a
|
|
177
|
+
# Content-Length header as part of the response. While we can
|
|
178
|
+
# readily buffer the response object to figure out how long it is,
|
|
179
|
+
# we have no guarantee that we aren't going to be buffering
|
|
180
|
+
# something *huge*.
|
|
181
|
+
#
|
|
182
|
+
# http://docstore.mik.ua/orelly/java-ent/servlet/ch05_03.htm
|
|
183
|
+
def rack_to_servlet(rack_response, response)
|
|
184
|
+
# Split apart the Rack response.
|
|
185
|
+
status, headers, body = rack_response
|
|
186
|
+
|
|
187
|
+
# We assume the request is finished if we got empty headers,
|
|
188
|
+
# an empty body, and we have a committed response.
|
|
189
|
+
finished = ( headers.empty? and
|
|
190
|
+
body.respond_to?(:empty?) and body.empty?)
|
|
191
|
+
return(true) if (finished and response.isCommitted)
|
|
192
|
+
|
|
193
|
+
# No need to send headers again if we've already shipped data
|
|
194
|
+
# out on an async request.
|
|
195
|
+
unless(response.isCommitted)
|
|
196
|
+
# Set the HTTP status code.
|
|
197
|
+
response.setStatus(status.to_i)
|
|
198
|
+
|
|
199
|
+
# Did we get a Content-Length header?
|
|
200
|
+
content_length = headers.delete('Content-Length')
|
|
201
|
+
response.setContentLength(content_length.to_i) if content_length
|
|
202
|
+
|
|
203
|
+
# Did we get a Content-Type header?
|
|
204
|
+
content_type = headers.delete('Content-Type')
|
|
205
|
+
response.setContentType(content_type) if content_type
|
|
206
|
+
|
|
207
|
+
# Add all the result headers.
|
|
208
|
+
headers.each do |h, v|
|
|
209
|
+
v.split("\n").each { |val| response.addHeader(h, val) }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# How else would we write output?
|
|
214
|
+
output = response.getOutputStream
|
|
215
|
+
|
|
216
|
+
# Turn the body into something nice and Java-y.
|
|
217
|
+
if(body.respond_to?(:to_path))
|
|
218
|
+
# We've been told to serve a file; use FileInputStream to
|
|
219
|
+
# stream the file directly to the servlet, because this is a
|
|
220
|
+
# lot faster than doing it with Ruby.
|
|
221
|
+
file = java.io.File.new(body.to_path)
|
|
222
|
+
|
|
223
|
+
# We set the content-length so we can use Keep-Alive, unless
|
|
224
|
+
# this is an async request.
|
|
225
|
+
response.setContentLength(file.length) unless content_length
|
|
226
|
+
|
|
227
|
+
# Stream the file directly.
|
|
228
|
+
buffer = Java::byte[4096].new
|
|
229
|
+
input_stream = FileInputStream.new(file)
|
|
230
|
+
while((count = input_stream.read(buffer)) != -1)
|
|
231
|
+
output.write(buffer, 0, count)
|
|
232
|
+
end
|
|
233
|
+
input_stream.close
|
|
234
|
+
else
|
|
235
|
+
body.each { |l| output.write(l.to_java_bytes) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Close the body if we're supposed to.
|
|
239
|
+
body.close if body.respond_to?(:close)
|
|
240
|
+
|
|
241
|
+
# All done.
|
|
242
|
+
output.flush
|
|
243
|
+
false
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011 David Kellum
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
5
|
+
# may not use this file except in compliance with the License. You may
|
|
6
|
+
# obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
13
|
+
# implied. See the License for the specific language governing
|
|
14
|
+
# permissions and limitations under the License.
|
|
15
|
+
#++
|
|
16
|
+
|
|
17
|
+
require 'fishwife'
|
|
18
|
+
|
|
19
|
+
module Rack
|
|
20
|
+
module Handler
|
|
21
|
+
# Rack expects Rack::Handler::Fishwife via require 'rack/handler/fishwife'
|
|
22
|
+
class Fishwife
|
|
23
|
+
|
|
24
|
+
# Called by rack to run
|
|
25
|
+
def self.run( app, opts = {} )
|
|
26
|
+
@server = ::Fishwife::HttpServer.new( opts )
|
|
27
|
+
@server.start( app )
|
|
28
|
+
@server.join
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Called by rack
|
|
32
|
+
def self.shutdown
|
|
33
|
+
@server.stop if @server
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/spec/data/hello.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Hello, world!
|
|
Binary file
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011 David Kellum
|
|
3
|
+
# Copyright (c) 2010-2011 Don Werve
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
6
|
+
# may not use this file except in compliance with the License. You may
|
|
7
|
+
# obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
14
|
+
# implied. See the License for the specific language governing
|
|
15
|
+
# permissions and limitations under the License.
|
|
16
|
+
#++
|
|
17
|
+
|
|
18
|
+
require 'spec_helper'
|
|
19
|
+
require 'test_app'
|
|
20
|
+
require 'thread'
|
|
21
|
+
require 'digest/md5'
|
|
22
|
+
require 'base64'
|
|
23
|
+
|
|
24
|
+
describe Fishwife do
|
|
25
|
+
def get(path, headers = {})
|
|
26
|
+
Net::HTTP.start(@options[:host], @options[:port]) do |http|
|
|
27
|
+
request = Net::HTTP::Get.new(path, headers)
|
|
28
|
+
http.request(request)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def post(path, params = nil, headers = {}, body = nil)
|
|
33
|
+
Net::HTTP.start(@options[:host], @options[:port]) do |http|
|
|
34
|
+
request = Net::HTTP::Post.new(path, headers)
|
|
35
|
+
request.form_data = params if params
|
|
36
|
+
request.body = body if body
|
|
37
|
+
http.request(request)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
before(:all) do
|
|
42
|
+
@lock = Mutex.new
|
|
43
|
+
@app = Rack::Lint.new(TestApp.new)
|
|
44
|
+
@options = { :host => '127.0.0.1', :port => 9201 }
|
|
45
|
+
Net::HTTP.version_1_2
|
|
46
|
+
@server = Fishwife::HttpServer.new(@options)
|
|
47
|
+
@server.start(@app)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
after(:all) do
|
|
51
|
+
@server.stop
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "returns 200 OK" do
|
|
55
|
+
response = get("/ping")
|
|
56
|
+
response.code.should == "200"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "returns 403 FORBIDDEN" do
|
|
60
|
+
response = get("/error/403")
|
|
61
|
+
response.code.should == "403"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "returns 404 NOT FOUND" do
|
|
65
|
+
response = get("/jimmy/hoffa")
|
|
66
|
+
response.code.should == "404"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "sets Rack headers" do
|
|
70
|
+
response = get("/echo")
|
|
71
|
+
response.code.should == "200"
|
|
72
|
+
content = YAML.load(response.body)
|
|
73
|
+
content["rack.version"].should == [ 1, 1 ]
|
|
74
|
+
content["rack.multithread"].should be_true
|
|
75
|
+
content["rack.multiprocess"].should be_false
|
|
76
|
+
content["rack.run_once"].should be_false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "passes form variables via GET" do
|
|
80
|
+
response = get("/echo?answer=42")
|
|
81
|
+
response.code.should == "200"
|
|
82
|
+
content = YAML.load(response.body)
|
|
83
|
+
content['request.params']['answer'].should == '42'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "passes form variables via POST" do
|
|
87
|
+
question = "What is the answer to life, the universe, and everything?"
|
|
88
|
+
response = post("/echo", 'question' => question)
|
|
89
|
+
response.code.should == "200"
|
|
90
|
+
content = YAML.load(response.body)
|
|
91
|
+
content['request.params']['question'].should == question
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "passes custom headers" do
|
|
95
|
+
response = get("/echo", "X-My-Header" => "Pancakes")
|
|
96
|
+
response.code.should == "200"
|
|
97
|
+
content = YAML.load(response.body)
|
|
98
|
+
content["HTTP_X_MY_HEADER"].should == "Pancakes"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "returns multiple values of the same header" do
|
|
102
|
+
response = get("/multi_headers")
|
|
103
|
+
response['Warning'].should == "warn-1, warn-2"
|
|
104
|
+
# Net::HTTP handle multiple headers with join( ", " )
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "lets the Rack app know it's running as a servlet" do
|
|
108
|
+
response = get("/echo", 'answer' => '42')
|
|
109
|
+
response.code.should == "200"
|
|
110
|
+
content = YAML.load(response.body)
|
|
111
|
+
content['rack.java.servlet'].should be_true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "is clearly Jetty" do
|
|
115
|
+
response = get("/ping")
|
|
116
|
+
response['server'].should =~ /jetty/i
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "sets the server port and hostname" do
|
|
120
|
+
response = get("/echo")
|
|
121
|
+
content = YAML.load(response.body)
|
|
122
|
+
content["SERVER_PORT"].should == "9201"
|
|
123
|
+
content["SERVER_NAME"].should == "127.0.0.1"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "passes the URI scheme" do
|
|
127
|
+
response = get("/echo")
|
|
128
|
+
content = YAML.load(response.body)
|
|
129
|
+
content['rack.url_scheme'].should == 'http'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "supports file downloads" do
|
|
133
|
+
response = get("/download")
|
|
134
|
+
response.code.should == "200"
|
|
135
|
+
response['Content-Type'].should == 'image/png'
|
|
136
|
+
response['Content-Disposition'].should ==
|
|
137
|
+
'attachment; filename=reddit-icon.png'
|
|
138
|
+
checksum = Digest::MD5.hexdigest(response.body)
|
|
139
|
+
checksum.should == '8da4b60a9bbe205d4d3699985470627e'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "supports file uploads" do
|
|
143
|
+
boundary = '349832898984244898448024464570528145'
|
|
144
|
+
content = []
|
|
145
|
+
content << "--#{boundary}"
|
|
146
|
+
content << 'Content-Disposition: form-data; name="file"; ' +
|
|
147
|
+
'filename="reddit-icon.png"'
|
|
148
|
+
content << 'Content-Type: image/png'
|
|
149
|
+
content << 'Content-Transfer-Encoding: base64'
|
|
150
|
+
content << ''
|
|
151
|
+
content <<
|
|
152
|
+
Base64.encode64( File.read('spec/data/reddit-icon.png') ).strip
|
|
153
|
+
content << "--#{boundary}--"
|
|
154
|
+
body = content.map { |l| l + "\r\n" }.join('')
|
|
155
|
+
headers = { "Content-Type" =>
|
|
156
|
+
"multipart/form-data; boundary=#{boundary}" }
|
|
157
|
+
response = post("/upload", nil, headers, body)
|
|
158
|
+
response.code.should == "200"
|
|
159
|
+
response.body.should == '8da4b60a9bbe205d4d3699985470627e'
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "handles async requests" do
|
|
163
|
+
lock = Mutex.new
|
|
164
|
+
buffer = Array.new
|
|
165
|
+
|
|
166
|
+
clients = 10.times.map do |index|
|
|
167
|
+
Thread.new do
|
|
168
|
+
Net::HTTP.start(@options[:host], @options[:port]) do |http|
|
|
169
|
+
response = http.get("/pull")
|
|
170
|
+
lock.synchronize {
|
|
171
|
+
buffer << "#{index}: #{response.body}" }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
lock.synchronize { buffer.should be_empty }
|
|
177
|
+
post("/push", 'message' => "one")
|
|
178
|
+
clients.each { |c| c.join }
|
|
179
|
+
lock.synchronize { buffer.should_not be_empty }
|
|
180
|
+
lock.synchronize { buffer.count.should == 10 }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
end
|