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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 51d03f6b35a78289c742fbed134a68963bc5ddd6
4
- data.tar.gz: 8d541074dd1f8fbde916034e659b28816417e863
3
+ metadata.gz: 4562f69fd3e73d156c4007d70400f07973c143ea
4
+ data.tar.gz: 14264f438e3e200ad52f18f63881087854da88b1
5
5
  SHA512:
6
- metadata.gz: 3365f3efe45797ab9fef24fd61e003425debbc680dc190ca14ffc7797fda812e520c1849a95232f199e3a50b220d04f37e8e922d8a72a8cc65bc1eaa605cfec7
7
- data.tar.gz: 3cb5df7d237484fe18fd8828f33a682d210339821322b7c2e709a826bde3cb66c9e0d638a5843b46e28fbab56cf09a0203bed7848ec3383174d3399ab2190c8f
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
- Iodine is intended to replace the use of a generic reacor, such as EventMachine or GReactor and it hides all the nasty details of creating the event loop.
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 with setting all the tasks up (upon exit).
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
- ## Server Usage
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.
@@ -1,7 +1,48 @@
1
- require "logger"
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/ssl_protocol"
57
+ require "iodine/ssl_connector"
17
58
  require "iodine/io"
59
+
60
+ # require 'iodine/http'
@@ -8,47 +8,18 @@ module Iodine
8
8
  #
9
9
  # use:
10
10
  #
11
- # GReactor.run_async(arg1, arg2, arg3 ...) { |arg1, arg2, arg3...| do_something }
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 [GReactor] always returns the reactor object.
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 30 seconds.\n"
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 30; t.kill; t.kill } }
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
@@ -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