kantan 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitmodules +6 -0
- data/CODE_OF_CONDUCT.md +77 -0
- data/Gemfile +3 -0
- data/LICENSE +201 -0
- data/README.md +44 -0
- data/Rakefile +40 -0
- data/examples/client.rb +98 -0
- data/examples/fetch_google.rb +78 -0
- data/examples/server.rb +87 -0
- data/examples/socket_abstraction.rb +123 -0
- data/examples/tls.rb +203 -0
- data/kantan.gemspec +22 -0
- data/lib/kantan/errors.rb +70 -0
- data/lib/kantan/hpack.rb +381 -0
- data/lib/kantan/huffman.rb +281 -0
- data/lib/kantan/rack_handler.rb +109 -0
- data/lib/kantan/stream.rb +87 -0
- data/lib/kantan/version.rb +5 -0
- data/lib/kantan.rb +1015 -0
- data/profile.rb +31 -0
- data/server.log +25 -0
- data/test/h2spec_server.rb +34 -0
- data/test/helper.rb +25 -0
- data/test/ssl_server.rb +69 -0
- data/test/test_hpack.rb +135 -0
- data/test/test_http2.rb +275 -0
- data/test/test_rack_handler.rb +307 -0
- data/test.rb +31 -0
- metadata +143 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require_relative '../lib/http2'
|
|
7
|
+
|
|
8
|
+
# This example demonstrates how the HTTP/2 implementation is abstracted
|
|
9
|
+
# from the underlying transport. You can pass ANY IO-like object that
|
|
10
|
+
# supports read() and write() methods.
|
|
11
|
+
|
|
12
|
+
puts "HTTP/2 Socket Abstraction Examples"
|
|
13
|
+
puts "=" * 60
|
|
14
|
+
|
|
15
|
+
# Example 1: Unix Domain Socket Pair
|
|
16
|
+
puts "\n1. Unix Domain Socket Pair (for testing)"
|
|
17
|
+
puts "-" * 60
|
|
18
|
+
|
|
19
|
+
client_sock, server_sock = Socket.pair(:UNIX, :STREAM, 0)
|
|
20
|
+
|
|
21
|
+
server_thread = Thread.new do
|
|
22
|
+
http2 = Kantan::Server.new(server_sock)
|
|
23
|
+
http2.on_stream do |stream|
|
|
24
|
+
http2.send_headers(stream.id, [[":status", "200"]])
|
|
25
|
+
http2.send_data(stream.id, "Response from Unix socket", end_stream: true)
|
|
26
|
+
end
|
|
27
|
+
http2.start
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
client_thread = Thread.new do
|
|
31
|
+
sleep 0.1
|
|
32
|
+
http2 = Kantan::Client.new(client_sock)
|
|
33
|
+
http2.on_data { |stream, data| puts " Received: #{data}" }
|
|
34
|
+
http2.start
|
|
35
|
+
http2.request([[":method", "GET"], [":path", "/"], [":scheme", "https"], [":authority", "test"]])
|
|
36
|
+
Thread.new { http2.run }
|
|
37
|
+
sleep 0.5
|
|
38
|
+
http2.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
client_thread.join
|
|
42
|
+
server_thread.join(1)
|
|
43
|
+
|
|
44
|
+
client_sock.close rescue nil
|
|
45
|
+
server_sock.close rescue nil
|
|
46
|
+
|
|
47
|
+
# Example 2: TCP Socket
|
|
48
|
+
puts "\n2. TCP Socket (standard network connection)"
|
|
49
|
+
puts "-" * 60
|
|
50
|
+
puts " Server: Kantan::Server.new(tcp_socket)"
|
|
51
|
+
puts " Client: Kantan::Client.new(TCPSocket.new('host', port))"
|
|
52
|
+
|
|
53
|
+
# Example 3: SSL/TLS Socket
|
|
54
|
+
puts "\n3. SSL/TLS Socket (secure connection)"
|
|
55
|
+
puts "-" * 60
|
|
56
|
+
puts " Server: Kantan::Server.new(ssl_socket)"
|
|
57
|
+
puts " Client: Kantan::Client.new(OpenSSL::SSL::SSLSocket.new(...))"
|
|
58
|
+
puts " See tls_example.rb for full implementation"
|
|
59
|
+
|
|
60
|
+
# Example 4: Pipe (for IPC)
|
|
61
|
+
puts "\n4. Pipe (inter-process communication)"
|
|
62
|
+
puts "-" * 60
|
|
63
|
+
|
|
64
|
+
reader, writer = IO.pipe
|
|
65
|
+
writer.binmode
|
|
66
|
+
reader.binmode
|
|
67
|
+
|
|
68
|
+
fork_pid = fork do
|
|
69
|
+
writer.close
|
|
70
|
+
http2 = Kantan::Server.new(reader)
|
|
71
|
+
http2.on_stream do |stream|
|
|
72
|
+
http2.send_headers(stream.id, [[":status", "200"]])
|
|
73
|
+
http2.send_data(stream.id, "Response from pipe", end_stream: true)
|
|
74
|
+
end
|
|
75
|
+
http2.start
|
|
76
|
+
exit
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
reader.close
|
|
80
|
+
|
|
81
|
+
http2_client = Kantan::Client.new(writer)
|
|
82
|
+
response_data = nil
|
|
83
|
+
|
|
84
|
+
http2_client.on_data do |stream, data|
|
|
85
|
+
response_data = data
|
|
86
|
+
puts " Received: #{data}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
http2_client.start
|
|
90
|
+
http2_client.request([[":method", "GET"], [":path", "/"], [":scheme", "https"], [":authority", "test"]])
|
|
91
|
+
|
|
92
|
+
Thread.new { http2_client.run }
|
|
93
|
+
sleep 0.5
|
|
94
|
+
|
|
95
|
+
Process.wait(fork_pid)
|
|
96
|
+
writer.close rescue nil
|
|
97
|
+
|
|
98
|
+
# Example 5: Frame-level abstraction (using StringIO for testing)
|
|
99
|
+
puts "\n5. Frame-level testing (using StringIO)"
|
|
100
|
+
puts "-" * 60
|
|
101
|
+
|
|
102
|
+
# Create a frame
|
|
103
|
+
frame = Kantan::Frame.new(
|
|
104
|
+
type: :settings,
|
|
105
|
+
flags: 0,
|
|
106
|
+
stream_id: 0,
|
|
107
|
+
payload: ""
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Encode to binary
|
|
111
|
+
binary = frame.to_binary
|
|
112
|
+
puts " Frame encoded: #{binary.bytesize} bytes"
|
|
113
|
+
|
|
114
|
+
# Decode from binary using StringIO
|
|
115
|
+
io = StringIO.new(binary)
|
|
116
|
+
decoded_frame = Kantan::Frame.parse(io)
|
|
117
|
+
puts " Frame decoded: type=#{decoded_frame.type}, stream_id=#{decoded_frame.stream_id}"
|
|
118
|
+
|
|
119
|
+
puts "\n" + "=" * 60
|
|
120
|
+
puts "Key Takeaway: The HTTP/2 implementation works with ANY"
|
|
121
|
+
puts "IO-like object (Socket, TCPSocket, SSLSocket, Pipe, etc.)"
|
|
122
|
+
puts "as long as it supports read() and write() methods."
|
|
123
|
+
puts "=" * 60
|
data/examples/tls.rb
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require_relative '../lib/http2'
|
|
7
|
+
|
|
8
|
+
# Example of using HTTP/2 with TLS/SSL
|
|
9
|
+
# This demonstrates how to wrap sockets with SSL for secure HTTP/2
|
|
10
|
+
|
|
11
|
+
def create_self_signed_cert
|
|
12
|
+
# Generate a self-signed certificate for testing
|
|
13
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
14
|
+
|
|
15
|
+
cert = OpenSSL::X509::Certificate.new
|
|
16
|
+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse("/C=US/O=Test/CN=localhost")
|
|
17
|
+
cert.not_before = Time.now
|
|
18
|
+
cert.not_after = Time.now + 365 * 24 * 60 * 60
|
|
19
|
+
cert.public_key = key.public_key
|
|
20
|
+
cert.serial = 0x0
|
|
21
|
+
cert.version = 2
|
|
22
|
+
|
|
23
|
+
# Add extensions
|
|
24
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
|
25
|
+
ef.subject_certificate = cert
|
|
26
|
+
ef.issuer_certificate = cert
|
|
27
|
+
|
|
28
|
+
cert.extensions = [
|
|
29
|
+
ef.create_extension("basicConstraints", "CA:TRUE", true),
|
|
30
|
+
ef.create_extension("subjectKeyIdentifier", "hash"),
|
|
31
|
+
ef.create_extension("subjectAltName", "DNS:localhost,IP:127.0.0.1")
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
cert.sign(key, OpenSSL::Digest::SHA256.new)
|
|
35
|
+
|
|
36
|
+
[cert, key]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run_tls_server
|
|
40
|
+
puts "Starting HTTP/2 over TLS server on port 8443..."
|
|
41
|
+
|
|
42
|
+
# Create TCP server
|
|
43
|
+
tcp_server = TCPServer.new(8443)
|
|
44
|
+
|
|
45
|
+
# Generate self-signed certificate
|
|
46
|
+
cert, key = create_self_signed_cert
|
|
47
|
+
|
|
48
|
+
# Setup SSL context
|
|
49
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
50
|
+
ssl_context.cert = cert
|
|
51
|
+
ssl_context.key = key
|
|
52
|
+
|
|
53
|
+
# Enable HTTP/2 via ALPN (Application-Layer Protocol Negotiation)
|
|
54
|
+
ssl_context.alpn_protocols = ['h2']
|
|
55
|
+
|
|
56
|
+
# Create SSL server
|
|
57
|
+
ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ssl_context)
|
|
58
|
+
|
|
59
|
+
puts "Server ready. Waiting for connections..."
|
|
60
|
+
puts "ALPN protocols: #{ssl_context.alpn_protocols.inspect}"
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
# Accept SSL connection
|
|
64
|
+
ssl_socket = ssl_server.accept
|
|
65
|
+
puts "\nNew connection from #{ssl_socket.peeraddr[2]}"
|
|
66
|
+
puts " Protocol: #{ssl_socket.alpn_protocol}"
|
|
67
|
+
|
|
68
|
+
# Verify HTTP/2 was negotiated
|
|
69
|
+
unless ssl_socket.alpn_protocol == 'h2'
|
|
70
|
+
puts " Warning: HTTP/2 not negotiated, got: #{ssl_socket.alpn_protocol}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
Thread.new do
|
|
74
|
+
begin
|
|
75
|
+
# Create HTTP/2 server with the SSL socket
|
|
76
|
+
http2_server = Kantan::Server.new(ssl_socket)
|
|
77
|
+
|
|
78
|
+
http2_server.on_headers do |stream, headers|
|
|
79
|
+
puts "\n[Stream #{stream.id}] Headers:"
|
|
80
|
+
headers.each { |name, value| puts " #{name}: #{value}" }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
http2_server.on_stream do |stream|
|
|
84
|
+
method = stream.received_headers.find { |n, _| n == ":method" }&.last
|
|
85
|
+
path = stream.received_headers.find { |n, _| n == ":path" }&.last
|
|
86
|
+
|
|
87
|
+
puts "[Stream #{stream.id}] Request: #{method} #{path}"
|
|
88
|
+
|
|
89
|
+
# Generate response
|
|
90
|
+
body = "Secure HTTP/2 response!\n\nYou are connected via TLS.\n"
|
|
91
|
+
body += "Time: #{Time.now}\n"
|
|
92
|
+
|
|
93
|
+
response_headers = [
|
|
94
|
+
[":status", "200"],
|
|
95
|
+
["content-type", "text/plain"],
|
|
96
|
+
["content-length", body.bytesize.to_s],
|
|
97
|
+
["server", "Pure Ruby HTTP/2 with TLS"]
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
http2_server.send_headers(stream.id, response_headers)
|
|
101
|
+
http2_server.send_data(stream.id, body, end_stream: true)
|
|
102
|
+
|
|
103
|
+
puts "[Stream #{stream.id}] Response sent"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
http2_server.start
|
|
107
|
+
rescue => e
|
|
108
|
+
puts "Error: #{e.message}"
|
|
109
|
+
puts e.backtrace[0..5]
|
|
110
|
+
ensure
|
|
111
|
+
ssl_socket.close unless ssl_socket.closed?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
rescue Interrupt
|
|
116
|
+
puts "\nShutting down server..."
|
|
117
|
+
ensure
|
|
118
|
+
ssl_server&.close
|
|
119
|
+
tcp_server&.close
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def run_tls_client
|
|
123
|
+
puts "Connecting to HTTP/2 over TLS server..."
|
|
124
|
+
|
|
125
|
+
# Create TCP socket
|
|
126
|
+
tcp_socket = TCPSocket.new('localhost', 8443)
|
|
127
|
+
|
|
128
|
+
# Setup SSL context
|
|
129
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
130
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE # For self-signed cert
|
|
131
|
+
ssl_context.alpn_protocols = ['h2']
|
|
132
|
+
|
|
133
|
+
# Create SSL socket
|
|
134
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
|
135
|
+
ssl_socket.sync_close = true
|
|
136
|
+
ssl_socket.connect
|
|
137
|
+
|
|
138
|
+
puts "Connected via TLS"
|
|
139
|
+
puts " Protocol: #{ssl_socket.alpn_protocol}"
|
|
140
|
+
puts " Cipher: #{ssl_socket.cipher[0]}"
|
|
141
|
+
|
|
142
|
+
# Create HTTP/2 client with the SSL socket
|
|
143
|
+
http2_client = Kantan::Client.new(ssl_socket)
|
|
144
|
+
|
|
145
|
+
http2_client.on_headers do |stream, headers|
|
|
146
|
+
puts "\n[Stream #{stream.id}] Response headers:"
|
|
147
|
+
headers.each { |name, value| puts " #{name}: #{value}" }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
http2_client.on_data do |stream, data|
|
|
151
|
+
puts "\n[Stream #{stream.id}] Response body:"
|
|
152
|
+
puts data
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Start connection
|
|
156
|
+
http2_client.start
|
|
157
|
+
|
|
158
|
+
# Make request
|
|
159
|
+
puts "\nSending request..."
|
|
160
|
+
request_headers = [
|
|
161
|
+
[":method", "GET"],
|
|
162
|
+
[":path", "/secure"],
|
|
163
|
+
[":scheme", "https"],
|
|
164
|
+
[":authority", "localhost:8443"]
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
stream = http2_client.request(request_headers)
|
|
168
|
+
|
|
169
|
+
# Run event loop
|
|
170
|
+
Thread.new { http2_client.run }
|
|
171
|
+
|
|
172
|
+
sleep 2
|
|
173
|
+
|
|
174
|
+
http2_client.close
|
|
175
|
+
puts "\nConnection closed"
|
|
176
|
+
rescue => e
|
|
177
|
+
puts "Error: #{e.message}"
|
|
178
|
+
puts e.backtrace[0..5]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Main
|
|
182
|
+
if __FILE__ == $0
|
|
183
|
+
if ARGV[0] == 'server'
|
|
184
|
+
run_tls_server
|
|
185
|
+
elsif ARGV[0] == 'client'
|
|
186
|
+
run_tls_client
|
|
187
|
+
else
|
|
188
|
+
puts "HTTP/2 over TLS Example"
|
|
189
|
+
puts "=" * 60
|
|
190
|
+
puts
|
|
191
|
+
puts "Usage:"
|
|
192
|
+
puts " ruby #{__FILE__} server # Run the server"
|
|
193
|
+
puts " ruby #{__FILE__} client # Run the client"
|
|
194
|
+
puts
|
|
195
|
+
puts "To test:"
|
|
196
|
+
puts " 1. In one terminal: ruby #{__FILE__} server"
|
|
197
|
+
puts " 2. In another terminal: ruby #{__FILE__} client"
|
|
198
|
+
puts
|
|
199
|
+
puts "Or test with curl (if built with HTTP/2 support):"
|
|
200
|
+
puts " curl -k --http2 https://localhost:8443/secure"
|
|
201
|
+
puts
|
|
202
|
+
end
|
|
203
|
+
end
|
data/kantan.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
$: << File.expand_path("lib")
|
|
2
|
+
|
|
3
|
+
require "kantan/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "kantan"
|
|
7
|
+
s.version = Kantan::VERSION
|
|
8
|
+
s.summary = "HTTP/2.0 parser written in Ruby"
|
|
9
|
+
s.description = "An HTTP/2.0 parser in Ruby"
|
|
10
|
+
s.authors = ["Aaron Patterson"]
|
|
11
|
+
s.email = "tenderlove@ruby-lang.org"
|
|
12
|
+
s.files = `git ls-files -z`.split("\x0")
|
|
13
|
+
s.test_files = s.files.grep(%r{^test/})
|
|
14
|
+
s.homepage = "https://github.com/tenderlove/kantan"
|
|
15
|
+
s.license = "Apache-2.0"
|
|
16
|
+
|
|
17
|
+
s.add_development_dependency 'minitest', '~> 5'
|
|
18
|
+
s.add_development_dependency 'rake', '~> 13.0'
|
|
19
|
+
s.add_development_dependency 'logger', '~> 1.0'
|
|
20
|
+
s.add_development_dependency 'rack', '~> 3'
|
|
21
|
+
s.add_development_dependency 'concurrent-ruby', '~> 1.3'
|
|
22
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kantan
|
|
4
|
+
module Errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
class ConnectionError < Error
|
|
8
|
+
attr_reader :remaining, :error_code
|
|
9
|
+
|
|
10
|
+
def initialize msg, remaining, error_code = 0x1
|
|
11
|
+
super(msg)
|
|
12
|
+
@remaining = remaining
|
|
13
|
+
@error_code = error_code
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class ProtocolError < ConnectionError
|
|
18
|
+
def initialize msg, remaining
|
|
19
|
+
super(msg, remaining, 0x1)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class FrameSizeError < ConnectionError
|
|
24
|
+
def initialize msg, remaining
|
|
25
|
+
super(msg, remaining, 0x6)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class FlowControlError < ConnectionError
|
|
30
|
+
def initialize msg, remaining
|
|
31
|
+
super(msg, remaining, 0x3)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class StreamClosedError < ConnectionError
|
|
36
|
+
def initialize msg, remaining
|
|
37
|
+
super(msg, remaining, 0x5)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class CompressionError < ConnectionError
|
|
42
|
+
def initialize msg = "Compression error", remaining = 0
|
|
43
|
+
super(msg, remaining, 0x9)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class StreamError < Error
|
|
48
|
+
attr_reader :stream_id, :error_code, :remaining
|
|
49
|
+
|
|
50
|
+
def initialize msg, stream_id, remaining = 0, error_code = 0x1
|
|
51
|
+
super(msg)
|
|
52
|
+
@stream_id = stream_id
|
|
53
|
+
@remaining = remaining
|
|
54
|
+
@error_code = error_code
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class StreamClosed < StreamError
|
|
59
|
+
def initialize msg, stream_id, remaining = 0
|
|
60
|
+
super(msg, stream_id, remaining, 0x5)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class RefusedStream < StreamError
|
|
65
|
+
def initialize msg, stream_id, remaining = 0
|
|
66
|
+
super(msg, stream_id, remaining, 0x7)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|