iodine 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +86 -8
- data/lib/iodine.rb +45 -2
- data/lib/iodine/core.rb +9 -37
- data/lib/iodine/http.rb +135 -0
- data/lib/iodine/http/hpack.rb +543 -0
- data/lib/iodine/http/http1.rb +217 -0
- data/lib/iodine/http/http2.rb +465 -0
- data/lib/iodine/http/rack_support.rb +105 -0
- data/lib/iodine/http/request.rb +413 -0
- data/lib/iodine/http/response.rb +355 -0
- data/lib/iodine/http/session.rb +110 -0
- data/lib/iodine/http/websocket_client.rb +224 -0
- data/lib/iodine/http/websocket_handler.rb +40 -0
- data/lib/iodine/http/websockets.rb +319 -0
- data/lib/iodine/io.rb +71 -28
- data/lib/iodine/protocol.rb +74 -25
- data/lib/iodine/settings.rb +44 -3
- data/lib/iodine/ssl_connector.rb +47 -0
- data/lib/iodine/timers.rb +5 -21
- data/lib/iodine/version.rb +1 -1
- data/lib/rack/handler/iodine.rb +3 -0
- data/{bin/http_test → manual tests/core_http_test } +6 -2
- data/bin/echo b/data/manual → tests/echo +0 -0
- data/bin/em playground b/data/manual tests/em → playground +0 -0
- data/manual tests/hello_world +56 -0
- metadata +20 -6
- data/lib/iodine/ssl_protocol.rb +0 -108
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4562f69fd3e73d156c4007d70400f07973c143ea
|
4
|
+
data.tar.gz: 14264f438e3e200ad52f18f63881087854da88b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab840a1c507e08b328b257e42038ec1dad3eaaaaad8703b1d0d1085707a20dddfb21b7e290eeec80008eb49c8beb8eb94901a17e0c9efa5648a9107c1d36d943
|
7
|
+
data.tar.gz: e4f5a83897e9bf8eb33225278010abc1be7bb0787eeec3eed77704b7f2f383448becad6c0b870988c77aecec5e3edba1248b1c46765376bff07cfc3f2ba9a406
|
data/README.md
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
# Iodine
|
2
2
|
|
3
|
-
Iodine makes writing evented server applications easy to write.
|
3
|
+
Iodine makes writing Object Oriented evented server applications easy to write.
|
4
4
|
|
5
|
-
|
5
|
+
In fact, it's so fun to write network protocols that mix and match together, that Iodine includes a built in Http, Http/2 (experimental) and Websocket server that act's a a great demonstration of the power behind Ruby and the Object Oriented approach.
|
6
6
|
|
7
|
-
To use Iodine, you just set up your tasks - including a single server, if you want one. Iodine will start running once your application is finished and it won't stop runing until all the tasks have completed.
|
8
|
-
|
9
|
-
Iodine v. 0.0.1 isn't well tested just yet... but I'm releasing it anyway, to reserve the name and because initial testing shows that it works.
|
7
|
+
To use Iodine, you just set up your tasks - including a single server, if you want one. Iodine will start running once your application is finished and it won't stop runing until all the scheduled tasks have completed.
|
10
8
|
|
11
9
|
## Installation
|
12
10
|
|
@@ -24,9 +22,11 @@ Or install it yourself as:
|
|
24
22
|
|
25
23
|
$ gem install iodine
|
26
24
|
|
27
|
-
## Simple Usage
|
25
|
+
## Simple Usage: Running tasks and shutting down
|
26
|
+
|
27
|
+
This mode of operation is effective if you have a `cron`-job that periodically initiates an Iodine Ruby script. It allows the script to easily initiate a task's stack and perform the tasks concurrently.
|
28
28
|
|
29
|
-
Iodine starts to work once you app is finished
|
29
|
+
Iodine starts to work once you app is finished setting all the tasks up (upon exit).
|
30
30
|
|
31
31
|
To see how that works, open your `irb` terminal an try this:
|
32
32
|
|
@@ -52,12 +52,90 @@ Iodine.threads = 5
|
|
52
52
|
exit
|
53
53
|
```
|
54
54
|
|
55
|
-
|
55
|
+
In this mode, Iodine will continue running until all the tasks have completed and than it will quite. Timer based tasks will be ignored.
|
56
|
+
|
57
|
+
## Simple Usage: Task polling (unreleased version)
|
58
|
+
|
59
|
+
This mode of operation is effective if want Iodine to periodically initiates new tasks, for instance if you cannot use `cron`.
|
60
|
+
|
61
|
+
To initiate this mode, simply set: `Iodine.protocol = :timers`
|
62
|
+
|
63
|
+
In example:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
require 'iodine'
|
67
|
+
|
68
|
+
# set concurrency level (defaults to a single thread).
|
69
|
+
Iodine.threads = 5
|
70
|
+
|
71
|
+
# set Iodine to keep listening to TimedEvent(s).
|
72
|
+
Iodine.protocol = :timers
|
73
|
+
|
74
|
+
# perform a periodical task every ten seconds
|
75
|
+
Iodine.run_every 10 do
|
76
|
+
Iodine.run { sleep 5; puts " * this could have been a long task..." }
|
77
|
+
puts "I could be polling a database to schedule more tasks..."
|
78
|
+
end
|
79
|
+
|
80
|
+
# Iodine will start running once your script is done and it will never stop unless stopped.
|
81
|
+
exit
|
82
|
+
```
|
83
|
+
|
84
|
+
In this mode, Iodine will continue running until it receives a kill signal (i.e. `^C`). Once the kill signal had been received, Iodine will start shutting down, allowing up to ~20-25 seconds to complete any pending tasks (timeout).
|
85
|
+
|
86
|
+
## Server Usage: an Http and Websocket (as well as Rack) server
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
## Server Usage: Plug in your network protocol
|
56
91
|
|
57
92
|
Iodine is designed to help write network services (Servers) where each script is intended to implement a single server.
|
58
93
|
|
59
94
|
This is not a philosophy based on any idea or preferences, but rather a response to real-world design where each Ruby script is usually assigned a single port for network access (hence, a single server).
|
60
95
|
|
96
|
+
To help you write your network service, Iodine starts you off with the `Iodine::Protocol`. All network protocols should inherit from this class (or implement it's essencial functionality).
|
97
|
+
|
98
|
+
Here's a quick Echo server:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
require 'iodine'
|
102
|
+
|
103
|
+
# inherit from ::Iodine::Protocol
|
104
|
+
class EchoServer < Iodine::Protocol
|
105
|
+
# The protocol class will call this withing a Mutex,
|
106
|
+
# making sure the IO isn't accessed while being initialized.
|
107
|
+
def on_open
|
108
|
+
Iodine.info "Opened connection."
|
109
|
+
set_timeout 5
|
110
|
+
end
|
111
|
+
# The protocol class will call this withing a Mutex, after reading the data from the IO.
|
112
|
+
# This makes this thread-safe per connection.
|
113
|
+
def on_message data
|
114
|
+
write("-- Closing connection, goodbye.\n") && close if data =~ /^(bye|close|exit|stop)/i
|
115
|
+
write(">> #{data.chomp}\n")
|
116
|
+
end
|
117
|
+
# Iodine makes sure this is called only once.
|
118
|
+
def on_close
|
119
|
+
Iodine.info "Closed connection."
|
120
|
+
end
|
121
|
+
# The is called whenever timeout is reached.
|
122
|
+
# By default, ping will close the connection.
|
123
|
+
# but we can do better...
|
124
|
+
def ping
|
125
|
+
# `write` will automatically close the connection if it fails.
|
126
|
+
write "-- Are you still there?\n"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
Iodine.protocol = EchoServer
|
132
|
+
|
133
|
+
# if running this code within irb:
|
134
|
+
exit
|
135
|
+
```
|
136
|
+
|
137
|
+
In this mode, Iodine will continue running until it receives a kill signal (i.e. `^C`). Once the kill signal had been received, Iodine will start shutting down, allowing up to ~20-25 seconds to complete any pending tasks (timeout).
|
138
|
+
|
61
139
|
## Development
|
62
140
|
|
63
141
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/iodine.rb
CHANGED
@@ -1,7 +1,48 @@
|
|
1
|
-
require
|
1
|
+
require 'logger'
|
2
2
|
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
# require 'securerandom'
|
3
5
|
|
4
6
|
|
7
|
+
# Iodine is an easy Object-Oriented library for writing network applications (servers) with your own
|
8
|
+
# network protocol.
|
9
|
+
#
|
10
|
+
# Please read the {file:README.md} file for an introduction to Iodine.
|
11
|
+
#
|
12
|
+
# Here's a quick and easy echo server,
|
13
|
+
# notice how Iodine will automatically start running once you finish setting everything up:
|
14
|
+
#
|
15
|
+
# require 'iodine'
|
16
|
+
#
|
17
|
+
# class MyProtocol < Iodine::Protocol
|
18
|
+
# # Iodine will call this whenever a new connection is opened.
|
19
|
+
# def on_open
|
20
|
+
# # Iodine includes logging as well as unique assigned instance ID's.
|
21
|
+
# Iodine.info "New connection id: #{id}"
|
22
|
+
# # Iodine includes timeout support with automatic pinging or connection termination.
|
23
|
+
# set_timeout 5
|
24
|
+
# end
|
25
|
+
# def on_message data
|
26
|
+
# write("-- Closing connection, goodbye.\n") && close if data =~ /^(bye|close|exit)/i
|
27
|
+
# write(">> #{data.chomp}\n")
|
28
|
+
# end
|
29
|
+
# # Iodine will call this whenever a new connection is closed.
|
30
|
+
# def on_close
|
31
|
+
# Iodine.info "Closing connection id: #{id}"
|
32
|
+
# end
|
33
|
+
# # Iodine will call this whenever a a timeout is reached.
|
34
|
+
# def ping
|
35
|
+
# # If `write` fails, it automatically closes the connection.
|
36
|
+
# write("-- Are you still there?\n")
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# # setting up the server is as easy as plugging in your Protocol class:
|
41
|
+
# Iodine.protocol = MyProtocol
|
42
|
+
#
|
43
|
+
# # if you are excecuting this script from IRB, exit IRB to start Iodine.
|
44
|
+
# exit
|
45
|
+
#
|
5
46
|
module Iodine
|
6
47
|
extend self
|
7
48
|
end
|
@@ -13,5 +54,7 @@ require "iodine/logging"
|
|
13
54
|
require "iodine/core"
|
14
55
|
require "iodine/timers"
|
15
56
|
require "iodine/protocol"
|
16
|
-
require "iodine/
|
57
|
+
require "iodine/ssl_connector"
|
17
58
|
require "iodine/io"
|
59
|
+
|
60
|
+
# require 'iodine/http'
|
data/lib/iodine/core.rb
CHANGED
@@ -8,47 +8,18 @@ module Iodine
|
|
8
8
|
#
|
9
9
|
# use:
|
10
10
|
#
|
11
|
-
#
|
11
|
+
# Iodine.run(arg1, arg2, arg3 ...) { |arg1, arg2, arg3...| do_something }
|
12
12
|
#
|
13
13
|
# the block will be run within the current context, allowing access to current methods and variables.
|
14
14
|
#
|
15
|
-
# @return [
|
15
|
+
# @return [Iodine] always returns the reactor object.
|
16
16
|
def run *args, &block
|
17
|
-
queue block, args
|
18
|
-
end
|
19
|
-
alias :run_async :run
|
20
|
-
|
21
|
-
# This method runs an object's method asynchronously and returns immediately. This method will also run an optional callback if a block is supplied.
|
22
|
-
#
|
23
|
-
# This method accepts:
|
24
|
-
# object:: an object who's method will be called.
|
25
|
-
# method:: the method's name to be called. type: Symbol.
|
26
|
-
# *args:: any arguments to be passed to the method.
|
27
|
-
# block (optional):: If a block is supplied, it will be used as a callback and the method's return value will be passed on to the block.
|
28
|
-
#
|
29
|
-
# @return [GReactor] always returns the reactor object.
|
30
|
-
def callback object, method_name, *args, &block
|
31
|
-
block ? queue(@callback_proc, [object.method(method_name), args, block]) : queue(object.method(method_name), args)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Adds a job OR a block to the queue. {GReactor.run_async} and {GReactor.callback} extend this core method.
|
35
|
-
#
|
36
|
-
# This method accepts two possible arguments:
|
37
|
-
# job:: An object that answers to `call`, usually a Proc or Lambda.
|
38
|
-
# args:: (optional) An Array of arguments to be passed on to the executed method.
|
39
|
-
#
|
40
|
-
# @return [GReactor] always returns the reactor object.
|
41
|
-
#
|
42
|
-
# The callback will NOT be called if the executed job failed (raised an exception).
|
43
|
-
# @see .run_async
|
44
|
-
#
|
45
|
-
# @see .callback
|
46
|
-
def queue job, args = nil
|
47
|
-
@queue << [job, args]
|
17
|
+
@queue << [block, args]
|
48
18
|
self
|
49
19
|
end
|
20
|
+
alias :run_async :run
|
50
21
|
|
51
|
-
# Adds a shutdown tasks. These tasks should be executed in order of creation.
|
22
|
+
# @return [Iodine] Adds a shutdown tasks. These tasks should be executed in order of creation.
|
52
23
|
def on_shutdown *args, &block
|
53
24
|
@shutdown_queue << [block, args]
|
54
25
|
self
|
@@ -61,7 +32,7 @@ module Iodine
|
|
61
32
|
@stop = true
|
62
33
|
@done = false
|
63
34
|
@logger = Logger.new(STDOUT)
|
64
|
-
@thread_count = 1
|
35
|
+
@spawn_count = @thread_count = 1
|
65
36
|
@ios = {}
|
66
37
|
@io_in = Queue.new
|
67
38
|
@io_out = Queue.new
|
@@ -92,14 +63,15 @@ module Iodine
|
|
92
63
|
@thread_count.times { threads << Thread.new { cycle } }
|
93
64
|
unless @stop
|
94
65
|
catch(:stop) { sleep }
|
95
|
-
@logger << "\nShutting down Iodine. Setting shutdown timeout to
|
66
|
+
@logger << "\nShutting down Iodine. Setting shutdown timeout to 25 seconds.\n"
|
96
67
|
@stop = true
|
97
68
|
# setup exit timeout.
|
98
|
-
threads.each {|t| Thread.new {sleep
|
69
|
+
threads.each {|t| Thread.new {sleep 25; t.kill; t.kill } }
|
99
70
|
end
|
100
71
|
threads.each {|t| t.join rescue true }
|
101
72
|
end
|
102
73
|
|
74
|
+
# performed once - the shutdown sequence.
|
103
75
|
def shutdown
|
104
76
|
return if @done
|
105
77
|
@stop = @done = true
|
data/lib/iodine/http.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'iodine'
|
2
|
+
require 'stringio'
|
3
|
+
require 'time'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
require 'uri'
|
7
|
+
require 'tmpdir'
|
8
|
+
require 'zlib'
|
9
|
+
require 'securerandom'
|
10
|
+
|
11
|
+
require 'iodine/http/request'
|
12
|
+
require 'iodine/http/response'
|
13
|
+
require 'iodine/http/session'
|
14
|
+
|
15
|
+
require 'iodine/http/http1'
|
16
|
+
|
17
|
+
require 'iodine/http/hpack'
|
18
|
+
require 'iodine/http/http2'
|
19
|
+
|
20
|
+
require 'iodine/http/websockets'
|
21
|
+
# require 'iodine/http/websockets_handler'
|
22
|
+
require 'iodine/http/websocket_client'
|
23
|
+
|
24
|
+
require 'iodine/http/rack_support'
|
25
|
+
|
26
|
+
|
27
|
+
module Iodine
|
28
|
+
|
29
|
+
# The {Iodine::Http} class allows the creation of Http and Websocket servers using Iodine.
|
30
|
+
#
|
31
|
+
# To start an Http server, simply require `iodine/http` (which isn't required by default) and set up
|
32
|
+
# your Http callback. i.e.:
|
33
|
+
#
|
34
|
+
# require 'iodine/http'
|
35
|
+
# Iodine::Http.on_http { |request, response| 'Hello World!' }
|
36
|
+
#
|
37
|
+
# To start a Websocket server, require `iodine/http` (which isn't required by default), create a Websocket handling Class and set up
|
38
|
+
# your Websocket callback. i.e.:
|
39
|
+
#
|
40
|
+
# require 'iodine/http'
|
41
|
+
# class WSChatServer
|
42
|
+
# def initialize nickname
|
43
|
+
# @nickname = nickname || "unknown"
|
44
|
+
# end
|
45
|
+
# def on_open protocol
|
46
|
+
# @io = protocol
|
47
|
+
# @io.broadcast "#{@nickname} has joined the chat!"
|
48
|
+
# @io << "Welcome #{@nickname}, you have joined the chat!"
|
49
|
+
# end
|
50
|
+
# def on_message data
|
51
|
+
# @io.broadcast "#{@nickname} >> #{data}"
|
52
|
+
# @io << ">> #{data}"
|
53
|
+
# end
|
54
|
+
# def on_broadcast data
|
55
|
+
# @io << data
|
56
|
+
# end
|
57
|
+
# def on_close
|
58
|
+
# @io.broadcast "#{@nickname} has left the chat!"
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# Iodine::Http.on_websocket { |request, response| WSChatServer.new request.params[:name]}
|
63
|
+
#
|
64
|
+
class Http < Iodine::Protocol
|
65
|
+
# Sets or gets the Http callback.
|
66
|
+
#
|
67
|
+
# An Http callback is a Proc like object that answers to `call(request, response)` and returns either:
|
68
|
+
# `true`:: the response has been set by the callback and can be managed (including any streaming) by the server.
|
69
|
+
# `false`:: the request shouldn't be answered or resource not found (error 404 will be sent as a response).
|
70
|
+
# String:: the String will be appended to the response and the response sent.
|
71
|
+
def self.on_http handler = nil, &block
|
72
|
+
@http_app = handler || block if handler || block
|
73
|
+
@http_app
|
74
|
+
end
|
75
|
+
# Sets or gets the Websockets callback.
|
76
|
+
#
|
77
|
+
# A Websockets callback is a Proc like object that answers to `call(request)` and returns either:
|
78
|
+
# `false`:: the request shouldn't be answered or resource not found (error 404 will be sent as a response).
|
79
|
+
# Websocket Handler:: a Websocket handler is an object that is expected to answer `on_message(data)` and `on_close`. See {} for more data.
|
80
|
+
def self.on_websocket handler = nil, &block
|
81
|
+
@websocket_app = handler || block if handler || block
|
82
|
+
@websocket_app
|
83
|
+
end
|
84
|
+
|
85
|
+
# Sets the session token for the Http server (String). Defaults to the name of the script + '_id'.
|
86
|
+
def self.session_token= token
|
87
|
+
@session_token = token
|
88
|
+
end
|
89
|
+
# Sets the session token for the Http server (String). Defaults to the name of the script.
|
90
|
+
def self.session_token
|
91
|
+
@session_token
|
92
|
+
end
|
93
|
+
|
94
|
+
# Creates a websocket client within a new task (non-blocking).
|
95
|
+
#
|
96
|
+
# Make sure to setup all the callbacks (as needed) prior to starting the connection. See {::Iodine::Http::WebsocketClient.connect}
|
97
|
+
#
|
98
|
+
# i.e.:
|
99
|
+
#
|
100
|
+
# require 'iodine/http'
|
101
|
+
# options = {}
|
102
|
+
# options[:on_open] = Proc.new { write "Hello there!"}
|
103
|
+
# options[:on_message] = Proc.new do |data|
|
104
|
+
# puts ">> #{data}";
|
105
|
+
# write "Bye!";
|
106
|
+
# # It's possible to update the callback midstream.
|
107
|
+
# on_message {|data| puts "-- Goodbye message: #{data}"; close}
|
108
|
+
# end
|
109
|
+
# options[:on_close] = Proc.new { puts "disconnected"}
|
110
|
+
#
|
111
|
+
# Iodine::Http.ws_connect "ws://echo.websocket.org", options
|
112
|
+
#
|
113
|
+
def self.ws_connect url, options={}, &block
|
114
|
+
::Iodine.run { ::Iodine::Http::WebsocketClient.connect url, options, &block }
|
115
|
+
end
|
116
|
+
|
117
|
+
@websocket_app = @http_app = NOT_IMPLEMENTED = Proc.new { |i,o| false }
|
118
|
+
@session_token = "#{File.basename($0, '.*')}_uuid"
|
119
|
+
end
|
120
|
+
|
121
|
+
@queue.tap do |q|
|
122
|
+
arr =[];
|
123
|
+
arr << q.pop until q.empty?;
|
124
|
+
run { Iodine.ssl_protocols = { 'h2' => Iodine::Http::Http2, 'http/1.1' => Iodine::Http } if @ssl && @ssl_protocols.empty? }
|
125
|
+
run do
|
126
|
+
if Iodine.protocol == ::Iodine::Http && ::Iodine::Http.on_http == ::Iodine::Http::NOT_IMPLEMENTED && ::Iodine::Http.on_websocket == ::Iodine::Http::NOT_IMPLEMENTED
|
127
|
+
::Iodine.protocol = :http_not_initialized
|
128
|
+
q << arr.shift until arr.empty?
|
129
|
+
run { Process.kill("INT", 0) }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
q << arr.shift until arr.empty?
|
133
|
+
end
|
134
|
+
end
|
135
|
+
Iodine.protocol = ::Iodine::Http
|
@@ -0,0 +1,543 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Iodine
|
4
|
+
class Http < ::Iodine::Protocol
|
5
|
+
class Http2 < ::Iodine::Protocol
|
6
|
+
class HPACK
|
7
|
+
class IndexTable
|
8
|
+
attr_reader :size
|
9
|
+
attr_accessor :max_size
|
10
|
+
def initialize
|
11
|
+
@list = []
|
12
|
+
@size = 4_096 # initial defaul size by standard
|
13
|
+
@actual_size = 0
|
14
|
+
end
|
15
|
+
def [] index
|
16
|
+
raise "HPACK Error - invalid header index: 0" if index == 0
|
17
|
+
return STATIC_LIST[index] if index < STATIC_LENGTH
|
18
|
+
raise "HPACK Error - invalid header index: #{index}" if @list.count <= (index - STATIC_LENGTH)
|
19
|
+
@list[index - STATIC_LENGTH]
|
20
|
+
end
|
21
|
+
alias :get_index :[]
|
22
|
+
def get_name index
|
23
|
+
get_index(index)[0]
|
24
|
+
end
|
25
|
+
def insert *field
|
26
|
+
@list.unshift field
|
27
|
+
field.each {|f| @actual_size += f.to_s.bytesize}; @actual_size += 32
|
28
|
+
resize
|
29
|
+
field
|
30
|
+
end
|
31
|
+
def find *field
|
32
|
+
index = STATIC_LIST.index(field)
|
33
|
+
return index if index
|
34
|
+
index = @list.index(field)
|
35
|
+
index ? (index + STATIC_LENGTH) : nil
|
36
|
+
end
|
37
|
+
def find_name name
|
38
|
+
index = 1
|
39
|
+
while STATIC_LIST[index]
|
40
|
+
return index if STATIC_LIST[index][0] == name
|
41
|
+
index += 1
|
42
|
+
end
|
43
|
+
index = 0
|
44
|
+
while @list[index]
|
45
|
+
return index+STATIC_LENGTH if @list[index][0] == name
|
46
|
+
index += 1
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
def resize value = nil
|
51
|
+
@size = value if value && value <= 4_096
|
52
|
+
while (@actual_size > @size) && @list.any?
|
53
|
+
@list.pop.each {|i| @actual_size -= i.to_s.bytesize}
|
54
|
+
@actual_size -= 32
|
55
|
+
end
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
@decoding_list = IndexTable.new
|
62
|
+
@encoding_list = IndexTable.new
|
63
|
+
end
|
64
|
+
|
65
|
+
def decode data
|
66
|
+
data = StringIO.new data
|
67
|
+
results = {}
|
68
|
+
while (field = decode_field(data))
|
69
|
+
name = (field[0].is_a?(String) && field[0][0] == ':') ? field[0][1..-1].to_sym : field[0]
|
70
|
+
results[name] ? (results[name].is_a?(String) ? (results[name] = [results[name], field[1]]) : (results[name] << field[1]) ) : (results[name] = field[1]) if field[1]
|
71
|
+
end
|
72
|
+
results
|
73
|
+
end
|
74
|
+
def encode headers = {}
|
75
|
+
buffer = ''
|
76
|
+
headers.each {|k, v| buffer << encode_field( (k.is_a?(String) ? k : ":#{k.to_s}".freeze) ,v) if v}
|
77
|
+
buffer
|
78
|
+
end
|
79
|
+
def resize max
|
80
|
+
@decoding_list.resize max
|
81
|
+
@encoding_list.resize max
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
def decode_field data # expects a StringIO or other IO object
|
86
|
+
byte = data.getbyte
|
87
|
+
return nil unless byte
|
88
|
+
if byte[7] == 1 # 0b1000_0000 == 0b1000_0000
|
89
|
+
# An indexed header field starts with the '1' 1-bit pattern, followed by the index of the matching header field, represented as an integer with a 7-bit prefix (see Section 5.1).
|
90
|
+
num = extract_number data, byte, 1
|
91
|
+
@decoding_list[num]
|
92
|
+
elsif byte & 192 == 64 # 0b1100_0000 == 0b0100_0000
|
93
|
+
# A literal header field with incremental indexing representation starts with the '01' 2-bit pattern.
|
94
|
+
# If the header field name matches the header field name of an entry stored in the static table or the dynamic table, the header field name can be represented using the index of that entry. In this case, the index of the entry is represented as an integer with a 6-bit prefix (see Section 5.1). This value is always non-zero.
|
95
|
+
# Otherwise, the header field name is represented as a string literal (see Section 5.2). A value 0 is used in place of the 6-bit index, followed by the header field name.
|
96
|
+
num = extract_number data, byte, 2
|
97
|
+
field_name = (num == 0) ? extract_string(data) : @decoding_list.get_name(num)
|
98
|
+
field_value = extract_string(data)
|
99
|
+
@decoding_list.insert field_name, field_value
|
100
|
+
elsif byte & 224 # 0b1110_0000 == 0
|
101
|
+
# A literal header field without indexing representation starts with the '0000' 4-bit pattern.
|
102
|
+
# If the header field name matches the header field name of an entry stored in the static table or the dynamic table, the header field name can be represented using the index of that entry.
|
103
|
+
# In this case, the index of the entry is represented as an integer with a 4-bit prefix (see Section 5.1). This value is always non-zero.
|
104
|
+
# Otherwise, the header field name is represented as a string literal (see Section 5.2) and a value 0 is used in place of the 4-bit index, followed by the header field name.
|
105
|
+
# OR
|
106
|
+
# A literal header field never-indexed representation starts with the '0001' 4-bit pattern + 4+ bits for index
|
107
|
+
num = extract_number data, byte, 4
|
108
|
+
field_name = (num == 0) ? extract_string(data) : @decoding_list.get_name(num)
|
109
|
+
field_value = extract_string(data)
|
110
|
+
[field_name, field_value]
|
111
|
+
elsif byte & 224 == 32 # 0b1110_0000 == 0b0010_0000
|
112
|
+
# A dynamic table size update starts with the '001' 3-bit pattern
|
113
|
+
# followed by the new maximum size, represented as an integer with a 5-bit prefix (see Section 5.1).
|
114
|
+
@decoding_list.resize extract_number(data, byte, 5)
|
115
|
+
[].freeze
|
116
|
+
else
|
117
|
+
raise "HPACK Error - invalid field indicator."
|
118
|
+
end
|
119
|
+
end
|
120
|
+
def encode_field name, value
|
121
|
+
if value.is_a?(Array)
|
122
|
+
return (value.map {|v| encode_field name, v} .join)
|
123
|
+
end
|
124
|
+
if name == 'set-cookie'
|
125
|
+
buffer = ''
|
126
|
+
buffer << pack_number( 55, 16, 4)
|
127
|
+
buffer << pack_string(value)
|
128
|
+
return buffer
|
129
|
+
end
|
130
|
+
index = @encoding_list.find(name, value)
|
131
|
+
return pack_number( index, 1, 1) if index
|
132
|
+
index = @encoding_list.find_name name
|
133
|
+
@encoding_list.insert name, value
|
134
|
+
buffer = ''
|
135
|
+
if index
|
136
|
+
buffer << pack_number( index, 64, 2)
|
137
|
+
else
|
138
|
+
buffer << pack_number( 0, 64, 2)
|
139
|
+
buffer << pack_string(name.to_s)
|
140
|
+
end
|
141
|
+
buffer << pack_string(value)
|
142
|
+
buffer
|
143
|
+
end
|
144
|
+
def extract_number data, prefix, prefix_length
|
145
|
+
mask = 255 >> prefix_length
|
146
|
+
return prefix & mask unless (prefix & mask) == mask
|
147
|
+
count = prefix = 0
|
148
|
+
loop do
|
149
|
+
c = data.getbyte
|
150
|
+
prefix = prefix | ((c & 127) << (7*count))
|
151
|
+
break if c[7] == 0
|
152
|
+
count += 1
|
153
|
+
end
|
154
|
+
prefix + mask
|
155
|
+
# rescue e =>
|
156
|
+
# raise "HPACK Error - number input invalid"
|
157
|
+
end
|
158
|
+
def pack_number number, prefix, prefix_length
|
159
|
+
n_length = 8-prefix_length
|
160
|
+
if (number + 1 ).bit_length <= n_length
|
161
|
+
return ((prefix << n_length) | number).chr
|
162
|
+
end
|
163
|
+
prefix = [(prefix << n_length) | (2**n_length - 1)]
|
164
|
+
number -= 2**n_length - 1
|
165
|
+
loop do
|
166
|
+
prefix << ((number & 127) | 128)
|
167
|
+
number = number >> 7
|
168
|
+
break if number == 0
|
169
|
+
end
|
170
|
+
(prefix << (prefix.pop & 127)).pack('C*'.freeze)
|
171
|
+
end
|
172
|
+
def pack_string string, deflate = true
|
173
|
+
string = deflate(string) if deflate
|
174
|
+
(pack_number(string.bytesize, (deflate ? 1 : 0), 1) + string).force_encoding ::Encoding::ASCII_8BIT
|
175
|
+
end
|
176
|
+
def extract_string data
|
177
|
+
byte = data.getbyte
|
178
|
+
hoffman = byte[7] == 1
|
179
|
+
length = extract_number data, byte, 1
|
180
|
+
if hoffman
|
181
|
+
inflate data.read(length)
|
182
|
+
else
|
183
|
+
data.read length
|
184
|
+
end
|
185
|
+
end
|
186
|
+
def inflate data
|
187
|
+
data = StringIO.new data
|
188
|
+
str = ''
|
189
|
+
buffer = ''
|
190
|
+
until data.eof?
|
191
|
+
byte = data.getbyte
|
192
|
+
8.times do |i|
|
193
|
+
buffer << byte[7-i].to_s
|
194
|
+
if HUFFMAN[buffer]
|
195
|
+
str << HUFFMAN[buffer].chr rescue raise("HPACK Error - Huffman EOS found")
|
196
|
+
buffer.clear
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
raise "HPACK Error - Huffman padding too long (#{buffer.length}): #{buffer}" if buffer.length > 29
|
201
|
+
str
|
202
|
+
end
|
203
|
+
def deflate data
|
204
|
+
str = ''
|
205
|
+
buffer = ''
|
206
|
+
data.bytes.each do |i|
|
207
|
+
buffer << HUFFMAN.key(i)
|
208
|
+
if (buffer % 8) == 0
|
209
|
+
str << [buffer].pack('b*')
|
210
|
+
buffer.clear
|
211
|
+
end
|
212
|
+
end
|
213
|
+
(8-(buffer.bytesize % 8)).times { buffer << '1'}
|
214
|
+
str << [buffer].pack('b*')
|
215
|
+
buffer.clear
|
216
|
+
str
|
217
|
+
end
|
218
|
+
STATIC_LIST = [ nil,
|
219
|
+
[":authority"],
|
220
|
+
[":method", "GET" ],
|
221
|
+
[":method", "POST" ],
|
222
|
+
[":path", "/" ],
|
223
|
+
[":path", "/index.html" ],
|
224
|
+
[":scheme", "http" ],
|
225
|
+
[":scheme", "https" ],
|
226
|
+
[":status", "200" ],
|
227
|
+
[":status", "204" ],
|
228
|
+
[":status", "206" ],
|
229
|
+
[":status", "304" ],
|
230
|
+
[":status", "400" ],
|
231
|
+
[":status", "404" ],
|
232
|
+
[":status", "500" ],
|
233
|
+
["accept-charset"],
|
234
|
+
["accept-encoding", "gzip, deflate" ],
|
235
|
+
["accept-language"],
|
236
|
+
["accept-ranges"],
|
237
|
+
["accept"],
|
238
|
+
["access-control-allow-origin"],
|
239
|
+
["age"],
|
240
|
+
["allow"],
|
241
|
+
["authorization"],
|
242
|
+
["cache-control"],
|
243
|
+
["content-disposition"],
|
244
|
+
["content-encoding"],
|
245
|
+
["content-language"],
|
246
|
+
["content-length"],
|
247
|
+
["content-location"],
|
248
|
+
["content-range"],
|
249
|
+
["content-type"],
|
250
|
+
["cookie"],
|
251
|
+
["date"],
|
252
|
+
["etag"],
|
253
|
+
["expect"],
|
254
|
+
["expires"],
|
255
|
+
["from"],
|
256
|
+
["host"],
|
257
|
+
["if-match"],
|
258
|
+
["if-modified-since"],
|
259
|
+
["if-none-match"],
|
260
|
+
["if-range"],
|
261
|
+
["if-unmodified-since"],
|
262
|
+
["last-modified"],
|
263
|
+
["link"],
|
264
|
+
["location"],
|
265
|
+
["max-forwards"],
|
266
|
+
["proxy-authenticate"],
|
267
|
+
["proxy-authorization"],
|
268
|
+
["range"],
|
269
|
+
["referer"],
|
270
|
+
["refresh"],
|
271
|
+
["retry-after"],
|
272
|
+
["server"],
|
273
|
+
["set-cookie"],
|
274
|
+
["strict-transport-security"],
|
275
|
+
["transfer-encoding"],
|
276
|
+
["user-agent"],
|
277
|
+
["vary"],
|
278
|
+
["via"],
|
279
|
+
["www-authenticate"] ].map! {|a| a.map! {|s| s.is_a?(String) ? s.freeze : s } && a.freeze if a}
|
280
|
+
STATIC_LENGTH = STATIC_LIST.length
|
281
|
+
|
282
|
+
HUFFMAN = [
|
283
|
+
"1111111111000",
|
284
|
+
"11111111111111111011000",
|
285
|
+
"1111111111111111111111100010",
|
286
|
+
"1111111111111111111111100011",
|
287
|
+
"1111111111111111111111100100",
|
288
|
+
"1111111111111111111111100101",
|
289
|
+
"1111111111111111111111100110",
|
290
|
+
"1111111111111111111111100111",
|
291
|
+
"1111111111111111111111101000",
|
292
|
+
"111111111111111111101010",
|
293
|
+
"111111111111111111111111111100",
|
294
|
+
"1111111111111111111111101001",
|
295
|
+
"1111111111111111111111101010",
|
296
|
+
"111111111111111111111111111101",
|
297
|
+
"1111111111111111111111101011",
|
298
|
+
"1111111111111111111111101100",
|
299
|
+
"1111111111111111111111101101",
|
300
|
+
"1111111111111111111111101110",
|
301
|
+
"1111111111111111111111101111",
|
302
|
+
"1111111111111111111111110000",
|
303
|
+
"1111111111111111111111110001",
|
304
|
+
"1111111111111111111111110010",
|
305
|
+
"111111111111111111111111111110",
|
306
|
+
"1111111111111111111111110011",
|
307
|
+
"1111111111111111111111110100",
|
308
|
+
"1111111111111111111111110101",
|
309
|
+
"1111111111111111111111110110",
|
310
|
+
"1111111111111111111111110111",
|
311
|
+
"1111111111111111111111111000",
|
312
|
+
"1111111111111111111111111001",
|
313
|
+
"1111111111111111111111111010",
|
314
|
+
"1111111111111111111111111011",
|
315
|
+
"010100",
|
316
|
+
"1111111000",
|
317
|
+
"1111111001",
|
318
|
+
"111111111010",
|
319
|
+
"1111111111001",
|
320
|
+
"010101",
|
321
|
+
"11111000",
|
322
|
+
"11111111010",
|
323
|
+
"1111111010",
|
324
|
+
"1111111011",
|
325
|
+
"11111001",
|
326
|
+
"11111111011",
|
327
|
+
"11111010",
|
328
|
+
"010110",
|
329
|
+
"010111",
|
330
|
+
"011000",
|
331
|
+
"00000",
|
332
|
+
"00001",
|
333
|
+
"00010",
|
334
|
+
"011001",
|
335
|
+
"011010",
|
336
|
+
"011011",
|
337
|
+
"011100",
|
338
|
+
"011101",
|
339
|
+
"011110",
|
340
|
+
"011111",
|
341
|
+
"1011100",
|
342
|
+
"11111011",
|
343
|
+
"111111111111100",
|
344
|
+
"100000",
|
345
|
+
"111111111011",
|
346
|
+
"1111111100",
|
347
|
+
"1111111111010",
|
348
|
+
"100001",
|
349
|
+
"1011101",
|
350
|
+
"1011110",
|
351
|
+
"1011111",
|
352
|
+
"1100000",
|
353
|
+
"1100001",
|
354
|
+
"1100010",
|
355
|
+
"1100011",
|
356
|
+
"1100100",
|
357
|
+
"1100101",
|
358
|
+
"1100110",
|
359
|
+
"1100111",
|
360
|
+
"1101000",
|
361
|
+
"1101001",
|
362
|
+
"1101010",
|
363
|
+
"1101011",
|
364
|
+
"1101100",
|
365
|
+
"1101101",
|
366
|
+
"1101110",
|
367
|
+
"1101111",
|
368
|
+
"1110000",
|
369
|
+
"1110001",
|
370
|
+
"1110010",
|
371
|
+
"11111100",
|
372
|
+
"1110011",
|
373
|
+
"11111101",
|
374
|
+
"1111111111011",
|
375
|
+
"1111111111111110000",
|
376
|
+
"1111111111100",
|
377
|
+
"11111111111100",
|
378
|
+
"100010",
|
379
|
+
"111111111111101",
|
380
|
+
"00011",
|
381
|
+
"100011",
|
382
|
+
"00100",
|
383
|
+
"100100",
|
384
|
+
"00101",
|
385
|
+
"100101",
|
386
|
+
"100110",
|
387
|
+
"100111",
|
388
|
+
"00110",
|
389
|
+
"1110100",
|
390
|
+
"1110101",
|
391
|
+
"101000",
|
392
|
+
"101001",
|
393
|
+
"101010",
|
394
|
+
"00111",
|
395
|
+
"101011",
|
396
|
+
"1110110",
|
397
|
+
"101100",
|
398
|
+
"01000",
|
399
|
+
"01001",
|
400
|
+
"101101",
|
401
|
+
"1110111",
|
402
|
+
"1111000",
|
403
|
+
"1111001",
|
404
|
+
"1111010",
|
405
|
+
"1111011",
|
406
|
+
"111111111111110",
|
407
|
+
"'",
|
408
|
+
"11111111111101",
|
409
|
+
"1111111111101",
|
410
|
+
"1111111111111111111111111100",
|
411
|
+
"11111111111111100110",
|
412
|
+
"1111111111111111010010",
|
413
|
+
"11111111111111100111",
|
414
|
+
"11111111111111101000",
|
415
|
+
"1111111111111111010011",
|
416
|
+
"1111111111111111010100",
|
417
|
+
"1111111111111111010101",
|
418
|
+
"11111111111111111011001",
|
419
|
+
"1111111111111111010110",
|
420
|
+
"11111111111111111011010",
|
421
|
+
"11111111111111111011011",
|
422
|
+
"11111111111111111011100",
|
423
|
+
"11111111111111111011101",
|
424
|
+
"11111111111111111011110",
|
425
|
+
"111111111111111111101011",
|
426
|
+
"11111111111111111011111",
|
427
|
+
"111111111111111111101100",
|
428
|
+
"111111111111111111101101",
|
429
|
+
"1111111111111111010111",
|
430
|
+
"11111111111111111100000",
|
431
|
+
"111111111111111111101110",
|
432
|
+
"11111111111111111100001",
|
433
|
+
"11111111111111111100010",
|
434
|
+
"11111111111111111100011",
|
435
|
+
"11111111111111111100100",
|
436
|
+
"111111111111111011100",
|
437
|
+
"1111111111111111011000",
|
438
|
+
"11111111111111111100101",
|
439
|
+
"1111111111111111011001",
|
440
|
+
"11111111111111111100110",
|
441
|
+
"11111111111111111100111",
|
442
|
+
"111111111111111111101111",
|
443
|
+
"1111111111111111011010",
|
444
|
+
"111111111111111011101",
|
445
|
+
"11111111111111101001",
|
446
|
+
"1111111111111111011011",
|
447
|
+
"1111111111111111011100",
|
448
|
+
"11111111111111111101000",
|
449
|
+
"11111111111111111101001",
|
450
|
+
"111111111111111011110",
|
451
|
+
"11111111111111111101010",
|
452
|
+
"1111111111111111011101",
|
453
|
+
"1111111111111111011110",
|
454
|
+
"111111111111111111110000",
|
455
|
+
"111111111111111011111",
|
456
|
+
"1111111111111111011111",
|
457
|
+
"11111111111111111101011",
|
458
|
+
"11111111111111111101100",
|
459
|
+
"111111111111111100000",
|
460
|
+
"111111111111111100001",
|
461
|
+
"1111111111111111100000",
|
462
|
+
"111111111111111100010",
|
463
|
+
"11111111111111111101101",
|
464
|
+
"1111111111111111100001",
|
465
|
+
"11111111111111111101110",
|
466
|
+
"11111111111111111101111",
|
467
|
+
"11111111111111101010",
|
468
|
+
"1111111111111111100010",
|
469
|
+
"1111111111111111100011",
|
470
|
+
"1111111111111111100100",
|
471
|
+
"11111111111111111110000",
|
472
|
+
"1111111111111111100101",
|
473
|
+
"1111111111111111100110",
|
474
|
+
"11111111111111111110001",
|
475
|
+
"11111111111111111111100000",
|
476
|
+
"11111111111111111111100001",
|
477
|
+
"11111111111111101011",
|
478
|
+
"1111111111111110001",
|
479
|
+
"1111111111111111100111",
|
480
|
+
"11111111111111111110010",
|
481
|
+
"1111111111111111101000",
|
482
|
+
"1111111111111111111101100",
|
483
|
+
"11111111111111111111100010",
|
484
|
+
"11111111111111111111100011",
|
485
|
+
"11111111111111111111100100",
|
486
|
+
"111111111111111111111011110",
|
487
|
+
"111111111111111111111011111",
|
488
|
+
"11111111111111111111100101",
|
489
|
+
"111111111111111111110001",
|
490
|
+
"1111111111111111111101101",
|
491
|
+
"1111111111111110010",
|
492
|
+
"111111111111111100011",
|
493
|
+
"11111111111111111111100110",
|
494
|
+
"111111111111111111111100000",
|
495
|
+
"111111111111111111111100001",
|
496
|
+
"11111111111111111111100111",
|
497
|
+
"111111111111111111111100010",
|
498
|
+
"111111111111111111110010",
|
499
|
+
"111111111111111100100",
|
500
|
+
"111111111111111100101",
|
501
|
+
"11111111111111111111101000",
|
502
|
+
"11111111111111111111101001",
|
503
|
+
"1111111111111111111111111101",
|
504
|
+
"111111111111111111111100011",
|
505
|
+
"111111111111111111111100100",
|
506
|
+
"111111111111111111111100101",
|
507
|
+
"11111111111111101100",
|
508
|
+
"111111111111111111110011",
|
509
|
+
"11111111111111101101",
|
510
|
+
"111111111111111100110",
|
511
|
+
"1111111111111111101001",
|
512
|
+
"111111111111111100111",
|
513
|
+
"111111111111111101000",
|
514
|
+
"11111111111111111110011",
|
515
|
+
"1111111111111111101010",
|
516
|
+
"1111111111111111101011",
|
517
|
+
"1111111111111111111101110",
|
518
|
+
"1111111111111111111101111",
|
519
|
+
"111111111111111111110100",
|
520
|
+
"111111111111111111110101",
|
521
|
+
"11111111111111111111101010",
|
522
|
+
"11111111111111111110100",
|
523
|
+
"11111111111111111111101011",
|
524
|
+
"111111111111111111111100110",
|
525
|
+
"11111111111111111111101100",
|
526
|
+
"11111111111111111111101101",
|
527
|
+
"111111111111111111111100111",
|
528
|
+
"111111111111111111111101000",
|
529
|
+
"111111111111111111111101001",
|
530
|
+
"111111111111111111111101010",
|
531
|
+
"111111111111111111111101011",
|
532
|
+
"1111111111111111111111111110",
|
533
|
+
"111111111111111111111101100",
|
534
|
+
"111111111111111111111101101",
|
535
|
+
"111111111111111111111101110",
|
536
|
+
"111111111111111111111101111",
|
537
|
+
"111111111111111111111110000",
|
538
|
+
"11111111111111111111101110",
|
539
|
+
"111111111111111111111111111111"].each_with_index.with_object({}) {|a, h| h[a[0]] = a[1] }
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|