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.
@@ -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