protocol-htty 0.2.0 → 0.4.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/htty/stream.rb +37 -25
- data/lib/protocol/htty/version.rb +1 -1
- data/readme.md +10 -0
- data/releases.md +10 -0
- data/test/protocol/htty/bootstrap.rb +3 -4
- data/test/protocol/htty/pty.rb +9 -4
- data/test/protocol/htty/stream.rb +5 -26
- data.tar.gz.sig +3 -2
- metadata +1 -15
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 957aa78a655ceb3136db0acd1614010a25d1cda14b6e0861e6ebfb371870b739
|
|
4
|
+
data.tar.gz: d70de3f0375aef142f90beca94c2bd50f9986bbc6bfaf2920ea64dae13185467
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdc24469f4d8bad151261849af0a4bd85b5ca86d2f6f007404e131cf90ab18f6fd279a82634a46565f3ffa19337c698741103c64220b7c89a8c1261d44652eea
|
|
7
|
+
data.tar.gz: 750995dc4c72f1eb9026bbba25795c8b5489d734d48e93edae8cabad4af12b4bc6a69746550e044f10ceaac2eab3f15fc6e2dddb08a919eccbc953c12c833ef3
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/protocol/htty/stream.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require "io/stream"
|
|
7
|
-
|
|
8
6
|
module Protocol
|
|
9
7
|
module HTTY
|
|
10
8
|
# Transport an opaque byte stream after the HTTY bootstrap handshake.
|
|
@@ -15,8 +13,12 @@ module Protocol
|
|
|
15
13
|
BOOTSTRAP_PREFIX = "+H"
|
|
16
14
|
RAW_MODE = "raw"
|
|
17
15
|
|
|
18
|
-
def self.open(
|
|
19
|
-
stream = self.new(
|
|
16
|
+
def self.open(input, output, bootstrap: nil, mode: RAW_MODE)
|
|
17
|
+
stream = self.new(input, output)
|
|
18
|
+
|
|
19
|
+
# Disable buffering:
|
|
20
|
+
input.sync = true
|
|
21
|
+
output.sync = true
|
|
20
22
|
|
|
21
23
|
case bootstrap
|
|
22
24
|
when :write
|
|
@@ -32,23 +34,26 @@ module Protocol
|
|
|
32
34
|
return stream
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
# Create a stream on top of
|
|
36
|
-
# @parameter
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# Create a stream on top of raw byte-preserving endpoints.
|
|
38
|
+
# @parameter input [IO] The readable endpoint.
|
|
39
|
+
# @parameter output [IO] The writable endpoint.
|
|
40
|
+
def initialize(input, output)
|
|
41
|
+
@input = input
|
|
42
|
+
@output = output
|
|
39
43
|
@local_closed = false
|
|
40
44
|
end
|
|
41
45
|
|
|
42
|
-
attr :
|
|
46
|
+
attr :input
|
|
47
|
+
attr :output
|
|
43
48
|
|
|
44
|
-
# Return the underlying
|
|
49
|
+
# Return the underlying output stream.
|
|
45
50
|
def io
|
|
46
|
-
@
|
|
51
|
+
@output
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def write_bootstrap(mode = RAW_MODE)
|
|
50
|
-
@
|
|
51
|
-
@
|
|
55
|
+
@output.write("#{DCS}#{BOOTSTRAP_PREFIX}#{mode}#{ST}")
|
|
56
|
+
@output.flush
|
|
52
57
|
end
|
|
53
58
|
|
|
54
59
|
def read_bootstrap
|
|
@@ -67,18 +72,19 @@ module Protocol
|
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
# Read application bytes from the HTTY transport.
|
|
75
|
+
# The HTTP/2 framer always requests exact byte counts (header size, then payload length), so we delegate directly to the underlying input.
|
|
70
76
|
def read(length = nil)
|
|
71
|
-
|
|
72
|
-
return @stream.read(length)
|
|
77
|
+
@input.read(length)
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
# Write application bytes after bootstrap.
|
|
76
81
|
# @returns [self]
|
|
77
82
|
# @raises [IOError] If the local side of the transport is closed.
|
|
78
|
-
def write(data)
|
|
83
|
+
def write(data, flush: false)
|
|
79
84
|
raise IOError, "HTTY stream is closed for writing!" if @local_closed
|
|
80
85
|
|
|
81
|
-
@
|
|
86
|
+
@output.write(data.to_s.b)
|
|
87
|
+
@output.flush if flush
|
|
82
88
|
|
|
83
89
|
return self
|
|
84
90
|
end
|
|
@@ -86,7 +92,7 @@ module Protocol
|
|
|
86
92
|
# Flush any buffered output through the underlying stream.
|
|
87
93
|
# @returns [void]
|
|
88
94
|
def flush
|
|
89
|
-
@
|
|
95
|
+
@output.flush
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
# Close the local write side of this stream abstraction.
|
|
@@ -95,7 +101,7 @@ module Protocol
|
|
|
95
101
|
def close_write(error = nil)
|
|
96
102
|
unless @local_closed
|
|
97
103
|
@local_closed = true
|
|
98
|
-
@
|
|
104
|
+
@output.flush
|
|
99
105
|
end
|
|
100
106
|
end
|
|
101
107
|
|
|
@@ -110,16 +116,22 @@ module Protocol
|
|
|
110
116
|
# Check whether the remote side may still provide more data.
|
|
111
117
|
# @returns [bool] True if the remote side has not sent or implied a close.
|
|
112
118
|
def readable?
|
|
113
|
-
|
|
119
|
+
!(@input.respond_to?(:closed?) && @input.closed?)
|
|
114
120
|
end
|
|
115
121
|
|
|
116
122
|
private
|
|
117
123
|
|
|
124
|
+
def read_some(length)
|
|
125
|
+
@input.read(length)
|
|
126
|
+
rescue EOFError, Errno::EIO
|
|
127
|
+
return nil
|
|
128
|
+
end
|
|
129
|
+
|
|
118
130
|
def read_payload
|
|
119
|
-
while prefix =
|
|
131
|
+
while prefix = read_some(1)
|
|
120
132
|
next unless prefix == ESC
|
|
121
133
|
|
|
122
|
-
marker =
|
|
134
|
+
marker = read_some(1)
|
|
123
135
|
return nil unless marker
|
|
124
136
|
next unless marker == "P"
|
|
125
137
|
|
|
@@ -130,11 +142,11 @@ module Protocol
|
|
|
130
142
|
end
|
|
131
143
|
|
|
132
144
|
def consume_packet
|
|
133
|
-
buffer =
|
|
145
|
+
buffer = String.new.b
|
|
134
146
|
|
|
135
|
-
while chunk =
|
|
147
|
+
while chunk = read_some(1)
|
|
136
148
|
if chunk == ESC
|
|
137
|
-
terminator =
|
|
149
|
+
terminator = read_some(1)
|
|
138
150
|
return buffer if terminator == "\\"
|
|
139
151
|
|
|
140
152
|
buffer << chunk
|
data/readme.md
CHANGED
|
@@ -36,6 +36,16 @@ Please see the [project documentation](https://socketry.github.io/protocol-htty/
|
|
|
36
36
|
|
|
37
37
|
Please see the [project releases](https://socketry.github.io/protocol-htty/releases/index) for all releases.
|
|
38
38
|
|
|
39
|
+
### v0.4.0
|
|
40
|
+
|
|
41
|
+
- **Breaking**: Drop the `io-stream` dependency. `Stream.new` and `Stream.open` no longer coerce or wrap their `input` and `output` arguments; raw IO objects are used as-is. Callers that relied on `stream.io` returning an `IO::Stream::Buffered` should wrap the IO themselves before passing it in.
|
|
42
|
+
- Simplify `Stream#read` to delegate directly to the underlying input, since the HTTP/2 framer always requests exact byte counts (header size, then payload length).
|
|
43
|
+
|
|
44
|
+
### v0.3.0
|
|
45
|
+
|
|
46
|
+
- Change `Protocol::HTTY::Stream` to take explicit input and output endpoints using `Stream.new(input, output)` and `Stream.open(input, output, **options)`.
|
|
47
|
+
- Read HTTY input without buffering ahead, preserving HTTP/2 frame boundaries by reading only the announced frame payload length.
|
|
48
|
+
|
|
39
49
|
### v0.2.0
|
|
40
50
|
|
|
41
51
|
- Add `Protocol::HTTY::Stream.open(stream, **options)` as the preferred constructor for bootstrapping HTTY over an existing stream.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
- **Breaking**: Drop the `io-stream` dependency. `Stream.new` and `Stream.open` no longer coerce or wrap their `input` and `output` arguments; raw IO objects are used as-is. Callers that relied on `stream.io` returning an `IO::Stream::Buffered` should wrap the IO themselves before passing it in.
|
|
6
|
+
- Simplify `Stream#read` to delegate directly to the underlying input, since the HTTP/2 framer always requests exact byte counts (header size, then payload length).
|
|
7
|
+
|
|
8
|
+
## v0.3.0
|
|
9
|
+
|
|
10
|
+
- Change `Protocol::HTTY::Stream` to take explicit input and output endpoints using `Stream.new(input, output)` and `Stream.open(input, output, **options)`.
|
|
11
|
+
- Read HTTY input without buffering ahead, preserving HTTP/2 frame boundaries by reading only the announced frame payload length.
|
|
12
|
+
|
|
3
13
|
## v0.2.0
|
|
4
14
|
|
|
5
15
|
- Add `Protocol::HTTY::Stream.open(stream, **options)` as the preferred constructor for bootstrapping HTTY over an existing stream.
|
|
@@ -26,8 +26,7 @@ end
|
|
|
26
26
|
describe Protocol::HTTY::Stream do
|
|
27
27
|
let(:input) {StringIO.new}
|
|
28
28
|
let(:output) {StringIO.new}
|
|
29
|
-
let(:
|
|
30
|
-
let(:stream) {subject.new(io)}
|
|
29
|
+
let(:stream) {subject.new(input, output)}
|
|
31
30
|
|
|
32
31
|
it "writes the HTTY raw bootstrap" do
|
|
33
32
|
stream.write_bootstrap
|
|
@@ -45,7 +44,7 @@ describe Protocol::HTTY::Stream do
|
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
it "reads a bootstrap split across one-byte reads" do
|
|
48
|
-
stream = subject.new(OneByteInput.new("hello\eP+Hraw\e\\"))
|
|
47
|
+
stream = subject.new(OneByteInput.new("hello\eP+Hraw\e\\"), StringIO.new)
|
|
49
48
|
|
|
50
49
|
expect(stream.read_bootstrap).to be == "raw"
|
|
51
50
|
end
|
|
@@ -56,7 +55,7 @@ describe Protocol::HTTY::Stream do
|
|
|
56
55
|
|
|
57
56
|
stream.read_bootstrap
|
|
58
57
|
|
|
59
|
-
expect(
|
|
58
|
+
expect(stream.read(5)).to be == "world"
|
|
60
59
|
end
|
|
61
60
|
|
|
62
61
|
it "raises on unsupported bootstrap modes" do
|
data/test/protocol/htty/pty.rb
CHANGED
|
@@ -20,6 +20,9 @@ class PTYStream
|
|
|
20
20
|
@buffer = +"".b
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
attr :input
|
|
24
|
+
attr :output
|
|
25
|
+
|
|
23
26
|
def read(length)
|
|
24
27
|
while @buffer.bytesize < length
|
|
25
28
|
@buffer << @input.readpartial(4096).b
|
|
@@ -104,7 +107,7 @@ describe "HTTY over a real PTY" do
|
|
|
104
107
|
it "ignores terminal noise before the bootstrap" do
|
|
105
108
|
Timeout.timeout(5) do
|
|
106
109
|
with_fixture("bootstrap") do |stream|
|
|
107
|
-
framer = Protocol::HTTY::Stream.new(stream)
|
|
110
|
+
framer = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
108
111
|
|
|
109
112
|
expect(framer.read_bootstrap).to be == "raw"
|
|
110
113
|
expect(stream.read(3)).to be == "RAW"
|
|
@@ -115,7 +118,7 @@ describe "HTTY over a real PTY" do
|
|
|
115
118
|
it "delivers the HTTP/2 connection preface after raw takeover" do
|
|
116
119
|
Timeout.timeout(5) do
|
|
117
120
|
with_fixture("raw_preface") do |stream|
|
|
118
|
-
framer = Protocol::HTTY::Stream.new(stream)
|
|
121
|
+
framer = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
119
122
|
|
|
120
123
|
expect(framer.read_bootstrap).to be == "raw"
|
|
121
124
|
|
|
@@ -130,7 +133,8 @@ describe "HTTY over a real PTY" do
|
|
|
130
133
|
it "runs an HTTP/2 session until command-side GOAWAY" do
|
|
131
134
|
Timeout.timeout(5) do
|
|
132
135
|
with_fixture("http2_server") do |stream|
|
|
133
|
-
Protocol::HTTY::Stream.new(stream)
|
|
136
|
+
stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
137
|
+
stream.read_bootstrap
|
|
134
138
|
|
|
135
139
|
framer = Protocol::HTTP2::Framer.new(stream)
|
|
136
140
|
client = Client.new(framer)
|
|
@@ -162,7 +166,8 @@ describe "HTTY over a real PTY" do
|
|
|
162
166
|
it "treats command exit after bootstrap without GOAWAY as an abort" do
|
|
163
167
|
Timeout.timeout(5) do
|
|
164
168
|
with_fixture("abort_after_bootstrap") do |stream|
|
|
165
|
-
|
|
169
|
+
stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
170
|
+
expect(stream.read_bootstrap).to be == "raw"
|
|
166
171
|
|
|
167
172
|
framer = Protocol::HTTP2::Framer.new(stream)
|
|
168
173
|
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
# Copyright, 2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "stringio"
|
|
7
|
-
require "tempfile"
|
|
8
7
|
require "protocol/http2/framer"
|
|
9
8
|
require "protocol/htty"
|
|
10
9
|
|
|
11
10
|
describe Protocol::HTTY::Stream do
|
|
12
11
|
let(:writer) {StringIO.new}
|
|
13
|
-
let(:stream) {subject.open(
|
|
12
|
+
let(:stream) {subject.open(StringIO.new, writer)}
|
|
14
13
|
|
|
15
14
|
it "writes raw bytes after bootstrap" do
|
|
16
15
|
stream.write_bootstrap
|
|
@@ -24,7 +23,7 @@ describe Protocol::HTTY::Stream do
|
|
|
24
23
|
writer.write("\eP+Hraw\e\\#{Protocol::HTTP2::CONNECTION_PREFACE}")
|
|
25
24
|
writer.rewind
|
|
26
25
|
|
|
27
|
-
reader = subject.open(
|
|
26
|
+
reader = subject.open(writer, StringIO.new, bootstrap: :read)
|
|
28
27
|
|
|
29
28
|
expect(reader.read(Protocol::HTTP2::CONNECTION_PREFACE.bytesize)).to be == Protocol::HTTP2::CONNECTION_PREFACE
|
|
30
29
|
expect(reader.read).to be == ""
|
|
@@ -32,7 +31,7 @@ describe Protocol::HTTY::Stream do
|
|
|
32
31
|
|
|
33
32
|
it "writes the bootstrap when opened in write mode" do
|
|
34
33
|
writer = StringIO.new
|
|
35
|
-
stream = subject.open(
|
|
34
|
+
stream = subject.open(StringIO.new, writer, bootstrap: :write)
|
|
36
35
|
|
|
37
36
|
expect(writer.string).to be == "\eP+Hraw\e\\"
|
|
38
37
|
stream.close
|
|
@@ -42,14 +41,14 @@ describe Protocol::HTTY::Stream do
|
|
|
42
41
|
writer.write("hello")
|
|
43
42
|
writer.rewind
|
|
44
43
|
|
|
45
|
-
reader = subject.open(
|
|
44
|
+
reader = subject.open(writer, StringIO.new)
|
|
46
45
|
|
|
47
46
|
expect(reader.read).to be == "hello"
|
|
48
47
|
expect(reader.read).to be == ""
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
it "exposes the underlying output stream" do
|
|
52
|
-
expect(stream.io).to be(:
|
|
51
|
+
expect(stream.io).to be(:equal?, writer)
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
it "flushes through the underlying stream" do
|
|
@@ -93,24 +92,4 @@ describe Protocol::HTTY::Stream do
|
|
|
93
92
|
stream.write("hello")
|
|
94
93
|
end.to raise_exception(IOError)
|
|
95
94
|
end
|
|
96
|
-
|
|
97
|
-
it "wraps raw IO handles using IO::Stream" do
|
|
98
|
-
Tempfile.create("protocol-htty") do |file|
|
|
99
|
-
io_stream = subject.open(file).io
|
|
100
|
-
|
|
101
|
-
expect(io_stream).to be(:is_a?, ::IO::Stream::Buffered)
|
|
102
|
-
io_stream.close
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
it "does not close wrapped raw IO handles when closed" do
|
|
107
|
-
Tempfile.create("protocol-htty") do |file|
|
|
108
|
-
wrapped_stream = subject.open(file)
|
|
109
|
-
|
|
110
|
-
wrapped_stream.close
|
|
111
|
-
|
|
112
|
-
expect(file).not.to be(:closed?)
|
|
113
|
-
file.close
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
95
|
end
|
data.tar.gz.sig
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
�g;��M�o��(� Y����Fl��iHs��,E(B�#Ȱ�C���ju�Ύ�o�K-��C�w�CUU����5S��vZET�������#��`����5q�D���!7�Z;G�֘�Y,#D����3P��r�0�D���J"В w
|
|
2
|
+
��f�"����irʅN�y�B�ZҸ�@F] 9?���6_;�є5�"�&���W�D���x�k�Oݭ�g��:(�C��-�B��غ]�luc�Ӟ��xt����
|
|
3
|
+
-3G� ��R^xƹ/��*�ƥ��"���3���o�|��ܗL��(�\
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-htty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -52,20 +52,6 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: io-stream
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - ">="
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.13.0
|
|
62
|
-
type: :runtime
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - ">="
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.13.0
|
|
69
55
|
executables: []
|
|
70
56
|
extensions: []
|
|
71
57
|
extra_rdoc_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|