noder 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +327 -0
- data/lib/noder.rb +18 -1
- data/lib/noder/events.rb +7 -0
- data/lib/noder/events/em_event_node.rb +55 -0
- data/lib/noder/events/event_emitter.rb +78 -0
- data/lib/noder/events/event_node.rb +43 -0
- data/lib/noder/events/event_stack.rb +63 -0
- data/lib/noder/events/listeners/base.rb +13 -0
- data/lib/noder/http.rb +7 -0
- data/lib/noder/http/connection.rb +52 -0
- data/lib/noder/http/listeners/not_found.rb +20 -0
- data/lib/noder/http/listeners/request.rb +13 -0
- data/lib/noder/http/listeners/response.rb +13 -0
- data/lib/noder/http/request.rb +53 -0
- data/lib/noder/http/response.rb +64 -0
- data/lib/noder/http/server.rb +91 -0
- data/lib/noder/http/utils.rb +96 -0
- data/lib/noder/utils.rb +10 -0
- data/lib/noder/version.rb +1 -1
- metadata +66 -3
- data/Rakefile +0 -12
data/README.md
CHANGED
@@ -0,0 +1,327 @@
|
|
1
|
+
Noder
|
2
|
+
=====
|
3
|
+
Node.js for Ruby
|
4
|
+
|
5
|
+
Overview
|
6
|
+
--------
|
7
|
+
|
8
|
+
Noder brings the architecture of Node.js to Ruby. It focuses on the implementation of Node.js's HTTP-related support, as Ruby's standard library and other gems already provide great analogs of many of Node.js's other core modules.
|
9
|
+
|
10
|
+
You may also be interested in [Expressr](https://github.com/tombenner/expressr) (Express.js for Ruby), which Noder was built to support, and [EM-Synchrony](https://github.com/igrigorik/em-synchrony). Noder runs on [EventMachine](https://github.com/eventmachine/eventmachine).
|
11
|
+
|
12
|
+
Example
|
13
|
+
-------
|
14
|
+
|
15
|
+
A web server can be created and started using the following script:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'noder'
|
19
|
+
|
20
|
+
server = Noder::HTTP::Server.new do |request, response|
|
21
|
+
response.write_head(200, { 'Content-Type' => 'text/plain' })
|
22
|
+
response.end('Hello world!')
|
23
|
+
end
|
24
|
+
server.listen(1337, '127.0.0.1')
|
25
|
+
```
|
26
|
+
|
27
|
+
To start the app, put the code into a file named `my_server.rb` and run it:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
$ ruby my_server.rb
|
31
|
+
Running Noder at 127.0.0.1:1337...
|
32
|
+
```
|
33
|
+
|
34
|
+
HTTP
|
35
|
+
----
|
36
|
+
|
37
|
+
### Noder::HTTP::Server
|
38
|
+
|
39
|
+
`Noder::HTTP::Server` lets you create and run HTTP servers.
|
40
|
+
|
41
|
+
#### .new(options={}, &block)
|
42
|
+
|
43
|
+
Creates the server.
|
44
|
+
|
45
|
+
##### options
|
46
|
+
|
47
|
+
* `:address` - The server's address (default: `'0.0.0.0'`)
|
48
|
+
* `:port` - The server's port (default: `8000`)
|
49
|
+
* `:environment` - The server's environment name (default: `'development'`)
|
50
|
+
* `:threadpool_size` - The size of the server's threadpool default: `20`)
|
51
|
+
* `:enable_ssl` - A boolean of whether SSL is enabled (default: `false`)
|
52
|
+
* `:ssl_key` - A filepath to the SSL key (default: `nil`)
|
53
|
+
* `:ssl_cert` - A filepath to the SSL cert (default: `nil`)
|
54
|
+
|
55
|
+
##### &block
|
56
|
+
|
57
|
+
A block that will be called for every request. It will be passed the request (a Noder::HTTP::Request) and response (a Noder::HTTP::Response) as arguments.
|
58
|
+
|
59
|
+
#### #listen(port=nil, address=nil, options={}, &block)
|
60
|
+
|
61
|
+
Starts accepting connections to the server. `options` are the same as the options in `.new`, and `&block` behaves the same as in `.new`.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
server = Noder::HTTP::Server.new
|
65
|
+
server.listen(8001) do |request, response|
|
66
|
+
response.write("Hello world!")
|
67
|
+
response.end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
#### #close
|
72
|
+
|
73
|
+
Stops the server. This is called when an `INT` or `TERM` signal is sent to a running server's process (e.g. when `Control-C` is pressed).
|
74
|
+
|
75
|
+
#### Event 'request'
|
76
|
+
|
77
|
+
Emitted for every request. The request and response are passed as arguments.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
server.on('request') do |request, response|
|
81
|
+
Noder.logger.info "Request params: #{request.params}"
|
82
|
+
response.set_header('MyHeader', 'My value')
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
#### Event 'close'
|
87
|
+
|
88
|
+
Emitted when the server is closing. No arguments are passed.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
server.on('close') do
|
92
|
+
Noder.logger.info "Stopping server..."
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
### Noder::HTTP::Request
|
97
|
+
|
98
|
+
A representation of an HTTP request.
|
99
|
+
|
100
|
+
#### #params
|
101
|
+
|
102
|
+
A hash of the request's params (the query string and POST data). The hash's keys are strings (e.g. `/?foo=bar` yields `{ 'foo' => 'bar' }`)
|
103
|
+
|
104
|
+
#### #headers
|
105
|
+
|
106
|
+
A hash of the request's headers (e.g. `{ 'Accept' => '*/*', 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) [...]' }`).
|
107
|
+
|
108
|
+
#### #request_method
|
109
|
+
|
110
|
+
The request's verb (e.g. `'GET'`, `'POST'`, etc).
|
111
|
+
|
112
|
+
#### #cookie
|
113
|
+
|
114
|
+
The request's Cookie header (e.g. `'my_cookie=123; my_other_cookie=foo'`).
|
115
|
+
|
116
|
+
#### #content_type
|
117
|
+
|
118
|
+
The request's Content-Type header (e.g. `'application/x-www-form-urlencoded'`).
|
119
|
+
|
120
|
+
#### #request_uri
|
121
|
+
|
122
|
+
The request's path, without the query string (e.g. `'/users/1/profile'`).
|
123
|
+
|
124
|
+
#### #query_string
|
125
|
+
|
126
|
+
The request's query string (e.g. `/about?foo=bar&baz=1` yields `'foo=bar&baz=1'`).
|
127
|
+
|
128
|
+
#### #protocol
|
129
|
+
|
130
|
+
The request's protocol (e.g. `'HTTP/1.1'`).
|
131
|
+
|
132
|
+
#### #ip
|
133
|
+
|
134
|
+
The client's IP address (e.g. `'68.1.8.45'`).
|
135
|
+
|
136
|
+
### Noder::HTTP::Response
|
137
|
+
|
138
|
+
A representation of an HTTP response.
|
139
|
+
|
140
|
+
#### #write(content)
|
141
|
+
|
142
|
+
Writes the content to the response's body.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
response.write('Hello world!')
|
146
|
+
```
|
147
|
+
|
148
|
+
#### #write_head(status, headers={})
|
149
|
+
|
150
|
+
Sets the response's status code and sets the specified headers (if any).
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
response.write_head(500, { 'MyHeader' => 'My value' })
|
154
|
+
```
|
155
|
+
|
156
|
+
#### #status_code
|
157
|
+
|
158
|
+
Gets or sets the response's status code
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
response.status_code # 200
|
162
|
+
response.status_code = 500
|
163
|
+
```
|
164
|
+
|
165
|
+
#### #set_header(name, value)
|
166
|
+
|
167
|
+
Sets the specified header.
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
response.set_header('MyHeader', 'My value')
|
171
|
+
```
|
172
|
+
|
173
|
+
#### #get_header(name, value)
|
174
|
+
|
175
|
+
Gets the specified header.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
response.get_header('MyHeader') # 'My value'
|
179
|
+
```
|
180
|
+
|
181
|
+
#### #remove_header(name)
|
182
|
+
|
183
|
+
Gets the specified header.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
response.remove_header('MyHeader')
|
187
|
+
```
|
188
|
+
|
189
|
+
#### #end(content=nil)
|
190
|
+
|
191
|
+
Sends the response. This must be called on every response instance.
|
192
|
+
|
193
|
+
If `content` is provided, it is equivalent to calling `write(content)` followed by `end`.
|
194
|
+
|
195
|
+
Events
|
196
|
+
------
|
197
|
+
|
198
|
+
### Noder::Events::EventEmitter
|
199
|
+
|
200
|
+
Include `EventEmitter` in classes which should manage events. For example:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class MyServer
|
204
|
+
include Noder::Events::EventEmitter
|
205
|
+
|
206
|
+
def initialize(&block)
|
207
|
+
on('start', &block)
|
208
|
+
on('stop', proc { puts 'Stopping...' })
|
209
|
+
end
|
210
|
+
|
211
|
+
def run
|
212
|
+
emit('start')
|
213
|
+
emit('stop')
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
server = MyServer.new do
|
218
|
+
puts 'Starting up...'
|
219
|
+
end
|
220
|
+
server.on('start') do
|
221
|
+
puts 'Still starting up...'
|
222
|
+
end
|
223
|
+
|
224
|
+
server.run
|
225
|
+
# Starting up...
|
226
|
+
# Still starting up...
|
227
|
+
# Stopping...
|
228
|
+
```
|
229
|
+
|
230
|
+
#### #on(event, callback=nil, options={}, &block)
|
231
|
+
|
232
|
+
Adds a listener to the specified event. The listener can either be an instance of a Proc (as the `callback` argument) or a block.
|
233
|
+
|
234
|
+
`add_listener` is an alias of `on`.
|
235
|
+
|
236
|
+
#### #emit(event)
|
237
|
+
|
238
|
+
Call the listeners for the specified event.
|
239
|
+
|
240
|
+
#### #remove_listener(event, listener)
|
241
|
+
|
242
|
+
Remove the listener. Listeners are compared using the `==` operator.
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
listener = proc { puts 'Working...' }
|
246
|
+
server.on('start', listener)
|
247
|
+
server.remove_listener('start', listener)
|
248
|
+
```
|
249
|
+
|
250
|
+
#### #remove_all_listeners(event)
|
251
|
+
|
252
|
+
Removes all of the listeners from the event. You probably don't want to call this on core Noder events.
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
server.remove_all_listeners('start')
|
256
|
+
```
|
257
|
+
|
258
|
+
#### #set_max_listeners(event, count)
|
259
|
+
|
260
|
+
Sets the maximum number of listeners for the specified event. A warning will be logged every time any additional listeners are added.
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
server.set_max_listeners('start', 100)
|
264
|
+
```
|
265
|
+
|
266
|
+
#### #listeners(event)
|
267
|
+
|
268
|
+
Returns an array of the listeners for the specified event.
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
server.listeners('start')
|
272
|
+
```
|
273
|
+
|
274
|
+
#### #listener_count(event)
|
275
|
+
|
276
|
+
Returns the number of listeners for the specified event.
|
277
|
+
|
278
|
+
```ruby
|
279
|
+
server.listener_count('start')
|
280
|
+
```
|
281
|
+
|
282
|
+
Logging
|
283
|
+
-------
|
284
|
+
|
285
|
+
Noder's `Logger` is available at `Noder.logger`. You can write to it:
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
Noder.logger.debug 'My debug message...'
|
289
|
+
Noder.logger.error 'My error message...'
|
290
|
+
```
|
291
|
+
|
292
|
+
You can modify it or replace it if you like, too:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
# Adjust attributes of the logger
|
296
|
+
Noder.logger.level = Logger::DEBUG
|
297
|
+
|
298
|
+
# Or create a custom Logger
|
299
|
+
Noder.logger = Logger.new(STDOUT)
|
300
|
+
Noder.logger.level = Logger::DEBUG
|
301
|
+
```
|
302
|
+
|
303
|
+
See the [Logger docs](http://www.ruby-doc.org/stdlib-2.0/libdoc/logger/rdoc/Logger.html) for more.
|
304
|
+
|
305
|
+
HTTPS
|
306
|
+
-----
|
307
|
+
|
308
|
+
To support HTTPS, set `:enable_ssl` to `true` and set the `:ssl_key` and `:ssl_cert` values to the appropriate file paths:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
options = {
|
312
|
+
enable_ssl: true,
|
313
|
+
ssl_key: File.expand_path('../certs/key.pem', __FILE__),
|
314
|
+
ssl_cert: File.expand_path('../certs/cert.pem', __FILE__)
|
315
|
+
}
|
316
|
+
server = Noder::HTTP::Server.new(options)
|
317
|
+
```
|
318
|
+
|
319
|
+
Notes
|
320
|
+
-----
|
321
|
+
|
322
|
+
Noder is not currently a full implementation of Node.js, and some of its underlying architecture differs from Node.js's. If you see any places where it could be improved or added to, absolutely feel free to submit a PR.
|
323
|
+
|
324
|
+
License
|
325
|
+
-------
|
326
|
+
|
327
|
+
Noder is released under the MIT License. Please see the MIT-LICENSE file for details.
|
data/lib/noder.rb
CHANGED
@@ -1,4 +1,21 @@
|
|
1
|
-
require
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
directory = File.dirname(File.absolute_path(__FILE__))
|
5
|
+
Dir.glob("#{directory}/noder/*.rb") { |file| require file }
|
2
6
|
|
3
7
|
module Noder
|
8
|
+
class << self
|
9
|
+
def logger
|
10
|
+
@logger ||= Logger.new(STDOUT)
|
11
|
+
end
|
12
|
+
|
13
|
+
def logger=(logger)
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def with(operation, callback=nil, &block)
|
18
|
+
EM.defer(operation, callback || block)
|
19
|
+
end
|
20
|
+
end
|
4
21
|
end
|
data/lib/noder/events.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module Noder
|
2
|
+
module Events
|
3
|
+
class EMEventNode
|
4
|
+
attr_accessor :callback, :next_node
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@callback = options[:callback]
|
8
|
+
@argument_keys = options[:argument_keys]
|
9
|
+
raise 'No callback provided' if @callback.nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
if callback.respond_to?(:matches_env?) && !callback.matches_env?(env)
|
14
|
+
if next_node
|
15
|
+
operation = proc { next_node.call(env) }
|
16
|
+
EM.defer(operation)
|
17
|
+
end
|
18
|
+
return
|
19
|
+
end
|
20
|
+
operation = proc { call_operation(env) }
|
21
|
+
if next_node
|
22
|
+
callback = proc { |env| next_node.call(env) }
|
23
|
+
EM.defer(operation, callback)
|
24
|
+
else
|
25
|
+
EM.defer(operation)
|
26
|
+
end
|
27
|
+
env
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def call_operation(env)
|
33
|
+
@env = env
|
34
|
+
if @argument_keys
|
35
|
+
arguments = Utils.slice_hash(@env, @argument_keys).values
|
36
|
+
else
|
37
|
+
arguments = [@env]
|
38
|
+
end
|
39
|
+
perform_callback(arguments)
|
40
|
+
@env
|
41
|
+
end
|
42
|
+
|
43
|
+
def perform_callback(arguments)
|
44
|
+
continue_method = proc { EM.signal_loopbreak }
|
45
|
+
if @callback.is_a?(Proc)
|
46
|
+
@callback.call(*arguments, continue_method)
|
47
|
+
elsif @callback.is_a?(Class)
|
48
|
+
@env = @callback.new(continue_method).call(*arguments)
|
49
|
+
else
|
50
|
+
@env = @callback.call(*arguments, continue_method)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Noder
|
2
|
+
module Events
|
3
|
+
module EventEmitter
|
4
|
+
def event_stacks
|
5
|
+
@event_stacks ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def max_listener_counts
|
9
|
+
@max_listener_counts ||= {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def on(event, callback=nil, options={}, &block)
|
13
|
+
max_count = max_listener_counts[event]
|
14
|
+
current_count = listener_count(event)
|
15
|
+
if max_count && current_count >= max_count
|
16
|
+
Noder.logger.warn "Maximum listener count exceeded for #{self.class} (max count is #{max_count}; current count is #{current_count})."
|
17
|
+
end
|
18
|
+
callback ||= block
|
19
|
+
options[:callback] = callback
|
20
|
+
event_stacks[event] ||= EventStack.new(node_class: node_class_for_event(event))
|
21
|
+
event_stacks[event].push(options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def emit(event, *arguments)
|
25
|
+
return if event_stacks[event].nil?
|
26
|
+
event_stacks[event].call(*arguments)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_listener(event, listener)
|
30
|
+
event_stacks[event].remove(listener)
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_all_listeners(event)
|
34
|
+
event_stacks[event].remove_all
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_max_listeners(event, count)
|
38
|
+
max_listener_counts[event] = count
|
39
|
+
end
|
40
|
+
|
41
|
+
def listeners(event)
|
42
|
+
if event_stacks[event]
|
43
|
+
event_stacks[event].items.map { |item| item[:callback] }
|
44
|
+
else
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def listener_count(event)
|
50
|
+
if event_stacks[event]
|
51
|
+
event_stacks[event].length
|
52
|
+
else
|
53
|
+
0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def event_stack(event)
|
58
|
+
event_stacks[event]
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :add_listener, :on
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def set_node_class_for_event(klass, event)
|
66
|
+
event_node_classes[event] = klass
|
67
|
+
end
|
68
|
+
|
69
|
+
def event_node_classes
|
70
|
+
@event_node_classes ||= {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def node_class_for_event(event)
|
74
|
+
event_node_classes[event] || EMEventNode
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Noder
|
2
|
+
module Events
|
3
|
+
class EventNode
|
4
|
+
attr_accessor :callback, :next_node
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@callback = options[:callback]
|
8
|
+
@argument_keys = options[:argument_keys]
|
9
|
+
@has_continued = false
|
10
|
+
raise 'No callback provided' if @callback.nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@env = env
|
15
|
+
if @argument_keys
|
16
|
+
arguments = Utils.slice_hash(@env, @argument_keys).values
|
17
|
+
else
|
18
|
+
arguments = [@env]
|
19
|
+
end
|
20
|
+
perform_callback(arguments)
|
21
|
+
continue unless @has_continued
|
22
|
+
end
|
23
|
+
|
24
|
+
def continue(env=nil)
|
25
|
+
@has_continued = true
|
26
|
+
next_node.call(env || @env) if next_node
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def perform_callback(arguments)
|
32
|
+
continue_method = method(:continue)
|
33
|
+
if @callback.is_a?(Proc)
|
34
|
+
@callback.call(*arguments, continue_method)
|
35
|
+
elsif @callback.is_a?(Class)
|
36
|
+
@env = @callback.new(continue_method).call(*arguments)
|
37
|
+
else
|
38
|
+
@env = @callback.call(*arguments, continue_method)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Noder
|
2
|
+
module Events
|
3
|
+
class EventStack
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
attr_reader :items
|
7
|
+
|
8
|
+
def_delegators :@items, :length
|
9
|
+
|
10
|
+
def initialize(options={})
|
11
|
+
@items = []
|
12
|
+
@node_class = options[:node_class] || EMEventNode
|
13
|
+
end
|
14
|
+
|
15
|
+
def push(options={})
|
16
|
+
@items << options
|
17
|
+
end
|
18
|
+
|
19
|
+
def insert_before(target_callback, item)
|
20
|
+
index = index_of_callback(target_callback)
|
21
|
+
raise "Item not found for callback: #{target_callback}" if index.nil?
|
22
|
+
@items.insert(index, item)
|
23
|
+
end
|
24
|
+
|
25
|
+
def replace(target_callback, item)
|
26
|
+
index = index_of_callback(target_callback)
|
27
|
+
raise "Item not found for callback: #{target_callback}" if index.nil?
|
28
|
+
@items[index] = item
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove(target_callback)
|
32
|
+
index = index_of_callback(target_callback)
|
33
|
+
@items.delete_at(index) if index
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove_all
|
37
|
+
@items = []
|
38
|
+
end
|
39
|
+
|
40
|
+
def index_of_callback(callback)
|
41
|
+
@items.index { |item| item[:callback] == callback }
|
42
|
+
end
|
43
|
+
|
44
|
+
def call(env=nil)
|
45
|
+
empty_node = @node_class.new({ callback: proc { |env| env } })
|
46
|
+
nodes = @items.map { |item| @node_class.new(item) }
|
47
|
+
first_node = nodes.reverse.inject(empty_node) do |next_node, current_node|
|
48
|
+
current_node.next_node = next_node
|
49
|
+
current_node
|
50
|
+
end
|
51
|
+
first_node.call(env)
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def does_item_match?(item, env)
|
57
|
+
callback = item[:callback]
|
58
|
+
return true unless callback.respond_to?(:matches_env?)
|
59
|
+
callback.matches_env?(env)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/noder/http.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'evma_httpserver'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module Noder
|
5
|
+
module HTTP
|
6
|
+
class Connection < EM::Connection
|
7
|
+
include EventMachine::HttpServer
|
8
|
+
|
9
|
+
attr_accessor :app, :environment, :request_stack, :settings
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super(*args)
|
13
|
+
@settings = args[1]
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
super
|
18
|
+
if settings[:enable_ssl]
|
19
|
+
start_tls(:private_key_file => settings[:ssl_key], :cert_chain_file => settings[:ssl_cert], :verify_peer => false)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_http_request
|
24
|
+
env = {
|
25
|
+
connection: self,
|
26
|
+
request_env: request_env,
|
27
|
+
request: nil,
|
28
|
+
response: nil
|
29
|
+
}
|
30
|
+
EM.defer do
|
31
|
+
request_stack.call(env)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def request_env
|
36
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
37
|
+
{
|
38
|
+
request_method: @http_request_method,
|
39
|
+
cookie: @http_cookie,
|
40
|
+
content_type: @http_content_type,
|
41
|
+
path_info: @http_path_info,
|
42
|
+
request_uri: @http_request_uri,
|
43
|
+
query_string: @http_query_string,
|
44
|
+
post_content: @http_post_content,
|
45
|
+
headers: @http_headers,
|
46
|
+
protocol: @http_protocol,
|
47
|
+
ip: ip
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Noder
|
2
|
+
module HTTP
|
3
|
+
module Listeners
|
4
|
+
class NotFound < Events::Listeners::Base
|
5
|
+
def call(env)
|
6
|
+
callback.call(env) if callback
|
7
|
+
response = env[:response]
|
8
|
+
render_not_found(response) unless response.is_rendered?
|
9
|
+
env
|
10
|
+
end
|
11
|
+
|
12
|
+
def render_not_found(response)
|
13
|
+
response.status_code = 404
|
14
|
+
response.write('Not Found')
|
15
|
+
response.end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Noder
|
2
|
+
module HTTP
|
3
|
+
class Request
|
4
|
+
attr_accessor :params
|
5
|
+
attr_reader :env
|
6
|
+
|
7
|
+
def initialize(env)
|
8
|
+
@env = env
|
9
|
+
@query = HTTP::Utils.parse(env[:query_string])
|
10
|
+
@params = @query
|
11
|
+
if env[:post_content] && env[:post_content] != ''
|
12
|
+
@params.merge!(HTTP::Utils.parse(env[:post_content]))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def headers
|
17
|
+
@headers ||= HTTP::Utils.parse_headers(env[:headers])
|
18
|
+
end
|
19
|
+
|
20
|
+
def request_method
|
21
|
+
env[:request_method]
|
22
|
+
end
|
23
|
+
|
24
|
+
def cookie
|
25
|
+
env[:cookie]
|
26
|
+
end
|
27
|
+
|
28
|
+
def content_type
|
29
|
+
env[:content_type]
|
30
|
+
end
|
31
|
+
|
32
|
+
def request_uri
|
33
|
+
env[:request_uri]
|
34
|
+
end
|
35
|
+
|
36
|
+
def query_string
|
37
|
+
env[:query_string]
|
38
|
+
end
|
39
|
+
|
40
|
+
def post_content
|
41
|
+
env[:post_content]
|
42
|
+
end
|
43
|
+
|
44
|
+
def protocol
|
45
|
+
env[:protocol]
|
46
|
+
end
|
47
|
+
|
48
|
+
def ip
|
49
|
+
env[:ip]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Noder
|
2
|
+
module HTTP
|
3
|
+
class Response < EventMachine::DelegatedHttpResponse
|
4
|
+
attr_accessor :params
|
5
|
+
|
6
|
+
def initialize(env)
|
7
|
+
super(env[:connection])
|
8
|
+
@params = env[:request].params
|
9
|
+
@is_rendered = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(content)
|
13
|
+
self.content ||= ''
|
14
|
+
self.content << content
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_head(status, headers={})
|
18
|
+
self.status = status
|
19
|
+
@headers.merge!(headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def status_code=(status)
|
23
|
+
self.status = status
|
24
|
+
end
|
25
|
+
|
26
|
+
def status_code
|
27
|
+
self.status
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_header(name, value)
|
31
|
+
@headers[name] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_header(name)
|
35
|
+
@headers[name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_header(name)
|
39
|
+
@headers.delete(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def end(content=nil)
|
43
|
+
return if @is_rendered
|
44
|
+
@is_rendered = true
|
45
|
+
write(content) if content
|
46
|
+
send_response
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_rendered?
|
50
|
+
@is_rendered
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def app
|
56
|
+
@delegate.app
|
57
|
+
end
|
58
|
+
|
59
|
+
def request_env
|
60
|
+
@delegate.request_env
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'em-synchrony'
|
2
|
+
|
3
|
+
module Noder
|
4
|
+
module HTTP
|
5
|
+
class Server
|
6
|
+
include Events::EventEmitter
|
7
|
+
|
8
|
+
attr_accessor :options
|
9
|
+
|
10
|
+
def initialize(options={}, &block)
|
11
|
+
defaults = {
|
12
|
+
address: '0.0.0.0',
|
13
|
+
port: 8000,
|
14
|
+
app: nil,
|
15
|
+
environment: 'development',
|
16
|
+
threadpool_size: 20,
|
17
|
+
enable_ssl: false,
|
18
|
+
ssl_key: nil,
|
19
|
+
ssl_cert: nil
|
20
|
+
}
|
21
|
+
@options = defaults.merge(options)
|
22
|
+
# The 'close' event is emitted as EM is stopped, so we need to handle the callbacks outside of
|
23
|
+
# the EM event loop with Events::EventNode instead of Events::EMEventNode
|
24
|
+
set_node_class_for_event(Events::EventNode, 'close')
|
25
|
+
push_default_callbacks
|
26
|
+
on('request', &block) if block
|
27
|
+
end
|
28
|
+
|
29
|
+
def listen(port=nil, address=nil, options={}, &block)
|
30
|
+
@options.merge!(options)
|
31
|
+
@options[:port] = port if port
|
32
|
+
@options[:address] = address if address
|
33
|
+
EM.threadpool_size = @options[:threadpool_size]
|
34
|
+
EM.epoll
|
35
|
+
EM.synchrony do
|
36
|
+
trap('INT') { close }
|
37
|
+
trap('TERM') { close }
|
38
|
+
# Listeners::NotFound should run after all other listeners, so we'll add it here
|
39
|
+
add_listener('request', Listeners::NotFound)
|
40
|
+
|
41
|
+
Noder.logger.info "Running Noder at #{@options[:address]}:#{@options[:port]}..."
|
42
|
+
emit('start')
|
43
|
+
connection_settings = Noder::Utils.slice_hash(@options, [:enable_ssl, :ssl_key, :ssl_cert])
|
44
|
+
EM.start_server(@options[:address], @options[:port], Noder::HTTP::Connection, block, connection_settings) do |connection|
|
45
|
+
connection.request_stack = event_stack('request')
|
46
|
+
connection.app = @options[:app]
|
47
|
+
connection.environment = @options[:environment]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def on(event, callback=nil, &block)
|
53
|
+
callback ||= block
|
54
|
+
case event
|
55
|
+
when 'request'
|
56
|
+
super('request', callback, argument_keys: [:request, :response])
|
57
|
+
when 'close'
|
58
|
+
super('close', callback)
|
59
|
+
else
|
60
|
+
super(event, callback)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def close
|
65
|
+
Noder.logger.info 'Stopping Noder...'
|
66
|
+
emit('close')
|
67
|
+
EM.stop
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def push_default_callbacks
|
73
|
+
default_callbacks.each do |event, items|
|
74
|
+
items.each do |item|
|
75
|
+
add_listener(event, item)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_callbacks
|
81
|
+
{
|
82
|
+
'close' => [],
|
83
|
+
'request' => [
|
84
|
+
Listeners::Request,
|
85
|
+
Listeners::Response
|
86
|
+
]
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Noder
|
4
|
+
module HTTP
|
5
|
+
module Utils
|
6
|
+
DEFAULT_SEP = /[&;] */n
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :key_space_limit
|
10
|
+
end
|
11
|
+
|
12
|
+
# The default number of bytes to allow parameter keys to take up.
|
13
|
+
# This helps prevent a rogue client from flooding a Request.
|
14
|
+
self.key_space_limit = 65536
|
15
|
+
|
16
|
+
class KeySpaceConstrainedParams
|
17
|
+
def initialize(limit = Utils.key_space_limit)
|
18
|
+
@limit = limit
|
19
|
+
@size = 0
|
20
|
+
@params = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
@params[key]
|
25
|
+
end
|
26
|
+
|
27
|
+
def []=(key, value)
|
28
|
+
@size += key.size if key && !@params.key?(key)
|
29
|
+
raise RangeError, 'exceeded available parameter key space' if @size > @limit
|
30
|
+
@params[key] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def key?(key)
|
34
|
+
@params.key?(key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_params_hash
|
38
|
+
hash = @params
|
39
|
+
hash.keys.each do |key|
|
40
|
+
value = hash[key]
|
41
|
+
if value.kind_of?(self.class)
|
42
|
+
hash[key] = value.to_params_hash
|
43
|
+
elsif value.kind_of?(Array)
|
44
|
+
value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if defined?(::Encoding)
|
52
|
+
def unescape(s, encoding = Encoding::UTF_8)
|
53
|
+
URI.decode_www_form_component(s, encoding)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
def unescape(s, encoding = nil)
|
57
|
+
URI.decode_www_form_component(s, encoding)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
module_function :unescape
|
61
|
+
|
62
|
+
def parse(qs, d = nil, &unescaper)
|
63
|
+
unescaper ||= method(:unescape)
|
64
|
+
|
65
|
+
params = KeySpaceConstrainedParams.new
|
66
|
+
|
67
|
+
(qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
|
68
|
+
next if p.empty?
|
69
|
+
k, v = p.split('=', 2).map(&unescaper)
|
70
|
+
|
71
|
+
if cur = params[k]
|
72
|
+
if cur.class == Array
|
73
|
+
params[k] << v
|
74
|
+
else
|
75
|
+
params[k] = [cur, v]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
params[k] = v
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
return params.to_params_hash
|
83
|
+
end
|
84
|
+
module_function :parse
|
85
|
+
|
86
|
+
def parse_headers(string)
|
87
|
+
string.split("\x00").reduce({}) do |hash, string|
|
88
|
+
key, value = string.split(': ', 2)
|
89
|
+
hash[key] = value
|
90
|
+
hash
|
91
|
+
end
|
92
|
+
end
|
93
|
+
module_function :parse_headers
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/noder/utils.rb
ADDED
data/lib/noder/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: noder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,56 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-05-
|
12
|
+
date: 2014-05-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: eventmachine_httpserver
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: em-synchrony
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.0.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.0
|
14
62
|
- !ruby/object:Gem::Dependency
|
15
63
|
name: rspec
|
16
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -34,10 +82,25 @@ executables: []
|
|
34
82
|
extensions: []
|
35
83
|
extra_rdoc_files: []
|
36
84
|
files:
|
85
|
+
- lib/noder/events/em_event_node.rb
|
86
|
+
- lib/noder/events/event_emitter.rb
|
87
|
+
- lib/noder/events/event_node.rb
|
88
|
+
- lib/noder/events/event_stack.rb
|
89
|
+
- lib/noder/events/listeners/base.rb
|
90
|
+
- lib/noder/events.rb
|
91
|
+
- lib/noder/http/connection.rb
|
92
|
+
- lib/noder/http/listeners/not_found.rb
|
93
|
+
- lib/noder/http/listeners/request.rb
|
94
|
+
- lib/noder/http/listeners/response.rb
|
95
|
+
- lib/noder/http/request.rb
|
96
|
+
- lib/noder/http/response.rb
|
97
|
+
- lib/noder/http/server.rb
|
98
|
+
- lib/noder/http/utils.rb
|
99
|
+
- lib/noder/http.rb
|
100
|
+
- lib/noder/utils.rb
|
37
101
|
- lib/noder/version.rb
|
38
102
|
- lib/noder.rb
|
39
103
|
- MIT-LICENSE
|
40
|
-
- Rakefile
|
41
104
|
- README.md
|
42
105
|
homepage: https://github.com/tombenner/noder
|
43
106
|
licenses:
|
data/Rakefile
DELETED