nut 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []