protocol-htty 0.2.0 → 0.3.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 +78 -22
- data/lib/protocol/htty/version.rb +1 -1
- data/readme.md +5 -0
- data/releases.md +5 -0
- data/test/protocol/htty/bootstrap.rb +3 -4
- data/test/protocol/htty/pty.rb +9 -4
- data/test/protocol/htty/stream.rb +16 -6
- data.tar.gz.sig +0 -0
- metadata +1 -1
- 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: 87a3aa4c350c134deb40265ae418e15fbd1158b70a8cf4c21e1748de514ca867
|
|
4
|
+
data.tar.gz: 506e9101a8df67e75aed817e3227919e06dc00ee5aa8d58d62c6d7139ae8d1ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82d2c3b0fbbd56482711a10da4dad9e0e86177168d1cbc198d126e1d0c347e27dd3123e5d3982f9884e7bc2ee8b8f1be429164f06f16d5c3dde65ff5424750e3
|
|
7
|
+
data.tar.gz: cb61305b1df49d733cc5b4e0755d2d8962813eafc1e3fb2137122014643b7b7a90bfd44e2490edd428eb32b8bb9a7e31745bfeaa8b8b847ea1ad39d5c2f8aa25
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/protocol/htty/stream.rb
CHANGED
|
@@ -15,8 +15,10 @@ module Protocol
|
|
|
15
15
|
BOOTSTRAP_PREFIX = "+H"
|
|
16
16
|
RAW_MODE = "raw"
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
HTTP2_FRAME_HEADER_SIZE = 9
|
|
19
|
+
|
|
20
|
+
def self.open(input, output, bootstrap: nil, mode: RAW_MODE)
|
|
21
|
+
stream = self.new(input, output)
|
|
20
22
|
|
|
21
23
|
case bootstrap
|
|
22
24
|
when :write
|
|
@@ -32,23 +34,27 @@ 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 = ::IO::Stream(output)
|
|
43
|
+
@frame_remaining = nil
|
|
39
44
|
@local_closed = false
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
attr :
|
|
47
|
+
attr :input
|
|
48
|
+
attr :output
|
|
43
49
|
|
|
44
|
-
# Return the underlying
|
|
50
|
+
# Return the underlying output stream.
|
|
45
51
|
def io
|
|
46
|
-
@
|
|
52
|
+
@output
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def write_bootstrap(mode = RAW_MODE)
|
|
50
|
-
@
|
|
51
|
-
@
|
|
56
|
+
@output.write("#{DCS}#{BOOTSTRAP_PREFIX}#{mode}#{ST}")
|
|
57
|
+
@output.flush
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
def read_bootstrap
|
|
@@ -68,17 +74,36 @@ module Protocol
|
|
|
68
74
|
|
|
69
75
|
# Read application bytes from the HTTY transport.
|
|
70
76
|
def read(length = nil)
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
if length == 0
|
|
78
|
+
@frame_remaining = nil if @frame_remaining == 0
|
|
79
|
+
return +"".b
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
requested_length = length
|
|
83
|
+
length = [length, @frame_remaining].min if length && @frame_remaining && @frame_remaining > 0
|
|
84
|
+
buffer = read_exact(length)
|
|
85
|
+
|
|
86
|
+
if buffer && requested_length == HTTP2_FRAME_HEADER_SIZE && !@frame_remaining
|
|
87
|
+
if buffer.bytesize == HTTP2_FRAME_HEADER_SIZE
|
|
88
|
+
frame_length = self.class.frame_length(buffer)
|
|
89
|
+
@frame_remaining = frame_length if frame_length > 0
|
|
90
|
+
end
|
|
91
|
+
elsif buffer && @frame_remaining
|
|
92
|
+
@frame_remaining -= buffer.bytesize
|
|
93
|
+
@frame_remaining = nil if @frame_remaining <= 0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return buffer
|
|
73
97
|
end
|
|
74
98
|
|
|
75
99
|
# Write application bytes after bootstrap.
|
|
76
100
|
# @returns [self]
|
|
77
101
|
# @raises [IOError] If the local side of the transport is closed.
|
|
78
|
-
def write(data)
|
|
102
|
+
def write(data, flush: false)
|
|
79
103
|
raise IOError, "HTTY stream is closed for writing!" if @local_closed
|
|
80
104
|
|
|
81
|
-
@
|
|
105
|
+
@output.write(data.to_s.b)
|
|
106
|
+
@output.flush if flush
|
|
82
107
|
|
|
83
108
|
return self
|
|
84
109
|
end
|
|
@@ -86,7 +111,7 @@ module Protocol
|
|
|
86
111
|
# Flush any buffered output through the underlying stream.
|
|
87
112
|
# @returns [void]
|
|
88
113
|
def flush
|
|
89
|
-
@
|
|
114
|
+
@output.flush
|
|
90
115
|
end
|
|
91
116
|
|
|
92
117
|
# Close the local write side of this stream abstraction.
|
|
@@ -95,7 +120,7 @@ module Protocol
|
|
|
95
120
|
def close_write(error = nil)
|
|
96
121
|
unless @local_closed
|
|
97
122
|
@local_closed = true
|
|
98
|
-
@
|
|
123
|
+
@output.flush
|
|
99
124
|
end
|
|
100
125
|
end
|
|
101
126
|
|
|
@@ -110,16 +135,47 @@ module Protocol
|
|
|
110
135
|
# Check whether the remote side may still provide more data.
|
|
111
136
|
# @returns [bool] True if the remote side has not sent or implied a close.
|
|
112
137
|
def readable?
|
|
113
|
-
|
|
138
|
+
!(@input.respond_to?(:closed?) && @input.closed?)
|
|
114
139
|
end
|
|
115
140
|
|
|
116
141
|
private
|
|
117
142
|
|
|
143
|
+
def self.frame_length(buffer)
|
|
144
|
+
length_high, length_low = buffer.unpack("Cn")
|
|
145
|
+
return (length_high << 16) | length_low
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read_exact(length)
|
|
149
|
+
return @input.read if length.nil?
|
|
150
|
+
|
|
151
|
+
buffer = +"".b
|
|
152
|
+
|
|
153
|
+
while buffer.bytesize < length
|
|
154
|
+
chunk = read_some(length - buffer.bytesize)
|
|
155
|
+
break unless chunk
|
|
156
|
+
|
|
157
|
+
buffer << chunk.b
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
return nil if buffer.empty?
|
|
161
|
+
return buffer
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def read_some(length)
|
|
165
|
+
if @input.respond_to?(:readpartial)
|
|
166
|
+
@input.readpartial(length)
|
|
167
|
+
else
|
|
168
|
+
@input.read(length)
|
|
169
|
+
end
|
|
170
|
+
rescue EOFError, Errno::EIO
|
|
171
|
+
return nil
|
|
172
|
+
end
|
|
173
|
+
|
|
118
174
|
def read_payload
|
|
119
|
-
while prefix =
|
|
175
|
+
while prefix = read_some(1)
|
|
120
176
|
next unless prefix == ESC
|
|
121
177
|
|
|
122
|
-
marker =
|
|
178
|
+
marker = read_some(1)
|
|
123
179
|
return nil unless marker
|
|
124
180
|
next unless marker == "P"
|
|
125
181
|
|
|
@@ -132,9 +188,9 @@ module Protocol
|
|
|
132
188
|
def consume_packet
|
|
133
189
|
buffer = +""
|
|
134
190
|
|
|
135
|
-
while chunk =
|
|
191
|
+
while chunk = read_some(1)
|
|
136
192
|
if chunk == ESC
|
|
137
|
-
terminator =
|
|
193
|
+
terminator = read_some(1)
|
|
138
194
|
return buffer if terminator == "\\"
|
|
139
195
|
|
|
140
196
|
buffer << chunk
|
data/readme.md
CHANGED
|
@@ -36,6 +36,11 @@ 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.3.0
|
|
40
|
+
|
|
41
|
+
- Change `Protocol::HTTY::Stream` to take explicit input and output endpoints using `Stream.new(input, output)` and `Stream.open(input, output, **options)`.
|
|
42
|
+
- Read HTTY input without buffering ahead, preserving HTTP/2 frame boundaries by reading only the announced frame payload length.
|
|
43
|
+
|
|
39
44
|
### v0.2.0
|
|
40
45
|
|
|
41
46
|
- 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,10 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.3.0
|
|
4
|
+
|
|
5
|
+
- Change `Protocol::HTTY::Stream` to take explicit input and output endpoints using `Stream.new(input, output)` and `Stream.open(input, output, **options)`.
|
|
6
|
+
- Read HTTY input without buffering ahead, preserving HTTP/2 frame boundaries by reading only the announced frame payload length.
|
|
7
|
+
|
|
3
8
|
## v0.2.0
|
|
4
9
|
|
|
5
10
|
- 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
|
|
|
@@ -10,7 +10,7 @@ require "protocol/htty"
|
|
|
10
10
|
|
|
11
11
|
describe Protocol::HTTY::Stream do
|
|
12
12
|
let(:writer) {StringIO.new}
|
|
13
|
-
let(:stream) {subject.open(
|
|
13
|
+
let(:stream) {subject.open(StringIO.new, writer)}
|
|
14
14
|
|
|
15
15
|
it "writes raw bytes after bootstrap" do
|
|
16
16
|
stream.write_bootstrap
|
|
@@ -24,7 +24,7 @@ describe Protocol::HTTY::Stream do
|
|
|
24
24
|
writer.write("\eP+Hraw\e\\#{Protocol::HTTP2::CONNECTION_PREFACE}")
|
|
25
25
|
writer.rewind
|
|
26
26
|
|
|
27
|
-
reader = subject.open(
|
|
27
|
+
reader = subject.open(writer, StringIO.new, bootstrap: :read)
|
|
28
28
|
|
|
29
29
|
expect(reader.read(Protocol::HTTP2::CONNECTION_PREFACE.bytesize)).to be == Protocol::HTTP2::CONNECTION_PREFACE
|
|
30
30
|
expect(reader.read).to be == ""
|
|
@@ -32,7 +32,7 @@ describe Protocol::HTTY::Stream do
|
|
|
32
32
|
|
|
33
33
|
it "writes the bootstrap when opened in write mode" do
|
|
34
34
|
writer = StringIO.new
|
|
35
|
-
stream = subject.open(
|
|
35
|
+
stream = subject.open(StringIO.new, writer, bootstrap: :write)
|
|
36
36
|
|
|
37
37
|
expect(writer.string).to be == "\eP+Hraw\e\\"
|
|
38
38
|
stream.close
|
|
@@ -42,7 +42,7 @@ describe Protocol::HTTY::Stream do
|
|
|
42
42
|
writer.write("hello")
|
|
43
43
|
writer.rewind
|
|
44
44
|
|
|
45
|
-
reader = subject.open(
|
|
45
|
+
reader = subject.open(writer, StringIO.new)
|
|
46
46
|
|
|
47
47
|
expect(reader.read).to be == "hello"
|
|
48
48
|
expect(reader.read).to be == ""
|
|
@@ -86,6 +86,16 @@ describe Protocol::HTTY::Stream do
|
|
|
86
86
|
expect(stream).to be(:readable?)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
it "reads HTTP/2 frame headers and payloads without reading ahead" do
|
|
90
|
+
header = [0, 5, 0, 0, 1].pack("CnCCN")
|
|
91
|
+
input = StringIO.new("#{header}helloextra")
|
|
92
|
+
reader = subject.open(input, StringIO.new)
|
|
93
|
+
|
|
94
|
+
expect(reader.read(9)).to be == header
|
|
95
|
+
expect(reader.read(16)).to be == "hello"
|
|
96
|
+
expect(reader.read(5)).to be == "extra"
|
|
97
|
+
end
|
|
98
|
+
|
|
89
99
|
it "rejects writes after the local side is closed" do
|
|
90
100
|
stream.close
|
|
91
101
|
|
|
@@ -96,7 +106,7 @@ describe Protocol::HTTY::Stream do
|
|
|
96
106
|
|
|
97
107
|
it "wraps raw IO handles using IO::Stream" do
|
|
98
108
|
Tempfile.create("protocol-htty") do |file|
|
|
99
|
-
io_stream = subject.open(file).io
|
|
109
|
+
io_stream = subject.open(file, file).io
|
|
100
110
|
|
|
101
111
|
expect(io_stream).to be(:is_a?, ::IO::Stream::Buffered)
|
|
102
112
|
io_stream.close
|
|
@@ -105,7 +115,7 @@ describe Protocol::HTTY::Stream do
|
|
|
105
115
|
|
|
106
116
|
it "does not close wrapped raw IO handles when closed" do
|
|
107
117
|
Tempfile.create("protocol-htty") do |file|
|
|
108
|
-
wrapped_stream = subject.open(file)
|
|
118
|
+
wrapped_stream = subject.open(file, file)
|
|
109
119
|
|
|
110
120
|
wrapped_stream.close
|
|
111
121
|
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|