nut 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +210 -0
- data/Rakefile +9 -0
- data/lib/nut/codes.rb +24 -0
- data/lib/nut/handler.rb +177 -0
- data/lib/nut/request.rb +262 -0
- data/lib/nut/response.rb +63 -0
- data/lib/nut/service.rb +39 -0
- data/lib/nut/type_symbols.rb +13 -0
- data/lib/nut/version.rb +9 -0
- data/lib/nut.rb +22 -0
- data/nut.gemspec +26 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3af333d81c0b9c200c94d0c6b057bd186160de8e
|
4
|
+
data.tar.gz: 4cbe18d68cc8ab6027f5be675e7b76ac82f9feea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 001a4fc589d9566ca0e7b6c9ec07bef69a91b48601f2b48c5be9c7bafd48026de9cfc17d7df22dd63ff5f4368599635a76789ac6968a16dd0e6ffa8ac6bab6a9
|
7
|
+
data.tar.gz: 9f7cb2a24274a8dc22f6ef41d103edc218f4327e4cfcf698fa82e274c46a012f097e1dc616665ce5d92e210036f8982177c410ea22a2bb44de564ceeb5830f90
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Paul Duncan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
# Nut
|
2
|
+
|
3
|
+
Miniature Reactive HTTP Server
|
4
|
+
|
5
|
+
## Presentation
|
6
|
+
|
7
|
+
This library is an implementation of an HTTP Server using the Reactor Pattern as provided by [RxIO](https://rubygems.org/gems/rxio).
|
8
|
+
This allows easy development of fast, non-blocking Web services.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
### Gemfile
|
13
|
+
```ruby
|
14
|
+
gem 'nut'
|
15
|
+
```
|
16
|
+
|
17
|
+
### Terminal
|
18
|
+
```bash
|
19
|
+
gem install -V nut
|
20
|
+
```
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
### Introduction
|
25
|
+
|
26
|
+
*Nut* is a miniature pure-Ruby implementation of a reactive HTTP 1.1 Server.
|
27
|
+
Relying on [RxIO](https://rubygems.org/gems/rxio), Nut is simple to use yet fast and lightweight.
|
28
|
+
|
29
|
+
### Request Handler
|
30
|
+
Nut uses a *request_handler* module to service client requests. This module is to be implemented by the user.
|
31
|
+
The request_handler module requires only one method: _handle_, which takes two Hashes as arguments: _request_ and _response_.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# Request Handler Module
|
35
|
+
module ExampleRequestHandler
|
36
|
+
def handle request, response
|
37
|
+
# All processing done here...
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create Nut Server around Request Handler
|
42
|
+
n = Nut::Service.new ExampleRequestHandler
|
43
|
+
```
|
44
|
+
|
45
|
+
### The Request Hash
|
46
|
+
|
47
|
+
The _request_ argument to the *handle* method is a hash containing information representing a client request.
|
48
|
+
|
49
|
+
The following fields are available:
|
50
|
+
* *:peer* - RxIO Peer Information Hash such as { name: 'foobar.eresse.net', addr: '51.254.97.136', port: 42432 }
|
51
|
+
* *:verb* - HTTP Verb such as :get / :post
|
52
|
+
* *:uri* - Request URI (without host and params)
|
53
|
+
* *:body* - Complete Request Body (without headers)
|
54
|
+
* *:ctype* - Content Type
|
55
|
+
* *:params* - Request Params Hash such as { foo: 'bar', test_file: { filename: 'raccoon.txt', data: '...' } }
|
56
|
+
* *:headers* - Request Headers Hash
|
57
|
+
|
58
|
+
### The Response Hash
|
59
|
+
|
60
|
+
The _response_ argument to the *handle* method is a hash that should be filled by the user to represent a response.
|
61
|
+
The following hash keys are valid:
|
62
|
+
|
63
|
+
* *:code* - HTTP Response Code (Numeric format) (200, 404, etc...) - If unspecified, defaults to *200* (unless a redirect is requested)
|
64
|
+
* *:body* - Response Body
|
65
|
+
* *:type* - Response Content-Type - If unspecified, will be automatically inferred from *:body* through [Typor](https://rubygems.org/gems/typor)
|
66
|
+
* *:redirect* - String (target of redirect) / Hash { to: 'http://url', permanent: true } (for a _301 Moved_ instead of _302 Found_)
|
67
|
+
|
68
|
+
#### Rendering
|
69
|
+
|
70
|
+
Rendering any output is achieved by appending data to *response[:body]*, as shown in the following example:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
# Request Handler Module
|
74
|
+
module ExampleRequestHandler
|
75
|
+
def handle request, response
|
76
|
+
|
77
|
+
# Spit out some HTML
|
78
|
+
response[:body] << "<html><body>Hello world!</body></html>"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create Nut Server around Request Handler
|
83
|
+
n = Nut::Service.new ExampleRequestHandler
|
84
|
+
```
|
85
|
+
|
86
|
+
#### Content-Type
|
87
|
+
|
88
|
+
Nut will try to determine the appropriate content-type based on the response body. However, this can easily be bypassed by setting *response[:type]* to anything (other than nil).
|
89
|
+
If *response[:type]* is a String, it will be used directly as the Content-Type.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# Request Handler Module
|
93
|
+
module ExampleRequestHandler
|
94
|
+
def handle request, response
|
95
|
+
|
96
|
+
# Respond with JSON
|
97
|
+
response[:body] = { success: true, foo: :bar }.to_json
|
98
|
+
response[:type] = 'application/json'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create Nut Server around Request Handler
|
103
|
+
n = Nut::Service.new ExampleRequestHandler
|
104
|
+
```
|
105
|
+
|
106
|
+
If *response[:type]* is a Symbol, it will be matched against a set of shortcuts to determine the Content-Type.
|
107
|
+
The available shortcuts are:
|
108
|
+
* :json
|
109
|
+
* :text
|
110
|
+
* :html
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# Request Handler Module
|
114
|
+
module ExampleRequestHandler
|
115
|
+
def handle request, response
|
116
|
+
|
117
|
+
# Respond with JSON
|
118
|
+
response[:body] = { success: true, foo: :bar }.to_json
|
119
|
+
response[:type] = :json
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Create Nut Server around Request Handler
|
124
|
+
n = Nut::Service.new ExampleRequestHandler
|
125
|
+
```
|
126
|
+
|
127
|
+
#### Redirecting
|
128
|
+
|
129
|
+
Redirecting is achieved by setting *response[:redirect]* to either a String or a Hash.
|
130
|
+
If *response[:redirect]* is a String, Nut will respond with a *302 Found* "temporary" redirect.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
# Request Handler Module
|
134
|
+
module ExampleRequestHandler
|
135
|
+
def handle request, response
|
136
|
+
|
137
|
+
# Temporarily Redirect to 'http://eresse.net'
|
138
|
+
response[:redirect] = 'http://eresse.net'
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Create Nut Server around Request Handler
|
143
|
+
n = Nut::Service.new ExampleRequestHandler
|
144
|
+
```
|
145
|
+
|
146
|
+
If *response[:redirect]* is a Hash, Nut will respond with either a *302 Found* or a *301 Moved* "permanent" redirect depending on the *:permanent* field.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# Request Handler Module
|
150
|
+
module ExampleRequestHandler
|
151
|
+
def handle request, response
|
152
|
+
|
153
|
+
# Permanently Redirect to 'http://eresse.net'
|
154
|
+
response[:redirect] = { to: 'http://eresse.net', permanent: true }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Create Nut Server around Request Handler
|
159
|
+
n = Nut::Service.new ExampleRequestHandler
|
160
|
+
```
|
161
|
+
|
162
|
+
### Best Practice
|
163
|
+
|
164
|
+
While anything is possible, the recommended pattern for using Nut is to extend the Nut Service class, and embed the Request Handler Module inside the new Service Class.
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
class ExampleService < Nut::Service
|
168
|
+
def initialize
|
169
|
+
super RequestHandler
|
170
|
+
end
|
171
|
+
|
172
|
+
module RequestHandler
|
173
|
+
def handle request, response
|
174
|
+
response[:body] << "You requested [#{request[:uri]}] as [#{request[:verb]}]"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
### Service Interface
|
181
|
+
|
182
|
+
The usual _run_ / _stop_ RxIO synchronous service interface is available, as well as the [Runify](https://rubygems.org/gems/runify)-ed version (_startup_ / _shutdown_).
|
183
|
+
|
184
|
+
#### Running the server
|
185
|
+
```ruby
|
186
|
+
# Create Nut Server
|
187
|
+
n = Nut::Service.new 'localhost', 23280, ExampleHandler
|
188
|
+
|
189
|
+
# Start
|
190
|
+
n.run
|
191
|
+
# Blocks until stop is called on the server...
|
192
|
+
```
|
193
|
+
|
194
|
+
#### Running in the background
|
195
|
+
```ruby
|
196
|
+
# Create Nut Server
|
197
|
+
n = Nut::Service.new 'localhost', 23280, ExampleHandler
|
198
|
+
|
199
|
+
# Start
|
200
|
+
n.startup
|
201
|
+
# Server is running in the background...
|
202
|
+
|
203
|
+
# Stop
|
204
|
+
n.shutdown
|
205
|
+
# Server has stopped
|
206
|
+
```
|
207
|
+
|
208
|
+
## License
|
209
|
+
|
210
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/nut/codes.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Nut
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'uri'
|
6
|
+
require 'cgi'
|
7
|
+
require 'typor'
|
8
|
+
require 'rxio'
|
9
|
+
|
10
|
+
# Internal Includes
|
11
|
+
require 'nut/request'
|
12
|
+
|
13
|
+
# Nut Module
|
14
|
+
module Nut
|
15
|
+
|
16
|
+
# HTTP Response Codes
|
17
|
+
CODES = {
|
18
|
+
200 => 'OK',
|
19
|
+
301 => 'Moved',
|
20
|
+
302 => 'Found',
|
21
|
+
404 => 'Not Found',
|
22
|
+
501 => 'Not Implemented'
|
23
|
+
}
|
24
|
+
end
|
data/lib/nut/handler.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
# Nut
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'uri'
|
6
|
+
require 'cgi'
|
7
|
+
require 'typor'
|
8
|
+
require 'rxio'
|
9
|
+
|
10
|
+
# Internal Includes
|
11
|
+
require 'nut/codes'
|
12
|
+
require 'nut/request'
|
13
|
+
require 'nut/response'
|
14
|
+
|
15
|
+
# Nut Module
|
16
|
+
module Nut
|
17
|
+
|
18
|
+
# Service Handler Module
|
19
|
+
module Handler
|
20
|
+
|
21
|
+
# Extend BinDelim I/O Filter
|
22
|
+
extend RxIO::IOFilters::BinDelim
|
23
|
+
|
24
|
+
# Set Message Delimiter
|
25
|
+
msg_delim "\n"
|
26
|
+
|
27
|
+
# Only handle HTTP/1.1 Requests
|
28
|
+
SUPPORTED_VERSION = 'http/1.1'
|
29
|
+
|
30
|
+
# On Join
|
31
|
+
# RxIO Service Interface Callback - Triggered each time a Client connects.
|
32
|
+
# @param [Hash] client An RxIO Client Hash
|
33
|
+
def self.on_join client
|
34
|
+
# NoOp
|
35
|
+
end
|
36
|
+
|
37
|
+
# On Drop
|
38
|
+
# RxIO Service Interface Callback - Triggered each time a Client disconnects.
|
39
|
+
# @param [Hash] client An RxIO Client Hash
|
40
|
+
def self.on_drop client
|
41
|
+
# NoOp
|
42
|
+
end
|
43
|
+
|
44
|
+
# Handle Message
|
45
|
+
# RxIO Service Interface Callback - Triggered each time a message is received from a Client.
|
46
|
+
# @param [Hash] client An RxIO Client Hash
|
47
|
+
# @param [String] msg The message received from the Client
|
48
|
+
def self.handle_msg client, msg
|
49
|
+
|
50
|
+
# Pass Full Message to Request Builder (including Message Delimiter)
|
51
|
+
handle_req_dline client, msg + @msg_delim
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sub-Process Input
|
55
|
+
# RxIO Service Interface Callback - Triggered each time a chunk of data is received from a Client.
|
56
|
+
# Some Content-Length-based Requests may not end with a CRLF - this pulls in any left-over data directly from the RxIO Input Buffer.
|
57
|
+
# @param [Hash] client Client Hash
|
58
|
+
def self.subprocess_input client
|
59
|
+
|
60
|
+
# Acquire Client Request Context Hash
|
61
|
+
creq = client[:nut].try(:[], :req) || {}
|
62
|
+
|
63
|
+
# Pull any left-overs from Input Buffer for Content-Length Requests
|
64
|
+
handle_req_dline client, creq[:client][:ibuf].slice!(0, creq[:client][:ibuf].bytesize) if Request.is_content_len? creq
|
65
|
+
end
|
66
|
+
|
67
|
+
# Handle Request Data Line
|
68
|
+
# Ensures availability of a Client Request Context before passing Assembling & Processing.
|
69
|
+
# @param [Hash] client An RxIO Client Hash
|
70
|
+
# @param [String] msg The message received from the Client (including newline)
|
71
|
+
def self.handle_req_dline client, msg
|
72
|
+
|
73
|
+
# Ensure & Acquire Client Request Context
|
74
|
+
creq = ensure_creq client
|
75
|
+
|
76
|
+
# Assemble & Process
|
77
|
+
assemble_process_req creq, msg unless creq[:body_complete]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Ensure Client Request Hash
|
81
|
+
# Ensures existence of Client Request Hash inside Client Context
|
82
|
+
# @param [Hash] client An RxIO Client Hash
|
83
|
+
def self.ensure_creq client
|
84
|
+
|
85
|
+
# Ensure Client Request Structure is available
|
86
|
+
client[:nut] ||= {}
|
87
|
+
client[:nut][:req] ||= {
|
88
|
+
|
89
|
+
# Request Lines (full request, including headers)
|
90
|
+
lines: [],
|
91
|
+
|
92
|
+
# Request Headers
|
93
|
+
headz: {},
|
94
|
+
|
95
|
+
# Full Request Line (including verb, URI and version)
|
96
|
+
req_l: nil,
|
97
|
+
|
98
|
+
# Full Request Body (just the body though, no headers)
|
99
|
+
req_b: '',
|
100
|
+
|
101
|
+
# Chunk Info (for Chunked Transfer-Encoding)
|
102
|
+
chunk: {},
|
103
|
+
|
104
|
+
# Client
|
105
|
+
client: client
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Assemble & Process Request
|
110
|
+
# Assembles and processes HTTP Requests from individual data lines.
|
111
|
+
# @param [Hash] creq Client Request Context
|
112
|
+
# @param [String] msg The message received from the Client (including newline)
|
113
|
+
def self.assemble_process_req creq, msg
|
114
|
+
|
115
|
+
# Register Request Line
|
116
|
+
creq[:lines] << msg
|
117
|
+
|
118
|
+
# Build Request Line
|
119
|
+
Request.build_req_line creq, msg
|
120
|
+
|
121
|
+
# Check Version
|
122
|
+
raise "Unsupported HTTP Version [#{creq[:version]}]" unless creq[:version].downcase == SUPPORTED_VERSION
|
123
|
+
|
124
|
+
# Build Headers
|
125
|
+
Request.build_header creq, msg
|
126
|
+
|
127
|
+
# Build Request Body
|
128
|
+
Request.build_req_body creq, msg
|
129
|
+
|
130
|
+
# Process Request when Complete
|
131
|
+
process_req creq[:client]
|
132
|
+
end
|
133
|
+
|
134
|
+
# Process Request for Client
|
135
|
+
# Constructs a complete Request Hash from a Client Request Context and passes it to the Request Handler Module's _handle_req_ method.
|
136
|
+
# @param [Hash] client An RxIO Client Hash
|
137
|
+
def self.process_req client
|
138
|
+
|
139
|
+
# Check Request Body Complete
|
140
|
+
return unless client[:nut][:req][:body_complete]
|
141
|
+
|
142
|
+
# Pull Request Hash & Complete
|
143
|
+
creq = client[:nut].delete :req
|
144
|
+
uri = URI.parse creq[:uri]
|
145
|
+
request = {
|
146
|
+
peer: client[:peer].dclone,
|
147
|
+
verb: creq[:verb].downcase.to_sym,
|
148
|
+
uri: uri,
|
149
|
+
body: creq[:req_b],
|
150
|
+
ctype: creq[:ctype],
|
151
|
+
params: Hash[*(CGI.parse(uri.query || '').inject([]) { |a, e| a + [e.first, (e.last.size == 1) ? e.last.first : e.last] })].sym_keys,
|
152
|
+
headers: creq[:headz].dclone
|
153
|
+
}
|
154
|
+
|
155
|
+
# Pull all Request Details
|
156
|
+
Request.extract_req_data request
|
157
|
+
|
158
|
+
# Prepare Response
|
159
|
+
response = { body: '' }
|
160
|
+
|
161
|
+
# Pass to Request Handler
|
162
|
+
client[:serv].request_handler.handle request, response
|
163
|
+
|
164
|
+
# Respond to Client
|
165
|
+
serve_response client, response
|
166
|
+
end
|
167
|
+
|
168
|
+
# Serve Response
|
169
|
+
# Sends out an HTTP Response to the Client.
|
170
|
+
# @param [Hash] response Response Hash
|
171
|
+
def self.serve_response client, response
|
172
|
+
|
173
|
+
# Send out Response String
|
174
|
+
write client, Response.build_response(response)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/lib/nut/request.rb
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
# Nut
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Nut Module
|
5
|
+
module Nut
|
6
|
+
|
7
|
+
# Request Module
|
8
|
+
module Request
|
9
|
+
|
10
|
+
# Content-Type Regex
|
11
|
+
CTYPE_REX = /^[^;]+/
|
12
|
+
|
13
|
+
# Boundary Regex
|
14
|
+
BOUNDARY_REX = /boundary=(.+)$/
|
15
|
+
|
16
|
+
# Multipart Field Regex
|
17
|
+
MULTIPART_FIELD_REX = /^([^=]+)=(.+)$/
|
18
|
+
|
19
|
+
# Pull Request Details
|
20
|
+
# Extracts any possible Form Data and extended Request details from a Request Hash
|
21
|
+
# @param [Hash] req Request Hash
|
22
|
+
def self.extract_req_data req
|
23
|
+
|
24
|
+
# Switch on Content-Type { Extract Form Data }
|
25
|
+
case req[:ctype]
|
26
|
+
when 'multipart/form-data'
|
27
|
+
extract_multipart_form_data req
|
28
|
+
when 'application/x-www-form-urlencoded'
|
29
|
+
extract_urlencoded_form_data req
|
30
|
+
when nil
|
31
|
+
# NoOp
|
32
|
+
else
|
33
|
+
raise "Unsupported Content-Type [#{req[:ctype]}]"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Extract Multipart Form Data
|
38
|
+
# Extracts Multipart Form Data from the Request Body and merges it into _req[:params]_
|
39
|
+
# @param [Hash] req Request Hash
|
40
|
+
def self.extract_multipart_form_data req
|
41
|
+
|
42
|
+
# Acquire Boundary
|
43
|
+
req[:boundary] = BOUNDARY_REX.match(req[:headers][:content_type]).try(:[], 1).try :gsub, /[ \t]*$/, ''
|
44
|
+
|
45
|
+
# Split Body into Parts
|
46
|
+
req[:parts] = req[:body].split("\r\n--#{req[:boundary]}").reject { |p| p.empty? }
|
47
|
+
|
48
|
+
# Remove last part ('--')
|
49
|
+
req[:parts].pop
|
50
|
+
|
51
|
+
# Collect Form Data from Parts
|
52
|
+
req[:parts].each do |p|
|
53
|
+
|
54
|
+
# Split Headers & Data
|
55
|
+
headers, data = p.split "\r\n\r\n", 2
|
56
|
+
|
57
|
+
# Generate Headers
|
58
|
+
headers = build_gen_head headers.split("\r\n")
|
59
|
+
|
60
|
+
# Extract additional information from headers
|
61
|
+
finfo = Hash[*(headers[:content_disposition].split('; ').inject([]) do |a, f|
|
62
|
+
m = MULTIPART_FIELD_REX.match(f)
|
63
|
+
if m
|
64
|
+
name = m.try :[], 1
|
65
|
+
val = m.try :[], 2
|
66
|
+
val = val.slice(1, val.size - 2) if val.start_with?('"') && val.end_with?('"')
|
67
|
+
a + [name.downcase.to_sym, val]
|
68
|
+
else
|
69
|
+
a
|
70
|
+
end
|
71
|
+
end)]
|
72
|
+
|
73
|
+
# Merge Request Params
|
74
|
+
req[:params][finfo[:name].to_sym] = finfo[:filename] ? { filename: finfo[:filename], data: data } : data
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Extract URL-Encoded Form Data
|
79
|
+
# Extracts URL-Encoded Form Data from the Request Body and merges it into _req[:params]_
|
80
|
+
# @param [Hash] req Request Hash
|
81
|
+
def self.extract_urlencoded_form_data req
|
82
|
+
|
83
|
+
# Split Body into Params & Merge
|
84
|
+
req[:body]
|
85
|
+
.split('&')
|
86
|
+
.collect { |p| p.split '=', 2 }
|
87
|
+
.each { |name, val| req[:params][name] = val }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Build Request Line
|
91
|
+
# Parses *msg* as the Request Line unless already set, extracting HTTP Verb, URI & Version.
|
92
|
+
# @param [Hash] creq Client Request Context
|
93
|
+
# @param [String] msg A line of text to be processed
|
94
|
+
def self.build_req_line creq, msg
|
95
|
+
|
96
|
+
# Check Request Line
|
97
|
+
return if creq[:req_l]
|
98
|
+
|
99
|
+
# Set Request Line
|
100
|
+
rl = msg.chomp
|
101
|
+
creq[:req_l] = rl
|
102
|
+
|
103
|
+
# Pull up Verb, URI, Version
|
104
|
+
creq[:verb] = rl.slice 0, rl.index(' ')
|
105
|
+
creq[:uri] = rl.slice rl.index(' ') + 1, rl.size
|
106
|
+
creq[:version] = creq[:uri].slice! creq[:uri].rindex(' ') + 1, creq[:uri].size
|
107
|
+
creq[:uri].chomp! ' '
|
108
|
+
end
|
109
|
+
|
110
|
+
# Build Header
|
111
|
+
# Extracts HTTP Headers from Request if available and not already set, returning true on success, false otherwise.
|
112
|
+
# @param [Hash] creq Client Request Context
|
113
|
+
# @param [String] msg A line of text to be processed
|
114
|
+
def self.build_header creq, msg
|
115
|
+
|
116
|
+
# Pull Headers if not already done
|
117
|
+
if msg.chomp == '' && creq[:headz].empty?
|
118
|
+
|
119
|
+
# Build Headers
|
120
|
+
creq[:headz] = build_gen_head creq[:lines].dclone.slice(1, creq[:lines].size)
|
121
|
+
|
122
|
+
# Acquire Clean Content-Type
|
123
|
+
creq[:ctype] = CTYPE_REX.match(creq[:headz][:content_type]).try :[], 0
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Build Generic Headers
|
128
|
+
# Constructs a structured Header Hash from an Array of text lines.
|
129
|
+
# @param [Array] lines A bunch of lines containing header data
|
130
|
+
# @return [Hash] A Hash representation of the Headers
|
131
|
+
def self.build_gen_head lines
|
132
|
+
|
133
|
+
# Prepare Header
|
134
|
+
headz = {}
|
135
|
+
|
136
|
+
# Pull up Header lines as { "Field-Name" => [val0, val1, ..., valX] }
|
137
|
+
lines
|
138
|
+
.inject([]) { |a, l| if /^[ \t]+/ =~ l then a.last << l.gsub(/^[ \t]+/, ' ') else a << l end; a }
|
139
|
+
.collect { |l| l.chomp.split ': ', 2 }
|
140
|
+
.select { |name, _val| name }
|
141
|
+
.each { |name, val| headz[name] ||= []; headz[name] << val }
|
142
|
+
|
143
|
+
# Concat Header fields into { field_name: 'field0,field1,...,fieldX' }
|
144
|
+
Hash[*(headz.inject([]) { |a, h| a + [h.first.downcase.gsub('-', '_').to_sym, h.last.join(',')] })]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Build Request Body
|
148
|
+
# Builds Request Body according to Request Headers (Content-Length / Transfer-Encoding)
|
149
|
+
# @param [Hash] creq Client Request Context
|
150
|
+
# @param [String] msg A line of text to be processed
|
151
|
+
def self.build_req_body creq, msg
|
152
|
+
|
153
|
+
# Check for Headers
|
154
|
+
return if creq[:headz].empty?
|
155
|
+
|
156
|
+
# Handle Chunked Transfer-Encoding
|
157
|
+
build_body_chunked creq, msg
|
158
|
+
|
159
|
+
# Handle Simple Body
|
160
|
+
build_body_normal creq, msg
|
161
|
+
|
162
|
+
# Handle No Body
|
163
|
+
build_no_body creq
|
164
|
+
end
|
165
|
+
|
166
|
+
# Build Body - Chunked
|
167
|
+
# Assembles Chunks from *msg* (if Headers are complete) and assembles Request Body from them.
|
168
|
+
# @param [Hash] creq Client Request Context
|
169
|
+
# @param [String] msg A line of text to be processed
|
170
|
+
def self.build_body_chunked creq, msg
|
171
|
+
|
172
|
+
# Check for Chunked Transfer-Encoding
|
173
|
+
return unless is_chunked? creq
|
174
|
+
|
175
|
+
# Are we already reading a chunk?
|
176
|
+
if creq[:chunk][:size]
|
177
|
+
|
178
|
+
# Pull Chunk Data
|
179
|
+
creq[:chunk][:data] ||= ''
|
180
|
+
creq[:chunk][:data] << msg.chomp
|
181
|
+
|
182
|
+
# Check Chunk Complete
|
183
|
+
if creq[:chunk][:data].bytes.size >= creq[:chunk][:size]
|
184
|
+
|
185
|
+
# Append to Body
|
186
|
+
creq[:req_b] << creq[:chunk][:data].bytes.slice(0, creq[:chunk][:size]).pack('c*')
|
187
|
+
|
188
|
+
# Reset Chunk
|
189
|
+
creq[:chunk] = {}
|
190
|
+
end
|
191
|
+
else
|
192
|
+
|
193
|
+
# Acquire next chunk size
|
194
|
+
creq[:chunk][:size] = /[0-9]/.match(msg.chomp).try(:[], 0).try :to_i
|
195
|
+
|
196
|
+
# Check Body Complete
|
197
|
+
creq[:body_complete] = true if creq[:chunk][:size] == 0
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Build Body - Normal (Content-Length)
|
202
|
+
# Assembles Request Body from *msg* if Headers are complete, until Content-Length is reached.
|
203
|
+
# @param [Hash] creq Client Request Context
|
204
|
+
# @param [String] msg A line of text to be processed
|
205
|
+
def self.build_body_normal creq, msg
|
206
|
+
|
207
|
+
# Check for Content-Length
|
208
|
+
return unless is_content_len? creq
|
209
|
+
|
210
|
+
# Reject first empty line (except for multipart requests, in which the extra CRLF helps in splitting up request parts)
|
211
|
+
if creq[:got_first_line] || (creq[:ctype] == 'multipart/form-data')
|
212
|
+
|
213
|
+
# Assemble Request Body
|
214
|
+
creq[:req_b] << msg
|
215
|
+
|
216
|
+
# Check Body Complete
|
217
|
+
if creq[:req_b].bytesize >= creq[:headz][:content_length].to_i
|
218
|
+
creq[:body_complete] = true
|
219
|
+
creq[:req_b] = creq[:req_b].bytes.slice(0, creq[:headz][:content_length].to_i).pack 'c*'
|
220
|
+
end
|
221
|
+
else
|
222
|
+
creq[:got_first_line] = true
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Build without Body
|
227
|
+
# Marks Request Body as Complete if no Body should be present (no content-length & no chunked encoding)
|
228
|
+
# @param [Hash] creq Client Request Context
|
229
|
+
def self.build_no_body creq
|
230
|
+
|
231
|
+
# Check whether Body should be present
|
232
|
+
return if expects_body? creq
|
233
|
+
|
234
|
+
# Mark No Body
|
235
|
+
creq[:body_complete] = true
|
236
|
+
end
|
237
|
+
|
238
|
+
# Is Content-Length?
|
239
|
+
# Determines whether request is bound by a content-length header.
|
240
|
+
# @param [Hash] creq Client Request Context
|
241
|
+
# @return [Boolean]
|
242
|
+
def self.is_content_len? creq
|
243
|
+
creq[:headz].try(:[], :content_length) && (!(creq[:headz].try(:[], :transfer_encoding)) || (creq[:headz].try(:[], :transfer_encoding) == 'identity'))
|
244
|
+
end
|
245
|
+
|
246
|
+
# Is Chunked?
|
247
|
+
# Determines whether request is chunked (as per transfer-encoding header).
|
248
|
+
# @param [Hash] creq Client Request Context
|
249
|
+
# @return [Boolean]
|
250
|
+
def self.is_chunked? creq
|
251
|
+
creq[:headz].try(:[], :transfer_encoding) && (creq[:headz].try(:[], :transfer_encoding) != 'identity')
|
252
|
+
end
|
253
|
+
|
254
|
+
# Expects Body?
|
255
|
+
# Determines whether request expects a body (according to headers)
|
256
|
+
# @param [Hash] creq Client Request Context
|
257
|
+
# @return [Boolean]
|
258
|
+
def self.expects_body? creq
|
259
|
+
is_content_len?(creq) || is_chunked?(creq)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
data/lib/nut/response.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Nut
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'uri'
|
6
|
+
require 'cgi'
|
7
|
+
require 'typor'
|
8
|
+
require 'rxio'
|
9
|
+
|
10
|
+
# Internal Includes
|
11
|
+
require 'nut/version'
|
12
|
+
require 'nut/codes'
|
13
|
+
require 'nut/type_symbols'
|
14
|
+
|
15
|
+
# Nut Module
|
16
|
+
module Nut
|
17
|
+
|
18
|
+
# Response Module
|
19
|
+
module Response
|
20
|
+
|
21
|
+
# Build Response
|
22
|
+
# Constructs a complete Response from a preliminary Response Hash (code, body)
|
23
|
+
# @param [Hash] response Preliminary Response Information
|
24
|
+
def self.build_response response
|
25
|
+
|
26
|
+
# Set Response Code
|
27
|
+
response[:code] ||= 200
|
28
|
+
|
29
|
+
# Determine Content-Type
|
30
|
+
response[:type] = TYPE_SYMBOLS[response[:type]] if response[:type].is_a? Symbol
|
31
|
+
response[:type] ||= Typor.data response[:body]
|
32
|
+
|
33
|
+
# Prepare Headers
|
34
|
+
response[:headers] = {
|
35
|
+
'Server': "#{SERVER_NAME} #{VERSION}",
|
36
|
+
'Content-Type': response[:type],
|
37
|
+
'Content-Length': response[:body].bytesize
|
38
|
+
}
|
39
|
+
|
40
|
+
# Handle Redirects
|
41
|
+
if response[:redirect]
|
42
|
+
|
43
|
+
# Allow '301 Moved' permanent redirects (default to '302 Found' Redirects)
|
44
|
+
response[:code] = response[:redirect].is_a?(String) ? 301 : (response[:redirect][:permanent] ? 302 : 301)
|
45
|
+
|
46
|
+
# Set Target
|
47
|
+
response[:headers]['Location'] = response[:redirect].is_a?(String) ? response[:redirect] : response[:redirect][:to]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Compile Response
|
51
|
+
compile_response response
|
52
|
+
end
|
53
|
+
|
54
|
+
# Compile Response
|
55
|
+
# Sends out an HTTP Response to the Client.
|
56
|
+
# @param [Hash] response Response Hash
|
57
|
+
def self.compile_response response
|
58
|
+
"HTTP/1.1 #{response[:code]} #{CODES[response[:code]]}\r\n" +
|
59
|
+
response[:headers].collect { |k, v| "#{k}: #{v}\r\n" }.join +
|
60
|
+
"\r\n" + response[:body]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/nut/service.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Nut
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'uri'
|
6
|
+
require 'cgi'
|
7
|
+
require 'rxio'
|
8
|
+
|
9
|
+
# Internal Includes
|
10
|
+
require 'nut/handler'
|
11
|
+
|
12
|
+
# Nut Module
|
13
|
+
module Nut
|
14
|
+
|
15
|
+
# Service Class
|
16
|
+
class Service < RxIO::Service
|
17
|
+
|
18
|
+
# Defaults
|
19
|
+
DEFAULT_ADDR = 'localhost'
|
20
|
+
DEFAULT_PORT = 23280
|
21
|
+
|
22
|
+
# Attribute Access
|
23
|
+
attr_reader :request_handler
|
24
|
+
|
25
|
+
# Construct
|
26
|
+
# Builds a *Service* set to listen for incoming connections @ _addr_ on _port_.
|
27
|
+
# @param [String] addr
|
28
|
+
# @param [Fixnum] port
|
29
|
+
# @param [Module] request_handler
|
30
|
+
def initialize addr = DEFAULT_ADDR, port = DEFAULT_PORT, request_handler
|
31
|
+
|
32
|
+
# Super
|
33
|
+
super addr, port, Handler
|
34
|
+
|
35
|
+
# Set Request Handler
|
36
|
+
@request_handler = request_handler
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/nut/version.rb
ADDED
data/lib/nut.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Nut
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'aromat'
|
6
|
+
require 'rxio'
|
7
|
+
|
8
|
+
# Internal Includes
|
9
|
+
require 'nut/version'
|
10
|
+
require 'nut/codes'
|
11
|
+
require 'nut/type_symbols'
|
12
|
+
require 'nut/request'
|
13
|
+
require 'nut/handler'
|
14
|
+
require 'nut/service'
|
15
|
+
|
16
|
+
# Nut Module
|
17
|
+
# Root Module for Nut
|
18
|
+
module Nut
|
19
|
+
|
20
|
+
# Server Name (as served back in Response Headers)
|
21
|
+
SERVER_NAME = 'Nut HTTP Server'
|
22
|
+
end
|
data/nut.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'nut/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'nut'
|
8
|
+
spec.version = Nut::VERSION
|
9
|
+
spec.authors = ['Eresse']
|
10
|
+
spec.email = ['eresse@eresse.net']
|
11
|
+
|
12
|
+
spec.summary = 'Miniature Reactive HTTP Server'
|
13
|
+
spec.description = 'Provides an HTTP Server implementation based on RxIO.'
|
14
|
+
spec.homepage = 'http://redmine.eresse.net/projects/nut'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler'
|
21
|
+
spec.add_development_dependency 'rake'
|
22
|
+
spec.add_runtime_dependency 'minitest'
|
23
|
+
spec.add_runtime_dependency 'aromat'
|
24
|
+
spec.add_runtime_dependency 'typor'
|
25
|
+
spec.add_runtime_dependency 'rxio'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nut
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eresse
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-03-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: aromat
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: typor
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rxio
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Provides an HTTP Server implementation based on RxIO.
|
98
|
+
email:
|
99
|
+
- eresse@eresse.net
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- Gemfile
|
106
|
+
- LICENSE.txt
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- lib/nut.rb
|
110
|
+
- lib/nut/codes.rb
|
111
|
+
- lib/nut/handler.rb
|
112
|
+
- lib/nut/request.rb
|
113
|
+
- lib/nut/response.rb
|
114
|
+
- lib/nut/service.rb
|
115
|
+
- lib/nut/type_symbols.rb
|
116
|
+
- lib/nut/version.rb
|
117
|
+
- nut.gemspec
|
118
|
+
homepage: http://redmine.eresse.net/projects/nut
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.5.1
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Miniature Reactive HTTP Server
|
142
|
+
test_files: []
|