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 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
@@ -0,0 +1,3 @@
1
+ .idea
2
+ Gemfile.lock
3
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+ require 'bundler/gem_tasks'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/test*.rb'
7
+ end
8
+
9
+ task :default => :test
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,13 @@
1
+ # Nut
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # Nut Module
5
+ module Nut
6
+
7
+ # Shortcut Symbols for Content-Types
8
+ TYPE_SYMBOLS = {
9
+ json: 'application/json',
10
+ text: 'text/plain',
11
+ html: 'text/html'
12
+ }
13
+ end
@@ -0,0 +1,9 @@
1
+ # Nut
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # Nut Module
5
+ module Nut
6
+
7
+ # Version
8
+ VERSION = '0.1.0'
9
+ end
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: []