neorack 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +58 -0
- data/Rakefile +6 -0
- data/SPEC.md +650 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/extensions/hijack.md +37 -0
- data/lib/neorack.rb +7 -0
- data/lib/neorack/builder.rb +102 -0
- data/lib/neorack/version.rb +3 -0
- data/neorack.gemspec +29 -0
- data/neorack_logo.png +0 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e8a5de5634584563b6e83b7e14a1cb7764f8cd5f45f7728f279c4044b0e7076d
|
4
|
+
data.tar.gz: d550f93859ddd7d803d1631d194d3e7d05fa248752403238bcf1cc9be39d1d15
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c30238d35ed24d54c7de16bf66a1e0386f27b469dfdc395f4a5d370d7b0afee8a10c8c3815a4bacb587bc52f41a618acf68d4cb60ffdd7afa04647df439a3081
|
7
|
+
data.tar.gz: 4318adb128db67a97446ee4b2f4d54d21db13a0bd22890103a702806140afc7d1be2aa1301c23c005982e2c09f66274ac42aaf0a89d98a3c41d633c81f6694b3
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at bo@bowild.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: https://contributor-covenant.org
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Boaz Segev
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# NeoRack
|
2
|
+
[![NeoRack logo](neorack_logo.png)](SPEC.md)
|
3
|
+
|
4
|
+
[Rack](https://github.com/rack/rack) is great and I love all it's done for Ruby. It made us all stronger, gave us a unified platform and saved us countless developer hours and time.
|
5
|
+
|
6
|
+
However, Rack's design is showing its age... its CGI model has shortcomings that we can all sort of mitigate and code around, but cost us developer hours and often leave us with degraded performance.
|
7
|
+
|
8
|
+
The NeoRack specification is designed to offer a solution for these shortcomings by:
|
9
|
+
|
10
|
+
* Making long-polling, streaming and long requests first class citizens.
|
11
|
+
|
12
|
+
* Supporting server feature testing during startup and application buildup (in addition to during response execution).
|
13
|
+
|
14
|
+
* Supporting server extensions that can be implemented by either the server or external gems.
|
15
|
+
|
16
|
+
* Supporting (optional) backwards compatibility with Rack.
|
17
|
+
|
18
|
+
My hope is that one day NeoRack and Rack could be merged in a way that makes developers happy and advanced web applications easy to author.
|
19
|
+
|
20
|
+
Please read the [NeoRack specifications](SPEC.md) for details.
|
21
|
+
|
22
|
+
---
|
23
|
+
|
24
|
+
# The NeoRack Gem
|
25
|
+
|
26
|
+
Currently the gem is mostly a placeholder for a gem that will slowly fill up with common helpers.
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Add this line to your application's Gemfile:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem 'neorack'
|
34
|
+
```
|
35
|
+
|
36
|
+
And then execute:
|
37
|
+
|
38
|
+
$ bundle install
|
39
|
+
|
40
|
+
Or install it yourself as:
|
41
|
+
|
42
|
+
$ gem install neorack
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
The only available module at the moment is the `NeoRack::Builder` that loads script files, offering them the DSL listed in the [NeoRack specifications](SPEC.md#neorack-application-scripts).
|
47
|
+
|
48
|
+
## Contributing
|
49
|
+
|
50
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/boazsegev/neorack. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/boazsegev/neorack/blob/master/CODE_OF_CONDUCT.md).
|
51
|
+
|
52
|
+
## License
|
53
|
+
|
54
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
55
|
+
|
56
|
+
## Code of Conduct
|
57
|
+
|
58
|
+
Everyone interacting in the NeoRack project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/boazsegev/neorack/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/SPEC.md
ADDED
@@ -0,0 +1,650 @@
|
|
1
|
+
# NeoRack Specification
|
2
|
+
|
3
|
+
NeoRack define a protocol / API for web application servers and applications to communicate with each other.
|
4
|
+
|
5
|
+
The following are the NeoRack Application, Server and Extension specifications.
|
6
|
+
|
7
|
+
NeoRack Applications, Servers and Extensions **MUST** follow in order to be considered conforming.
|
8
|
+
|
9
|
+
Note that NeoRack Applications, Servers and Extensions are **NOT** required to include any specific NeoRack related gem in order to conform to these specifications.
|
10
|
+
|
11
|
+
## NeoRack Applications
|
12
|
+
|
13
|
+
A NeoRack application is any Ruby object that responds to the method `call` where the `call` method accepts two arguments (`request`, `response`). i.e.,
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
APP = Proc.new {|request, response| response << "Hello World" }
|
17
|
+
```
|
18
|
+
|
19
|
+
For backwards compatibility reasons, a NeoRack Application that returns while both `response.streaming?` and `response.finished?` would return `false` **SHOULD NOT** return an `Array` instance object as its final value - otherwise the returned value might be processed.
|
20
|
+
|
21
|
+
If both `response.streaming?` and `response.finished?` return `false` after the Application returns, the Server **MUST** call `response.finish`.
|
22
|
+
|
23
|
+
i.e., in pseudo code:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
r = APP.call(request, response)
|
27
|
+
unless response.streaming? || response.finished?
|
28
|
+
process_backwards_compatible_response(r) if r.is_a?(Array)
|
29
|
+
response.finish
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## The NeoRack Request Object
|
34
|
+
|
35
|
+
The NeoRack Request Object (`request`) **SHOULD** be an instance of a class that inherits from `Hash` (but is **NOT** an instance of `Hash`) and **MUST** implement the following "Hash-like" methods: `[]`, `[]=`, `each`, `size`, `has_key?` and `merge!` in the same way they are implemented by `Hash`.
|
36
|
+
|
37
|
+
The `request` object **MAY** be used by Applications and/or Extensions to store and/or communicate additional data relevant to the request.
|
38
|
+
|
39
|
+
**Note**: during Server initialization, an application might `extend` or `include` its own modules into the Server's Request class. For this reason it is better if that class is not the Hash class itself.
|
40
|
+
|
41
|
+
### Request methods
|
42
|
+
|
43
|
+
The `request` object **MUST** respond to the following methods:
|
44
|
+
|
45
|
+
#### `server`
|
46
|
+
|
47
|
+
Returns the Server class / object that called the Application object.
|
48
|
+
|
49
|
+
### Request key-value pairs
|
50
|
+
|
51
|
+
The Server **MUST** set following key-value pairs in the NeoRack Request object. All keys are Symbols (not Strings).
|
52
|
+
|
53
|
+
#### `:VERSION_SPEC`
|
54
|
+
|
55
|
+
The version for this specification as a three member Array of Numbers, currently `[0,0,1]`.
|
56
|
+
|
57
|
+
#### `:VERSION`
|
58
|
+
|
59
|
+
The HTTP version String as reported by the client. For HTTP/2 and HTTP/3 (QUIC) use `'HTTP/2'` and `'HTTP/3'` respectively.
|
60
|
+
|
61
|
+
#### `:secure`
|
62
|
+
|
63
|
+
**MUST** be set to false unless the server itself handles TLS/SSL encryption for this request / connection.
|
64
|
+
|
65
|
+
#### `:scheme`
|
66
|
+
|
67
|
+
Depending on the request URL and environment, usually `http`, `ws`, `https` or `wss`.
|
68
|
+
|
69
|
+
#### `:method`
|
70
|
+
|
71
|
+
The HTTP method used for the request, set as a String instance (`'HEAD'`, `'GET'`, `'POST'`, `'UPDATE'`, etc').
|
72
|
+
|
73
|
+
#### `:query`
|
74
|
+
|
75
|
+
The portion of the request URL that follows the `'?'`, if any. **MUST** be a String (even if empty).
|
76
|
+
|
77
|
+
#### `:path`
|
78
|
+
|
79
|
+
The portion of the request URL before the query and after the host name and optional authentication details.
|
80
|
+
|
81
|
+
**MUST** be a String. **MUST** start with the `'/'` character (even if the original path was an empty String).
|
82
|
+
|
83
|
+
Servers that support multiple Application objects **MAY** change this value in a documented manner.
|
84
|
+
|
85
|
+
#### `:path_root`
|
86
|
+
|
87
|
+
The portion of the request URL path that was extracted from the beginning of the `:path` String or an empty String.
|
88
|
+
|
89
|
+
**MUST** be a String (even if empty).
|
90
|
+
|
91
|
+
**MUST NOT** end with `'/'`.
|
92
|
+
|
93
|
+
Unless empty, **MUST** start with a `'/'`.
|
94
|
+
|
95
|
+
Servers that support multiple Application objects **SHOULD** document how `path_root` is used.
|
96
|
+
|
97
|
+
**Note**: it **MUST** be possible to construct a valid URL that routes to the same Application object using:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
url = "#{request[:scheme]}://#{request['host']}#{request[:path_root]}#{request[:path]}?#{request[:query]}"
|
101
|
+
```
|
102
|
+
|
103
|
+
#### `:body`
|
104
|
+
|
105
|
+
An HTTP payload (body) object (see details further on).
|
106
|
+
|
107
|
+
If a payload exists, this value **MUST** be set. Otherwise this **MAY** be set to `nil`, left unset or point to an empty body object.
|
108
|
+
|
109
|
+
### HTTP Request Headers
|
110
|
+
|
111
|
+
All HTTP headers, **MUST** be set as key-value pairs in the `request` where the key is a **String** and the value is either a String or an Array of Strings.
|
112
|
+
|
113
|
+
Header names **MUST** be converted into their lower case equivalent before the key-value pair is set (i.e., `"Content-Length"` MUST be converted to `"content-length"`).
|
114
|
+
|
115
|
+
Unless the Server implements the `:backwards_compatible` extension (see further on), HTTP headers **MUST** be the **ONLY** String keys in the `request` set by the Server or by any of its Extension. Otherwise, it might make it impossible to implemented a `:backwards_compatible` extension.
|
116
|
+
|
117
|
+
When a header arrives only once, it's value **MUST** be set as a String instance object.
|
118
|
+
|
119
|
+
Headers that arrive more than once **SHOULD** be set as an Array of Strings, ordered by header value arrival (i.e., `headers['cache-control'] = ['no-cache', 'no-store']`)
|
120
|
+
|
121
|
+
**ONLY IF** [the HTTP protocols allows for this variation](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2), Servers **MAY** also merge header values into a single comma separated value String instance or separate a single header into an Array of Strings.
|
122
|
+
|
123
|
+
NeoRack Applications and Extensions **MUST** be prepared to handle both variations (Array / String).
|
124
|
+
|
125
|
+
### HTTP Request Body
|
126
|
+
|
127
|
+
The NeoRack Request Body (`request[:body]`) is an IO-like object which contains the raw HTTP POST data (payload / body). It is backwards compatible with the original Rack specification with the addition of the methods `length`.
|
128
|
+
|
129
|
+
When no HTTP payload / body was received, `request[:body]` **SHOULD** be `nil`, but **MAY** be a Request Body Object that maps to an empty String.
|
130
|
+
|
131
|
+
The `request[:body]` object (if set) **MUST** respond to IO style methods `gets`, `each`, `read`, `rewind` and `length`. These methods **MUST** behave the same way as described by [the Ruby IO documentation](https://ruby-doc.org/core-2.7.1/IO.html). The following clarifications were pretty much copied from [the original Rack specification](https://github.com/rack/rack/blob/master/SPEC.rdoc):
|
132
|
+
|
133
|
+
#### `gets`
|
134
|
+
|
135
|
+
**MUST** be called without arguments and Reads the next "line" from the body object. It returns either a String (on success), or `nil` (on `EOF`).
|
136
|
+
|
137
|
+
#### `read(length = nil, buffer = nil)`
|
138
|
+
|
139
|
+
If given, `length` **MUST** be a non-negative Integer (>= 0) or `nil`,
|
140
|
+
|
141
|
+
If `length` is given and not `nil`, then this method reads at most `length` bytes from the body object.
|
142
|
+
|
143
|
+
If `length` is not given or `nil`, then this method reads all data until `EOF`.
|
144
|
+
|
145
|
+
When `EOF` is reached, this method returns `nil` if `length` is given and not `nil`, or `""` if `length` is not given or is `nil`.
|
146
|
+
|
147
|
+
If given, `buffer` **MUST** be a String.
|
148
|
+
|
149
|
+
If `buffer` is given, then the read data will be placed into `buffer` instead of a newly created String object.
|
150
|
+
|
151
|
+
#### `each`
|
152
|
+
|
153
|
+
**MUST** be called without arguments and only yield one String instance object per iteration.
|
154
|
+
|
155
|
+
#### `rewind`
|
156
|
+
|
157
|
+
**MUST** be called without arguments. It rewinds the body object back to the beginning. It **MUST NOT** raise `Errno::ESPIPE`: that is, it may not be a pipe or a socket. Therefore, developers **MUST** buffer the input data into some rewindable object if the underlying body object is not rewindable.
|
158
|
+
|
159
|
+
#### `length`
|
160
|
+
|
161
|
+
Returns the length of the data in the body object.
|
162
|
+
|
163
|
+
**Note**: Applications **SHOULD NOT** access the `'content-length'` header using the request object and **SHOULD** prefer to access this data using the `request[:body]` object (`request[:body].length`).
|
164
|
+
|
165
|
+
#### `close`
|
166
|
+
|
167
|
+
**MUST NOT** be called on the body object except by the Server.
|
168
|
+
|
169
|
+
## The NeoRack Response Object
|
170
|
+
|
171
|
+
The NeoRack Response Object (`response`) manages the response data, including the response status, header data and body (payload).
|
172
|
+
|
173
|
+
### NeoRack Response Methods
|
174
|
+
|
175
|
+
The NeoRack Response Object **MUST** respond to the following methods:
|
176
|
+
|
177
|
+
#### `status`
|
178
|
+
|
179
|
+
Returns the response status.
|
180
|
+
|
181
|
+
#### `status=`
|
182
|
+
|
183
|
+
Sets the response status.
|
184
|
+
|
185
|
+
**SHOULD** raise an exception if either `streaming?` or `finished?` would have returned `true`.
|
186
|
+
|
187
|
+
#### `add_header(name, value)`
|
188
|
+
|
189
|
+
Adds a header to the response. If the header exists, the header will be sent multiple times (i.e., `'set-cookie'`).
|
190
|
+
|
191
|
+
If either `name` or `value` are `nil`, does nothing (returns, no exception is raised).
|
192
|
+
|
193
|
+
`name` and `value` **MUST** be either `nil`, a String object or a Number. Otherwise an `ArgumentError` exception **MUST** be raised.
|
194
|
+
|
195
|
+
The header `name` **SHOULD** be lowercase, but Servers **SHOULD** expect application to be inconsistent in this regard.
|
196
|
+
|
197
|
+
The Server **MAY** perform any additional action, such as sending an *Early Hints* responses or invoking an HTTP/2 *push promise* request for `'link'` headers, attempt to auto-correct or rewrite header data, etc'. Such behavior **SHOULD** be documented.
|
198
|
+
|
199
|
+
**MUST** raise an exception if either `streaming?` or `finished?` would have returned `true`.
|
200
|
+
|
201
|
+
#### `reset_header(name, value = nil)`
|
202
|
+
|
203
|
+
If `name` is `nil`, does nothing (returns, no exception is raised).
|
204
|
+
|
205
|
+
Deletes the header from the response if the header exists.
|
206
|
+
|
207
|
+
If `value` is set, calls `add_header(name, value)`
|
208
|
+
|
209
|
+
**MUST** raise an exception if either `streaming?` or `finished?` would have returned `true`.
|
210
|
+
|
211
|
+
#### `set_cookie(name, value, options={})`
|
212
|
+
|
213
|
+
If `name` is `nil`, does nothing (returns, no exception is raised).
|
214
|
+
|
215
|
+
If `value` is `nil`, sets the cookie to be deleted.
|
216
|
+
|
217
|
+
If both `name` and `value` are set, adds a cookie to the response. If the cookie exists, it is overwritten.
|
218
|
+
|
219
|
+
**MUST** raise an exception if either `streaming?` or `finished?` would have returned `true`.
|
220
|
+
|
221
|
+
Servers **SHOULD NOT** encode cookie data (names / value). Encoding the data is the Application's responsibility and choice.
|
222
|
+
|
223
|
+
`name` and `value` **MUST** be either `nil` or a String object. Otherwise an `ArgumentError` exception **MUST** be raised. **Note**: empty String objects are valid, but **SHOULD** be avoided by the Applications.
|
224
|
+
|
225
|
+
If `name` is an invalid cookie name or `value` is an invalid cookie value, behavior is undefined (see [here](https://tools.ietf.org/html/rfc6265) and [here](https://stackoverflow.com/questions/1969232/what-are-allowed-characters-in-cookies)). The Server **MAY** attempt to auto-correct the issue, raise and exception, quietly fail, drop the connection, etc'.
|
226
|
+
|
227
|
+
The `option` hash **MUST** recognize the following Symbols for setting cookies (see also the [`Set-Cookie` header details](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)):
|
228
|
+
|
229
|
+
* `:path` - (String) A path that must exist in the requested URL, or the browser won't send the Cookie header.
|
230
|
+
|
231
|
+
* `:domain` - (String) The domain for which this cookie applies. Valid a specific domain String object (i.e. `'example.com'`). Note that subdomains are always allowed.
|
232
|
+
|
233
|
+
* `:same_site` - (Boolean) If set, **MUST** be either `:strict`, `:lax` or `:none`. [Read details here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie).
|
234
|
+
|
235
|
+
* `:secure` - (Boolean) A secure cookie is only sent to the server when a request is made with the `https` scheme.
|
236
|
+
|
237
|
+
* `:http_only` - (Boolean) If `true`, forbids JavaScript from accessing the cookie through a script (i.e, using `document.cookie`).
|
238
|
+
|
239
|
+
**Note**: The cookie is still sent with JavaScript originated requests.
|
240
|
+
|
241
|
+
* `:max_age` - (Number) The number of seconds until the cookie expires. A zero or negative number will expire the cookie immediately.
|
242
|
+
|
243
|
+
* `:expires` - (Number / Time) **SHOULD** be considered deprecated by Application developers. It is only provided for compatibility purposes.
|
244
|
+
|
245
|
+
`:expires` is the maximum "due date" lifetime for the cookie as either a number (Unix style time stamp) or a Ruby Time instance object. If provided, servers **MUST** translate the time stamp into an HTTP Date format.
|
246
|
+
|
247
|
+
If both `:max_age` and `:expires` are set, `:max_age` **MUST** be preferred and `:expires` **SHOULD** be ignored. Servers **MAY** send both values (`Max-Age` always has precedence).
|
248
|
+
|
249
|
+
If only `:expires` is set, the Server **MAY** convert `:expires` to `:max_age` values.
|
250
|
+
|
251
|
+
**Note**: if both `:max_age` and `:expires` are missing, the cookie will be considered a session cookie by the client.
|
252
|
+
|
253
|
+
#### `write(data, offset = 0, length = nil)`
|
254
|
+
|
255
|
+
**MUST** raise an exception if `finished?` would have returned `true`.
|
256
|
+
|
257
|
+
If `streaming?` is `false`, the Server **MUST**:
|
258
|
+
|
259
|
+
* Add the data to be sent to the response payload (body), unless `request[:method] == 'HEAD'`.
|
260
|
+
|
261
|
+
If `streaming?` is `true`, the Server **MUST**:
|
262
|
+
|
263
|
+
* Unless previously performed:
|
264
|
+
|
265
|
+
* Update the `'transfer-encoding'` header as required by the protocol, unless the `'content-length'` header was previously set or the server knows the final length of the data to be sent.
|
266
|
+
|
267
|
+
* Validate and send the status and header data.
|
268
|
+
|
269
|
+
* If `request[:method] == 'HEAD'`, return at this point without further processing.
|
270
|
+
|
271
|
+
* Add the data to be sent to the response payload (body).
|
272
|
+
|
273
|
+
* Send any pending data in the response payload / body (or schedule to do so) using the proper encoding scheme (streaming responses my need to be `chunked`).
|
274
|
+
|
275
|
+
**Note**: depending on their design, Servers **MAY** block to until the data was actually sent through the socket.
|
276
|
+
|
277
|
+
Returns `self` (the `response` object).
|
278
|
+
|
279
|
+
`data` **MUST** either:
|
280
|
+
|
281
|
+
* Be a String instance object.
|
282
|
+
|
283
|
+
* Respond to both `to_path` and `close`, allowing the Server to `close` the object and send it from disk (possibly in that order).
|
284
|
+
|
285
|
+
* Respond to both `fileno` and `close`, allowing the Server to take ownership of the IO device and stream it.
|
286
|
+
|
287
|
+
If `offset` is set, the first `offset` bytes in the `data` object are ignored (not sent). `offset` **MUST** be a Number.
|
288
|
+
|
289
|
+
If `length` is set, up to `length` bytes from the `data` object will be sent. `length` **MUST** be either `nil` or a Number.
|
290
|
+
|
291
|
+
Servers **MUST** test the `length` value for overflow whenever possible (may not be always possible). If `length` overflows it is **NOT** an error, but it could cause network errors that the Server **MUST** properly handle or require that the response be streamed.
|
292
|
+
|
293
|
+
#### `<<`
|
294
|
+
|
295
|
+
The `<<` method is an alias to `write`.
|
296
|
+
|
297
|
+
#### `stream`
|
298
|
+
|
299
|
+
**MUST** raise an exception if `finished?` would have returned `true`.
|
300
|
+
|
301
|
+
Sets the response to streaming.
|
302
|
+
|
303
|
+
Returns `self` (the `response` object).
|
304
|
+
|
305
|
+
If `write` was previously called, **MUST** perform all necessary operations as if the call to `write` was performed after the call to `stream` - i.e., update the proper `'transfer-encoding'` header, validate and send the status and header data, etc'.
|
306
|
+
|
307
|
+
#### `streaming?`
|
308
|
+
|
309
|
+
**MUST** return `false` unless both `stream` was previously called and `finished?` is `false`.
|
310
|
+
|
311
|
+
Otherwise, **MUST** return `true`.
|
312
|
+
|
313
|
+
#### `finish(data = nil, offset = 0, length = nil)`
|
314
|
+
|
315
|
+
**SHOULD** raise an exception if `finished?` would have returned `true`, but **MAY** quietly fail without doing anything.
|
316
|
+
|
317
|
+
The Server **MUST**:
|
318
|
+
|
319
|
+
* Call `write` if `data` isn't `nil`.
|
320
|
+
|
321
|
+
**Note**: if `stream` was previously called but no data was previously sent (`write` wasn't called previously), the Server **SHOULD** behave as if the `stream` method was never called (i.e., set `'content-length'` rather than stream, if possible).
|
322
|
+
|
323
|
+
* Unless previously performed, validate and send the status and header data.
|
324
|
+
|
325
|
+
* Unless `request[:method] == 'HEAD'`, send any pending data in the response payload / body (or schedule to do so) using the proper encoding scheme.
|
326
|
+
|
327
|
+
* Call (or schedule) all the `run_after` callbacks.
|
328
|
+
|
329
|
+
#### `finished?`
|
330
|
+
|
331
|
+
**MUST** return `false` **unless** either `finish` or `cancel` were called.
|
332
|
+
|
333
|
+
Otherwise, **MUST** return `true`.
|
334
|
+
|
335
|
+
#### `cancel(error_code)`
|
336
|
+
|
337
|
+
Cancels the response object, returning the response management to the Server.
|
338
|
+
|
339
|
+
If possible, the Server **MUST** send an appropriate error response.
|
340
|
+
|
341
|
+
**SHOULD** raise an exception if `error_code` is less than 400 or grater than 599 (`< 400 || >= 600`).
|
342
|
+
|
343
|
+
**MUST** raise an exception if either `streaming?` or `finished?` would have returned `true`.
|
344
|
+
|
345
|
+
## NeoRack Servers
|
346
|
+
|
347
|
+
A NeoRack Server **MUST**, at the very least, handle network and HTTP protocol details to the minimal level required to implement the NeoRack `request` and NeoRack `response` objects and call a NeoRack Application object.
|
348
|
+
|
349
|
+
A NeoRack Server **MUST** accept at least one NeoRack Applications and forward a `request` and `response` object to the Application.
|
350
|
+
|
351
|
+
If a NeoRack Server accepts more than a single NeoRack Application, the Server **SHOULD** document how it selects the Application object.
|
352
|
+
|
353
|
+
Servers **SHOULD** implement the `hijack` extension if possible. It **SHOULD NOT** be used by applications, but it often is.
|
354
|
+
|
355
|
+
### Server methods
|
356
|
+
|
357
|
+
A NeoRack Server **MUST** implement the following methods in the Server object returned by a call to `request.server`:
|
358
|
+
|
359
|
+
#### `forking?`
|
360
|
+
|
361
|
+
**MUST** be set to `true` **ONLY IF** the Server's concurrency model expects to use `fork` (i.e., workers processes or a process-per-connection). If this is not the case, returns `false`.
|
362
|
+
|
363
|
+
This allows Extensions and Applications know if some features require IPC (Inter Process Communication).
|
364
|
+
|
365
|
+
#### `blocking?`
|
366
|
+
|
367
|
+
**MUST** be set to `true` **ONLY IF** the Server's concurrency model allows execution to block without negative side-effects, i.e., process/thread/fiber-per-connection design patterns.
|
368
|
+
|
369
|
+
#### `extensions`
|
370
|
+
|
371
|
+
Returns a Hash object that maps extension name Symbols to version number arrays. **MAY** be an empty Hash.
|
372
|
+
|
373
|
+
Version number arrays **MUST** be frozen Array who's first three members are Numbers that follow [semantic versioning](https://semver.org).
|
374
|
+
|
375
|
+
i.e.: `request[:SERVER].extensions[:rack_compatible] # => [1,3,0]`
|
376
|
+
|
377
|
+
#### `classes`
|
378
|
+
|
379
|
+
Returns a Hash object where Symbol keys are mapped to Class objects (not Class instances).
|
380
|
+
|
381
|
+
The Server MUST set the following key-value pairs:
|
382
|
+
|
383
|
+
* `:request` **MUST** map to the Class used for the Server's `request` objects.
|
384
|
+
|
385
|
+
* `:response` **MUST** map to the Class used for the Server's `response` objects.
|
386
|
+
|
387
|
+
* `:concurrency` **MUST** map to either `Thread` or `Fiber`, allowing the Server to hint at the preferred way for the Application to handle concurrent processing. Servers **SHOULD** set this value to `Thread` unless they are `Fiber` scheduling aware.
|
388
|
+
|
389
|
+
This **MAY** be used to test for and implement extensions. i.e.:
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
# an Application could call this or a similar line using the DSL (see later) to add features to the Response object
|
393
|
+
server.classes(:response).include MyNeoRackHelpers unless server.extensions[:common_helpers] && server.extensions[:common_helpers][0] == 1
|
394
|
+
```
|
395
|
+
|
396
|
+
Extensions to this specification **MAY** require additional names to be added as long as these names are unique.
|
397
|
+
|
398
|
+
## NeoRack Extensions
|
399
|
+
|
400
|
+
NeoRack Servers **MAY** be extended either internally, through the Server supporting an extension, or externally, through setting up middleware and/or adding mixins to the published classes and updating the Server's `extensions` and `classes`.
|
401
|
+
|
402
|
+
Extensions registered in the NeoRack repository will have their specifications published in the `extensions` folder and **MUST** specify a unique name for the Servers `extensions` Hash, specify a [semantic versioning](https://semver.org) compliant 3 numbered version array and be mature.
|
403
|
+
|
404
|
+
Developers may also ask to register extension drafts to be placed in the `extensions/drafts` folder and request community feedback.
|
405
|
+
|
406
|
+
### External Extensions
|
407
|
+
|
408
|
+
External extensions **SHOULD** implement a `register` method that accepts at least one argument - the `self` object of the DSL execution environment.
|
409
|
+
|
410
|
+
The following is a made-up logging extension implementation example that should be considered as pseudo code (and is actually "middleware" in nature):
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
# Place module somewhere
|
414
|
+
module MyLoggingExtension
|
415
|
+
# registers the extension from the DLS
|
416
|
+
def self.register(dsl)
|
417
|
+
return if server.extensions[:logging_example]
|
418
|
+
dsl.server.extensions[:logging_example] = [0,0,1]
|
419
|
+
dsl.run_before(self.method :on_start)
|
420
|
+
dsl.run_after(self.method :on_finish)
|
421
|
+
end
|
422
|
+
# Marks time
|
423
|
+
def self.on_start(request, response)
|
424
|
+
request[:STARTED_AT] = Time.now
|
425
|
+
end
|
426
|
+
# Prints log
|
427
|
+
def self.on_finish(request)
|
428
|
+
delta = Time.now - request[:STARTED_AT]
|
429
|
+
puts "HTTP request %s%s took %.4fs"%(request[:path_root], request[:path], delta)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# in the DSL, require the module and perform:
|
434
|
+
MyLoggingExtension.register(self)
|
435
|
+
```
|
436
|
+
|
437
|
+
## Starting up a NeoRack Server and Application
|
438
|
+
|
439
|
+
A NeoRack Server **MUST** document its startup and shutdown procedures.
|
440
|
+
|
441
|
+
A NeoRack Server **MAY** offer a Ruby API for setting up and/or running and/or managing the Server from within Ruby.
|
442
|
+
|
443
|
+
A NeoRack Server **SHOULD** implement a CLI (Command Line Interface) and **SHOULD** expose as many options through its CLI, minimizing the need for server specific setup files / code.
|
444
|
+
|
445
|
+
### Common CLI Arguments
|
446
|
+
|
447
|
+
If provided, the CLI **MUST** be able to load a Ruby script as detailed in the **NeoRack Application Scripts** section. The default script name is `'config.ru'`.
|
448
|
+
|
449
|
+
It is **RECOMMENDED** that Servers automatically test for the `ADDRESS` and `PORT` environment variables if those aren't set by the command line.
|
450
|
+
|
451
|
+
It is **RECOMMENDED** that Servers recognize the first unnamed CLI option as a the script name for the NeoRack application.
|
452
|
+
|
453
|
+
The following common CLI option names are **RECOMMENDED** (but to each their own):
|
454
|
+
|
455
|
+
* `-b` - the address to listen to.
|
456
|
+
|
457
|
+
* `-p` - the port number to listen to.
|
458
|
+
|
459
|
+
* `-D` - log Debug messages, if any.
|
460
|
+
|
461
|
+
* `-url` - possible alternative to the `-b` and `-p`, allowing a URL type address for the address and port binding.
|
462
|
+
|
463
|
+
* `-l` - log HTTP requests once the response was sent (finished), if supported.
|
464
|
+
|
465
|
+
* `-k` - sets HTTP keep-alive timeout in seconds.
|
466
|
+
|
467
|
+
* `-maxbd` - sets the approximate HTTP upload limit in Mb.
|
468
|
+
|
469
|
+
* `-maxhd` - sets the approximate total header length limit per HTTP request in Kb.
|
470
|
+
|
471
|
+
* `-t` - if multi-threaded mode is supported, sets the number of threads to be used.
|
472
|
+
|
473
|
+
* `-w` - if a process worker pool is supported, sets the number of worker processes to be used.
|
474
|
+
|
475
|
+
### NeoRack Application Scripts
|
476
|
+
|
477
|
+
NeoRack Servers **SHOULD** support loading NeoRack Applications using a Ruby script (i.e., `'config.ru'`).
|
478
|
+
|
479
|
+
When loading such a script, the following methods **MUST** be made available to the script as if they were global methods (a DSL):
|
480
|
+
|
481
|
+
#### `server`
|
482
|
+
|
483
|
+
Returns the Server object, allowing access to the Server methods.
|
484
|
+
|
485
|
+
#### `run(application)`
|
486
|
+
|
487
|
+
Sets the NeoRack Application object for the current Script.
|
488
|
+
|
489
|
+
#### `run_before(proc_obj = nil, &block)`
|
490
|
+
|
491
|
+
This may be used for pre-request logic, such as authentication, database connection checkout, etc'.
|
492
|
+
|
493
|
+
Either the `proc_obj` or `block` **MUST** respond to `call(request, response)`.
|
494
|
+
|
495
|
+
Only on of these objects (`proc_obj` or `block`) will be used. `proc_obj` has precedence.
|
496
|
+
|
497
|
+
The NeoRack Server **MUST** call the object **before** calling the Application.
|
498
|
+
|
499
|
+
The NeoRack Server **MUST** call the callbacks in order of insertion.
|
500
|
+
|
501
|
+
If `response.finished?` is `true` after the Server called the object, the server **MUST** stop processing the request.
|
502
|
+
|
503
|
+
#### `run_after(proc_obj = nil, &block)`
|
504
|
+
|
505
|
+
This may be used for cleanup logic, such as removing database connections from the `request` Hash, logging, etc'.
|
506
|
+
|
507
|
+
Either the `proc_obj` or `block` **MUST** respond to `call(request)`.
|
508
|
+
|
509
|
+
Only on of these objects (`proc_obj` or `block`) will be used. `proc_obj` has precedence.
|
510
|
+
|
511
|
+
The NeoRack Server **MUST** call the object (or schedule it) when `response.finish` is called.
|
512
|
+
|
513
|
+
The NeoRack Server **MUST** call the callbacks in **reverse** order of insertion.
|
514
|
+
|
515
|
+
#### `use(middleware, *args, &block)`
|
516
|
+
|
517
|
+
Provided for for backwards compatibility with a nested middleware design pattern.
|
518
|
+
|
519
|
+
#### `warmup(proc_obj = nil, &block)`
|
520
|
+
|
521
|
+
Provided for for backwards compatibility, takes a block and/or Proc object that will be called with the final Application object.
|
522
|
+
|
523
|
+
#### Example Application Script Loader
|
524
|
+
|
525
|
+
An example application script loader is available in the NeoRack `'/gem'` folder, [in the file `'builder.rb'`](./gem/lib/neorack/builder).
|
526
|
+
|
527
|
+
Servers **MAY** roll their own, copy the code or require the `neorack` gem at their discretion.
|
528
|
+
|
529
|
+
## NeoRack Compatibility with Rack
|
530
|
+
|
531
|
+
NeoRack backwards compatibility with [the CGI style Rack specifications](https://github.com/rack/rack/blob/master/SPEC.rdoc) is considered an Extension and uses the reserved extension name `:backwards_compatible`.
|
532
|
+
|
533
|
+
Backwards compatibility extensions **MUST**:
|
534
|
+
|
535
|
+
* Set all `env` values as set in the [CGI style Rack applications](https://github.com/rack/rack/blob/master/SPEC.rdoc).
|
536
|
+
|
537
|
+
* Set the Server's `extensions[:backwards_compatible]` values to the Rack protocol version they support (i.e., `[1,3,0].freeze`).
|
538
|
+
|
539
|
+
* Set `env['neorack.request']` to the `request` object.
|
540
|
+
|
541
|
+
* Set `env['neorack.response']` to the `response` object.
|
542
|
+
|
543
|
+
Backwards compatibility extensions **SHOULD** use the `request` object for implementing the Rack style `env`.
|
544
|
+
|
545
|
+
The following is an example for an external backwards compatibility extension, it is untested, might not work and should be considered pseudo code:
|
546
|
+
|
547
|
+
```ruby
|
548
|
+
# place this extension code somewhere
|
549
|
+
module NeoRack
|
550
|
+
class BackwardsCompatibility
|
551
|
+
def self.register(dsl)
|
552
|
+
unless dsl.server.extensions[:backwards_compatible] && dsl.server.extensions[:backwards_compatible][0] == 1
|
553
|
+
dsl.server.extensions[:backwards_compatible] = [1,3,0].freeze
|
554
|
+
dsl.use(self)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
def initialize(app)
|
558
|
+
@app = app
|
559
|
+
end
|
560
|
+
|
561
|
+
def call(request, response)
|
562
|
+
add_old_env_values_to_request(request, response)
|
563
|
+
old = @app.call(request)
|
564
|
+
process_old_return_value(old, response)
|
565
|
+
end
|
566
|
+
|
567
|
+
# example implementation
|
568
|
+
def self.add_old_env_values_to_request(request, response)
|
569
|
+
new_headers = {}
|
570
|
+
request.each do |k,v|
|
571
|
+
if (k.is_a?(String) && k[0].ord >= 'a'.ord && k[0].ord <= 'a'.ord)
|
572
|
+
if k == 'content-length' || k == 'content-type'
|
573
|
+
new_headers[k.swapcase.gsub!('-', '_')] ||= v
|
574
|
+
else
|
575
|
+
new_headers["#{HTTP_}#{k.swapcase.gsub('-', '_')}"] ||= v
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
request.merge! new_headers
|
580
|
+
# set whatever Rack requires... i.e.:
|
581
|
+
request['REQUEST_METHOD'] = request[:method]
|
582
|
+
request['rack.url_scheme'] = request[:scheme]
|
583
|
+
request['SERVER_NAME'] = request['host']
|
584
|
+
request['SCRIPT_NAME'] = request[:path_root]
|
585
|
+
request['PATH_INFO'] = request[:path]
|
586
|
+
request['QUERY_STRING'] = request[:query]
|
587
|
+
request['rack.version'] = [1,3,0]
|
588
|
+
request['rack.errors'] = STDERR
|
589
|
+
if request[:body]
|
590
|
+
request['rack.input'] = request[:body]
|
591
|
+
request['CONTENT_LENGTH'] = request[:body].length.to_s
|
592
|
+
request['CONTENT_TYPE'] = request[:body].type if request[:body].type
|
593
|
+
else
|
594
|
+
request['rack.input'] = StringIO.new
|
595
|
+
end
|
596
|
+
# allow NeoRack aware apps access to these objects
|
597
|
+
request['neorack.request'] = request
|
598
|
+
request['neorack.response'] = response
|
599
|
+
# support hijack if supported
|
600
|
+
if(request.server.extensions[:hijack])
|
601
|
+
request['rack.hijack'] = Proc.new { request['rack.hijack_io'] = response.hijack(false) }
|
602
|
+
request['rack.hijack?'] = true
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
# example implementation
|
607
|
+
def self.process_old_return_value(old, response)
|
608
|
+
# do nothing if it was already done.
|
609
|
+
return nil if(response.finished? || response.streaming?)
|
610
|
+
raise "unexpected return value from application" unless (old.is_a?(Array) && old.length == 3)
|
611
|
+
# set status
|
612
|
+
response.status = old[0].to_i
|
613
|
+
# copy headers to new response object
|
614
|
+
hijacked = old[2].delete('rack.hijack')
|
615
|
+
old[1].each {|name, val| val = val.split("\n"); val.each {|v| response.add_header(name, v)} }
|
616
|
+
# handle hijacking or send body
|
617
|
+
if(hijacked && request.server.extensions[:hijack])
|
618
|
+
hijacked.call(response.hijack(true))
|
619
|
+
else
|
620
|
+
case old[2].class
|
621
|
+
when String
|
622
|
+
response << str
|
623
|
+
old[2].close if old[2].respond_to?(:close)
|
624
|
+
when Array
|
625
|
+
old[2].each {|str| response << str }
|
626
|
+
old[2].close if old[2].respond_to?(:close)
|
627
|
+
else
|
628
|
+
# start streaming the response
|
629
|
+
response.stream
|
630
|
+
# perform `each` in a new thread / fiber, as it may block the server
|
631
|
+
if(old['neorack.request'][:SERVER].blocking?)
|
632
|
+
old[2].each {|str| response << str }
|
633
|
+
response.finish
|
634
|
+
else
|
635
|
+
old['neorack.request'][:SERVER].classes[:concurrency].new do
|
636
|
+
old[2].each {|str| response << str }
|
637
|
+
response.finish
|
638
|
+
end
|
639
|
+
end
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
end
|
644
|
+
end
|
645
|
+
|
646
|
+
# run this code in the DSL
|
647
|
+
NeoRack::BackwardsCompatibility.register(self)
|
648
|
+
APP = Proc.new {|env| [200, {}, ["Hello World"]] }
|
649
|
+
run APP
|
650
|
+
```
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "neorack"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Hijacking Extension
|
2
|
+
|
3
|
+
This NeoRack extension describes a connection hijacking API that allows for backwards compatibility.
|
4
|
+
|
5
|
+
## Extension Name
|
6
|
+
|
7
|
+
NeoRack Servers that implement this extension **MUST** set `extensions[:hijack]` so it is equal to `[1,0,0].freeze`.
|
8
|
+
|
9
|
+
NeoRack Servers that implement this extension **MUST** set `classes[:hijack]` to the class of the object returned by `response.hijack`.
|
10
|
+
|
11
|
+
## Response Methods
|
12
|
+
|
13
|
+
NeoRack Servers that implement this extension **MUST** implement the following methods in the `response` object.
|
14
|
+
|
15
|
+
#### `hijack(send_headers = false)`
|
16
|
+
|
17
|
+
**SHOULD** raise an exception if `response.streaming?` would have returned `true`.
|
18
|
+
|
19
|
+
**MUST** raise an exception if `response.finished?` would have returned `true`.
|
20
|
+
|
21
|
+
**MUST** cause future calls to `response.finished?` to return `true`.
|
22
|
+
|
23
|
+
Returns an IO like object that responds to the following methods in the same was a Ruby IO or Socket object would have responded: `fileno`, `read`, `write`, `close`, `read_nonblock`, `write_nonblock`, `flush`, `close_read`, `close_write`, `closed?`.
|
24
|
+
|
25
|
+
The `fileno` method is provided to allow implementations to `poll` the IO device and this feature **SHOULD** be supported. However, NeoRack Applications and other Extensions **MUST NOT** use `fileno` for anything else, as this might break some implementations. i.e., when the server supports HTTP/2 tunneling, writing or reading from the `fileno` will break the HTTP/2 protocol.
|
26
|
+
|
27
|
+
If `send_headers` is `true`, the Server **MUST** send the status and headers before returning the IO object.
|
28
|
+
|
29
|
+
**Note**: unless the server handles the IO object calls to `write` and `write_nonblock`, the status and header data must be completely written to the system's IO buffer before the `hijack` method can return.
|
30
|
+
|
31
|
+
## Recommendations
|
32
|
+
|
33
|
+
It is **RECOMMENDED** that NeoRack Applications and Extensions consider `hijack` as a last result solution.
|
34
|
+
|
35
|
+
It is **RECOMMENDED** that NeoRack Servers don't pass an actual IO object, but instead wrap the IO object in a container that handles as many IO concerns as possible.
|
36
|
+
|
37
|
+
Servers **SHOULD** do they're best to handle all network and protocol concerns and support extensions that will allow applications to remain blissfully unaware of network details.
|
data/lib/neorack.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module NeoRack
|
2
|
+
# A single application NeoRack application DSL script handler.
|
3
|
+
#
|
4
|
+
# Use the {NeoRack::Builder.load} methods for loading scripts.
|
5
|
+
class Builder
|
6
|
+
# NOTE: Internal names end with `___` to minimize possible namespace concerns.
|
7
|
+
|
8
|
+
# Initializes a new builder object and run the script in filename
|
9
|
+
def initialize(server, script, filename)
|
10
|
+
@server___, @app___, @warmup___, @app___= server, nil, nil, nil
|
11
|
+
@stack_pre___, @stack___, @stack_post___ = [].dup, [].dup, [].dup
|
12
|
+
# run script in context of the object, enabling the DLS
|
13
|
+
instance_eval(script, filename)
|
14
|
+
end
|
15
|
+
|
16
|
+
# DSL method - access the Server object and its methods
|
17
|
+
def server
|
18
|
+
@server___
|
19
|
+
end
|
20
|
+
|
21
|
+
# DSL method - set the application to be used by the Script
|
22
|
+
def run(application)
|
23
|
+
# add middleware to a middleware stack
|
24
|
+
@app___ = application
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# DSL method - runs `.call(request, response)` before the application handles the response.
|
29
|
+
#
|
30
|
+
# Used pre-request logic, such as authentication, database connection checkout, etc'.
|
31
|
+
def run_before(prc = nil, &block)
|
32
|
+
prc ||= block
|
33
|
+
raise(ArgumentError, "this method requires an object that responds to `call(request, response)`") unless(prc.respond_to?(:call))
|
34
|
+
@stack_pre___ << prc
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# DSL method - runs `.call(request)` after the response ended (when steaming, this is delayed until streaming ends).
|
39
|
+
#
|
40
|
+
# Used for cleanup logic, such as removing database connections from the `request` Hash, logging, etc'.
|
41
|
+
def run_after(prc = nil, &block)
|
42
|
+
prc ||= block
|
43
|
+
raise(ArgumentError, "this method requires an object that responds to `call(request, response)`") unless(prc.respond_to?(:call))
|
44
|
+
@stack_pre___ << prc
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# DSL method for backwards compatibility
|
49
|
+
def use(middleware, *args, &block)
|
50
|
+
# add middleware to a middleware stack
|
51
|
+
@stack___ << [middleware, args, block]
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# DSL method for backwards compatibility
|
56
|
+
def warmup(prc = nil, &block)
|
57
|
+
@warmup___ ||= prc || block
|
58
|
+
end
|
59
|
+
|
60
|
+
# Internal use: returns the setup callback stack, the application object and the cleanup callback stack.
|
61
|
+
def build_stack___
|
62
|
+
raise "Application object missing!" unless @app___
|
63
|
+
@stack___ << @app___
|
64
|
+
app = @stack___.pop
|
65
|
+
tmp = nil
|
66
|
+
while((tmp = @stack___.pop))
|
67
|
+
if tmp[3]
|
68
|
+
app = tmp[0].new(app, *tmp[1], &tmp[2])
|
69
|
+
else
|
70
|
+
app = tmp[0].new(app, *tmp[1])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@app___ = app
|
74
|
+
@stack_post___.reverse!
|
75
|
+
@warmup___.call(@app___) if @warmup___
|
76
|
+
[@stack_pre___, @app___, @stack_post___]
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns a three member Array containing the setup callback stack (Array), the application object and the cleanup callback stack (Array).
|
80
|
+
#
|
81
|
+
# On script loading failure (i.e., file name doesn't exist), returns `nil`.
|
82
|
+
#
|
83
|
+
# Note: may raise an exception if the script itself raises an exception.
|
84
|
+
#
|
85
|
+
# Use:
|
86
|
+
#
|
87
|
+
# pre_request, app, post_request = *NeoRack::Builder.load(MyServerClass, 'config.ru')
|
88
|
+
# raise "MyServer couldn't find 'config.ru'" unless app && pre_request && post_request
|
89
|
+
#
|
90
|
+
def self.load(server_klass, filename = 'config.ru')
|
91
|
+
# try to load the file
|
92
|
+
script = ::File.read(filename) rescue nil
|
93
|
+
return `nil` unless script
|
94
|
+
# remove UTF-8 BOM, see: https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom
|
95
|
+
script.slice!(0..2) if script.encoding == Encoding::UTF_8 && script.start_with?('\xef\xbb\xbf')
|
96
|
+
# start a new builder
|
97
|
+
instance = NeoRack::Builder.new(server_klass, script, filename)
|
98
|
+
# build stack and return
|
99
|
+
instance.build_stack___
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/neorack.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'lib/neorack/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "neorack"
|
5
|
+
spec.version = NeoRack::VERSION
|
6
|
+
spec.authors = ["Bo"]
|
7
|
+
spec.email = ["bo@facil.io"]
|
8
|
+
|
9
|
+
spec.summary = %q{Common helpers and external extensions for NeoRack.}
|
10
|
+
spec.description = %q{This gem is optional. NeoRack is a specification, not a gem... but to save developers time in (re)writing the DSL builder and other common tasks, this gem is provided.}
|
11
|
+
spec.homepage = "https://github.com/boazsegev/neorack"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
|
+
|
15
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/boazsegev/neorack"
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/boazsegev/neorack/gem/CHANGELOG.md"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
end
|
data/neorack_logo.png
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: neorack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bo
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-09-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: This gem is optional. NeoRack is a specification, not a gem... but to
|
14
|
+
save developers time in (re)writing the DSL builder and other common tasks, this
|
15
|
+
gem is provided.
|
16
|
+
email:
|
17
|
+
- bo@facil.io
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- ".gitignore"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- CODE_OF_CONDUCT.md
|
25
|
+
- Gemfile
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- SPEC.md
|
30
|
+
- bin/console
|
31
|
+
- bin/setup
|
32
|
+
- extensions/hijack.md
|
33
|
+
- lib/neorack.rb
|
34
|
+
- lib/neorack/builder.rb
|
35
|
+
- lib/neorack/version.rb
|
36
|
+
- neorack.gemspec
|
37
|
+
- neorack_logo.png
|
38
|
+
homepage: https://github.com/boazsegev/neorack
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata:
|
42
|
+
allowed_push_host: https://rubygems.org
|
43
|
+
homepage_uri: https://github.com/boazsegev/neorack
|
44
|
+
source_code_uri: https://github.com/boazsegev/neorack
|
45
|
+
changelog_uri: https://github.com/boazsegev/neorack/gem/CHANGELOG.md
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.3.0
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.1.2
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Common helpers and external extensions for NeoRack.
|
65
|
+
test_files: []
|