protocol-htty 0.1.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 +141 -58
- data/lib/protocol/htty/version.rb +1 -1
- data/lib/protocol/htty.rb +0 -1
- data/readme.md +20 -8
- data/releases.md +12 -0
- data/test/protocol/htty/bootstrap.rb +92 -0
- data/test/protocol/htty/pty.rb +200 -0
- data/test/protocol/htty/stream.rb +42 -16
- data.tar.gz.sig +0 -0
- metadata +5 -19
- metadata.gz.sig +0 -0
- data/lib/protocol/htty/framer.rb +0 -112
- data/test/protocol/htty/framer.rb +0 -64
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
|
@@ -7,86 +7,125 @@ require "io/stream"
|
|
|
7
7
|
|
|
8
8
|
module Protocol
|
|
9
9
|
module HTTY
|
|
10
|
-
# Transport an opaque byte stream
|
|
10
|
+
# Transport an opaque byte stream after the HTTY bootstrap handshake.
|
|
11
11
|
class Stream
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
ESC = "\e"
|
|
13
|
+
DCS = "#{ESC}P"
|
|
14
|
+
ST = "#{ESC}\\"
|
|
15
|
+
BOOTSTRAP_PREFIX = "+H"
|
|
16
|
+
RAW_MODE = "raw"
|
|
17
|
+
|
|
18
|
+
HTTP2_FRAME_HEADER_SIZE = 9
|
|
19
|
+
|
|
20
|
+
def self.open(input, output, bootstrap: nil, mode: RAW_MODE)
|
|
21
|
+
stream = self.new(input, output)
|
|
22
|
+
|
|
23
|
+
case bootstrap
|
|
24
|
+
when :write
|
|
25
|
+
stream.write_bootstrap(mode)
|
|
26
|
+
when :read
|
|
27
|
+
actual_mode = stream.read_bootstrap
|
|
28
|
+
|
|
29
|
+
unless actual_mode == mode
|
|
30
|
+
raise ProtocolError, "Expected HTTY bootstrap mode #{mode.inspect}, got #{actual_mode.inspect}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return stream
|
|
35
|
+
end
|
|
36
|
+
|
|
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
|
|
23
44
|
@local_closed = false
|
|
24
|
-
@remote_closed = false
|
|
25
45
|
end
|
|
26
46
|
|
|
27
|
-
attr :
|
|
47
|
+
attr :input
|
|
48
|
+
attr :output
|
|
28
49
|
|
|
29
|
-
# Return the
|
|
30
|
-
# @returns [IO | IO::Stream] The output side of the framed transport.
|
|
50
|
+
# Return the underlying output stream.
|
|
31
51
|
def io
|
|
32
|
-
@
|
|
52
|
+
@output
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def write_bootstrap(mode = RAW_MODE)
|
|
56
|
+
@output.write("#{DCS}#{BOOTSTRAP_PREFIX}#{mode}#{ST}")
|
|
57
|
+
@output.flush
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def read_bootstrap
|
|
61
|
+
while payload = read_payload
|
|
62
|
+
next unless payload.start_with?(BOOTSTRAP_PREFIX)
|
|
63
|
+
mode = payload.delete_prefix(BOOTSTRAP_PREFIX)
|
|
64
|
+
|
|
65
|
+
unless mode == RAW_MODE
|
|
66
|
+
raise ProtocolError, "Unsupported HTTY bootstrap mode: #{mode.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return mode
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return nil
|
|
33
73
|
end
|
|
34
74
|
|
|
35
75
|
# Read application bytes from the HTTY transport.
|
|
36
|
-
# @parameter length [Integer | Nil] The exact number of bytes to read, or `nil` for all buffered bytes.
|
|
37
|
-
# @returns [String | Nil] The requested bytes, an empty binary string for `0`, or `nil` if more data is required or the remote side is closed.
|
|
38
76
|
def read(length = nil)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
77
|
+
if length == 0
|
|
78
|
+
@frame_remaining = nil if @frame_remaining == 0
|
|
79
|
+
return +"".b
|
|
80
|
+
end
|
|
42
81
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
82
|
+
requested_length = length
|
|
83
|
+
length = [length, @frame_remaining].min if length && @frame_remaining && @frame_remaining > 0
|
|
84
|
+
buffer = read_exact(length)
|
|
46
85
|
|
|
47
|
-
if
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
94
|
end
|
|
95
|
+
|
|
96
|
+
return buffer
|
|
52
97
|
end
|
|
53
98
|
|
|
54
|
-
# Write application bytes
|
|
55
|
-
# @parameter data [String | Array(Integer)] The opaque bytes to send.
|
|
99
|
+
# Write application bytes after bootstrap.
|
|
56
100
|
# @returns [self]
|
|
57
101
|
# @raises [IOError] If the local side of the transport is closed.
|
|
58
|
-
def write(data)
|
|
102
|
+
def write(data, flush: false)
|
|
59
103
|
raise IOError, "HTTY stream is closed for writing!" if @local_closed
|
|
60
104
|
|
|
61
|
-
data
|
|
62
|
-
|
|
63
|
-
until data.empty?
|
|
64
|
-
chunk = data.byteslice(0, @packet_size)
|
|
65
|
-
@framer.write_chunk(chunk)
|
|
66
|
-
data = data.byteslice(chunk.bytesize..)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
@framer.flush
|
|
105
|
+
@output.write(data.to_s.b)
|
|
106
|
+
@output.flush if flush
|
|
70
107
|
|
|
71
108
|
return self
|
|
72
109
|
end
|
|
73
110
|
|
|
74
|
-
# Flush any buffered output through the underlying
|
|
111
|
+
# Flush any buffered output through the underlying stream.
|
|
75
112
|
# @returns [void]
|
|
76
113
|
def flush
|
|
77
|
-
@
|
|
114
|
+
@output.flush
|
|
78
115
|
end
|
|
79
116
|
|
|
80
117
|
# Close the local write side of this stream abstraction.
|
|
81
118
|
# HTTY does not define a close packet, and closing this object does not close the underlying terminal IO.
|
|
82
119
|
# @returns [void]
|
|
83
|
-
def
|
|
120
|
+
def close_write(error = nil)
|
|
84
121
|
unless @local_closed
|
|
85
122
|
@local_closed = true
|
|
86
|
-
@
|
|
123
|
+
@output.flush
|
|
87
124
|
end
|
|
88
125
|
end
|
|
89
126
|
|
|
127
|
+
alias close close_write
|
|
128
|
+
|
|
90
129
|
# Check whether the local side of the transport is closed.
|
|
91
130
|
# @returns [bool] True if local writes have been closed.
|
|
92
131
|
def closed?
|
|
@@ -96,28 +135,72 @@ module Protocol
|
|
|
96
135
|
# Check whether the remote side may still provide more data.
|
|
97
136
|
# @returns [bool] True if the remote side has not sent or implied a close.
|
|
98
137
|
def readable?
|
|
99
|
-
|
|
138
|
+
!(@input.respond_to?(:closed?) && @input.closed?)
|
|
100
139
|
end
|
|
101
140
|
|
|
102
141
|
private
|
|
103
142
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
107
156
|
|
|
108
|
-
|
|
109
|
-
@remote_closed = true
|
|
110
|
-
break
|
|
111
|
-
end
|
|
112
|
-
@buffer << chunk
|
|
157
|
+
buffer << chunk.b
|
|
113
158
|
end
|
|
159
|
+
|
|
160
|
+
return nil if buffer.empty?
|
|
161
|
+
return buffer
|
|
114
162
|
end
|
|
115
163
|
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
|
|
174
|
+
def read_payload
|
|
175
|
+
while prefix = read_some(1)
|
|
176
|
+
next unless prefix == ESC
|
|
177
|
+
|
|
178
|
+
marker = read_some(1)
|
|
179
|
+
return nil unless marker
|
|
180
|
+
next unless marker == "P"
|
|
181
|
+
|
|
182
|
+
return consume_packet
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
return nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def consume_packet
|
|
189
|
+
buffer = +""
|
|
190
|
+
|
|
191
|
+
while chunk = read_some(1)
|
|
192
|
+
if chunk == ESC
|
|
193
|
+
terminator = read_some(1)
|
|
194
|
+
return buffer if terminator == "\\"
|
|
195
|
+
|
|
196
|
+
buffer << chunk
|
|
197
|
+
buffer << terminator if terminator
|
|
198
|
+
else
|
|
199
|
+
buffer << chunk
|
|
200
|
+
end
|
|
201
|
+
end
|
|
119
202
|
|
|
120
|
-
|
|
203
|
+
raise EOFError, "Incomplete HTTY chunk!"
|
|
121
204
|
end
|
|
122
205
|
end
|
|
123
206
|
end
|
data/lib/protocol/htty.rb
CHANGED
data/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Protocol::HTTY
|
|
2
2
|
|
|
3
|
-
`protocol-htty` defines a small, terminal-safe
|
|
3
|
+
`protocol-htty` defines a small, terminal-safe bootstrap for carrying a raw HTTP/2 byte stream over terminal-attached sessions.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/socketry/protocol-htty/actions?workflow=Test)
|
|
6
6
|
|
|
@@ -10,34 +10,46 @@ Traditional terminal user interfaces are useful, but they are also a poor fit fo
|
|
|
10
10
|
|
|
11
11
|
In practice, this means TUIs often force applications into compromises: text-heavy layouts, ad-hoc protocols, and bespoke escape-sequence behavior that is hard to standardise across runtimes and terminals.
|
|
12
12
|
|
|
13
|
-
HTTY exists to keep the portability and deployment advantages of terminal workflows while avoiding the need to build an entire application model out of terminal control codes. Instead of asking the terminal stream itself to represent higher-level UI state, HTTY provides a small
|
|
13
|
+
HTTY exists to keep the portability and deployment advantages of terminal workflows while avoiding the need to build an entire application model out of terminal control codes. Instead of asking the terminal stream itself to represent higher-level UI state, HTTY provides a small bootstrap that can hand a terminal session over to a normal plaintext HTTP/2 connection, enabling applications to attach browser surfaces to a normal terminal session over HTTY.
|
|
14
14
|
|
|
15
15
|
## Design
|
|
16
16
|
|
|
17
|
-
HTTY does not model application requests, regions, or resources. It transports the two directions of a single plaintext HTTP/2 (`h2c`) connection over terminal-
|
|
17
|
+
HTTY does not model application requests, regions, or resources. It transports the two directions of a single plaintext HTTP/2 (`h2c`) connection over a raw terminal-attached byte stream without introducing a second session protocol.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
HTTY v1 begins with one DCS bootstrap sequence:
|
|
20
20
|
|
|
21
21
|
``` text
|
|
22
|
-
ESC P
|
|
22
|
+
ESC P + H raw ESC \
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The
|
|
25
|
+
After that bootstrap has been consumed, the session carries plain `h2c` bytes. The takeover layer intentionally stays small so it can be reimplemented in other runtimes.
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
29
29
|
Please see the [project documentation](https://socketry.github.io/protocol-htty/) for more details.
|
|
30
30
|
|
|
31
|
-
- [Getting Started](https://socketry.github.io/protocol-htty/guides/getting-started/index) - This guide explains how to get started with `protocol-htty` for
|
|
31
|
+
- [Getting Started](https://socketry.github.io/protocol-htty/guides/getting-started/index) - This guide explains how to get started with `protocol-htty` for DCS-bootstrapped raw HTTP/2 byte stream transport.
|
|
32
32
|
|
|
33
|
-
- [HTTY Specification](https://socketry.github.io/protocol-htty/guides/specification/index) - This document specifies HTTY as a
|
|
33
|
+
- [HTTY Specification](https://socketry.github.io/protocol-htty/guides/specification/index) - This document specifies HTTY as a DCS-bootstrapped raw-mode takeover transport for carrying a plaintext HTTP/2 (`h2c`) connection over terminal-attached sessions.
|
|
34
34
|
|
|
35
35
|
## Releases
|
|
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
|
+
|
|
44
|
+
### v0.2.0
|
|
45
|
+
|
|
46
|
+
- Add `Protocol::HTTY::Stream.open(stream, **options)` as the preferred constructor for bootstrapping HTTY over an existing stream.
|
|
47
|
+
- Inline HTTY bootstrap encoding and decoding into `Protocol::HTTY::Stream`, keeping the public API focused on the single raw byte-stream abstraction used by HTTP/2.
|
|
48
|
+
|
|
39
49
|
### v0.1.0
|
|
40
50
|
|
|
51
|
+
- Initial release.
|
|
52
|
+
|
|
41
53
|
## Contributing
|
|
42
54
|
|
|
43
55
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
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
|
+
|
|
8
|
+
## v0.2.0
|
|
9
|
+
|
|
10
|
+
- Add `Protocol::HTTY::Stream.open(stream, **options)` as the preferred constructor for bootstrapping HTTY over an existing stream.
|
|
11
|
+
- Inline HTTY bootstrap encoding and decoding into `Protocol::HTTY::Stream`, keeping the public API focused on the single raw byte-stream abstraction used by HTTP/2.
|
|
12
|
+
|
|
3
13
|
## v0.1.0
|
|
14
|
+
|
|
15
|
+
- Initial release.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "stringio"
|
|
7
|
+
require "protocol/htty"
|
|
8
|
+
|
|
9
|
+
class OneByteInput
|
|
10
|
+
def initialize(data)
|
|
11
|
+
@data = data.b
|
|
12
|
+
@offset = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read(length = nil)
|
|
16
|
+
return nil if @offset >= @data.bytesize
|
|
17
|
+
|
|
18
|
+
length = [length || @data.bytesize, 1].min
|
|
19
|
+
chunk = @data.byteslice(@offset, length)
|
|
20
|
+
@offset += chunk.bytesize
|
|
21
|
+
|
|
22
|
+
return chunk
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe Protocol::HTTY::Stream do
|
|
27
|
+
let(:input) {StringIO.new}
|
|
28
|
+
let(:output) {StringIO.new}
|
|
29
|
+
let(:stream) {subject.new(input, output)}
|
|
30
|
+
|
|
31
|
+
it "writes the HTTY raw bootstrap" do
|
|
32
|
+
stream.write_bootstrap
|
|
33
|
+
|
|
34
|
+
expect(output.string).to be == "\eP+Hraw\e\\"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "reads the HTTY raw bootstrap" do
|
|
38
|
+
input.string = "hello\eP+Hraw\e\\world"
|
|
39
|
+
input.rewind
|
|
40
|
+
|
|
41
|
+
mode = stream.read_bootstrap
|
|
42
|
+
|
|
43
|
+
expect(mode).to be == "raw"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "reads a bootstrap split across one-byte reads" do
|
|
47
|
+
stream = subject.new(OneByteInput.new("hello\eP+Hraw\e\\"), StringIO.new)
|
|
48
|
+
|
|
49
|
+
expect(stream.read_bootstrap).to be == "raw"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "preserves bytes after the HTTY bootstrap" do
|
|
53
|
+
input.string = "\eP+Hraw\e\\world"
|
|
54
|
+
input.rewind
|
|
55
|
+
|
|
56
|
+
stream.read_bootstrap
|
|
57
|
+
|
|
58
|
+
expect(stream.read(5)).to be == "world"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "raises on unsupported bootstrap modes" do
|
|
62
|
+
input.string = "\eP+Hframed\e\\"
|
|
63
|
+
input.rewind
|
|
64
|
+
|
|
65
|
+
expect do
|
|
66
|
+
stream.read_bootstrap
|
|
67
|
+
end.to raise_exception(Protocol::HTTY::ProtocolError)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "raises on incomplete bootstraps" do
|
|
71
|
+
input.string = "\eP+Hraw"
|
|
72
|
+
input.rewind
|
|
73
|
+
|
|
74
|
+
expect do
|
|
75
|
+
stream.read_bootstrap
|
|
76
|
+
end.to raise_exception(EOFError)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "ignores unrelated DCS payloads before the bootstrap" do
|
|
80
|
+
input.string = "\ePfoo\e\\\eP+Hraw\e\\"
|
|
81
|
+
input.rewind
|
|
82
|
+
|
|
83
|
+
expect(stream.read_bootstrap).to be == "raw"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "ignores implementation-specific reset markers before the bootstrap" do
|
|
87
|
+
input.string = "\eP+reset:token\e\\\eP+Hraw\e\\"
|
|
88
|
+
input.rewind
|
|
89
|
+
|
|
90
|
+
expect(stream.read_bootstrap).to be == "raw"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "open3"
|
|
7
|
+
require "pty"
|
|
8
|
+
require "rbconfig"
|
|
9
|
+
require "timeout"
|
|
10
|
+
require "protocol/http2"
|
|
11
|
+
require "protocol/http2/client"
|
|
12
|
+
require "protocol/http2/stream"
|
|
13
|
+
require "protocol/htty"
|
|
14
|
+
require "protocol/htty/fixtures"
|
|
15
|
+
|
|
16
|
+
class PTYStream
|
|
17
|
+
def initialize(input, output)
|
|
18
|
+
@input = input
|
|
19
|
+
@output = output
|
|
20
|
+
@buffer = +"".b
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr :input
|
|
24
|
+
attr :output
|
|
25
|
+
|
|
26
|
+
def read(length)
|
|
27
|
+
while @buffer.bytesize < length
|
|
28
|
+
@buffer << @input.readpartial(4096).b
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
data = @buffer.byteslice(0, length)
|
|
32
|
+
@buffer = @buffer.byteslice(length, @buffer.bytesize) || +"".b
|
|
33
|
+
|
|
34
|
+
return data
|
|
35
|
+
rescue EOFError, Errno::EIO
|
|
36
|
+
raise EOFError, "PTY closed"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write(data)
|
|
40
|
+
@output.write(data)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def flush
|
|
44
|
+
@output.flush
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def close
|
|
48
|
+
@input.close rescue nil
|
|
49
|
+
@output.close rescue nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def closed?
|
|
53
|
+
@input.closed?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class ResponseStream < Protocol::HTTP2::Stream
|
|
58
|
+
attr :response_headers
|
|
59
|
+
attr :body
|
|
60
|
+
|
|
61
|
+
def initialize(...)
|
|
62
|
+
super
|
|
63
|
+
@response_headers = []
|
|
64
|
+
@body = +"".b
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def process_headers(frame)
|
|
68
|
+
@response_headers = super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def process_data(frame)
|
|
72
|
+
data = super
|
|
73
|
+
@body << data.b if data
|
|
74
|
+
return data
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Client < Protocol::HTTP2::Client
|
|
79
|
+
def create_stream(id = next_stream_id)
|
|
80
|
+
ResponseStream.create(self, id)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "HTTY over a real PTY" do
|
|
85
|
+
let(:root) {File.expand_path("../../..", __dir__)}
|
|
86
|
+
let(:ruby_load_path) {File.join(root, "lib")}
|
|
87
|
+
|
|
88
|
+
def spawn_fixture(name)
|
|
89
|
+
environment = {"RUBYLIB" => ruby_load_path}
|
|
90
|
+
|
|
91
|
+
PTY.spawn(environment, RbConfig.ruby, Protocol::HTTY::Fixtures.executable_path(name))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def with_fixture(name)
|
|
95
|
+
input, output, pid = spawn_fixture(name)
|
|
96
|
+
input.binmode
|
|
97
|
+
output.binmode
|
|
98
|
+
|
|
99
|
+
stream = PTYStream.new(input, output)
|
|
100
|
+
|
|
101
|
+
yield stream
|
|
102
|
+
ensure
|
|
103
|
+
stream&.close
|
|
104
|
+
Process.wait(pid) rescue nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "ignores terminal noise before the bootstrap" do
|
|
108
|
+
Timeout.timeout(5) do
|
|
109
|
+
with_fixture("bootstrap") do |stream|
|
|
110
|
+
framer = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
111
|
+
|
|
112
|
+
expect(framer.read_bootstrap).to be == "raw"
|
|
113
|
+
expect(stream.read(3)).to be == "RAW"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "delivers the HTTP/2 connection preface after raw takeover" do
|
|
119
|
+
Timeout.timeout(5) do
|
|
120
|
+
with_fixture("raw_preface") do |stream|
|
|
121
|
+
framer = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
122
|
+
|
|
123
|
+
expect(framer.read_bootstrap).to be == "raw"
|
|
124
|
+
|
|
125
|
+
stream.write(Protocol::HTTP2::CONNECTION_PREFACE)
|
|
126
|
+
stream.flush
|
|
127
|
+
|
|
128
|
+
expect(stream.read(10)).to be == "PREFACE_OK"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "runs an HTTP/2 session until command-side GOAWAY" do
|
|
134
|
+
Timeout.timeout(5) do
|
|
135
|
+
with_fixture("http2_server") do |stream|
|
|
136
|
+
stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
137
|
+
stream.read_bootstrap
|
|
138
|
+
|
|
139
|
+
framer = Protocol::HTTP2::Framer.new(stream)
|
|
140
|
+
client = Client.new(framer)
|
|
141
|
+
client.send_connection_preface
|
|
142
|
+
|
|
143
|
+
request = client.create_stream
|
|
144
|
+
request.send_headers(
|
|
145
|
+
[[":method", "GET"], [":path", "/"], [":scheme", "http"], [":authority", "htty.local"]],
|
|
146
|
+
Protocol::HTTP2::END_STREAM
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
goaway = false
|
|
150
|
+
|
|
151
|
+
until goaway
|
|
152
|
+
begin
|
|
153
|
+
frame = client.read_frame
|
|
154
|
+
goaway = frame.is_a?(Protocol::HTTP2::GoawayFrame)
|
|
155
|
+
rescue Protocol::HTTP2::GoawayError
|
|
156
|
+
goaway = true
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
expect(request.response_headers.to_h[":status"]).to be == "200"
|
|
161
|
+
expect(request.body).to be == "OK\n"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "treats command exit after bootstrap without GOAWAY as an abort" do
|
|
167
|
+
Timeout.timeout(5) do
|
|
168
|
+
with_fixture("abort_after_bootstrap") do |stream|
|
|
169
|
+
stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
|
|
170
|
+
expect(stream.read_bootstrap).to be == "raw"
|
|
171
|
+
|
|
172
|
+
framer = Protocol::HTTP2::Framer.new(stream)
|
|
173
|
+
|
|
174
|
+
expect do
|
|
175
|
+
framer.read_frame
|
|
176
|
+
end.to raise_exception(EOFError)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "recovers the surrounding shell after an aborted session" do
|
|
182
|
+
client = File.join(root, "examples/pty/client.rb")
|
|
183
|
+
|
|
184
|
+
output, error, status = Timeout.timeout(10) do
|
|
185
|
+
Open3.capture3(
|
|
186
|
+
{"HTTY_SHELL" => "bash", "HTTY_FAIL_ON" => "5"},
|
|
187
|
+
RbConfig.ruby,
|
|
188
|
+
client,
|
|
189
|
+
chdir: root
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
combined_output = "#{output}#{error}"
|
|
194
|
+
|
|
195
|
+
expect(status).to be(:success?)
|
|
196
|
+
expect(combined_output).to be(:include?, "[htty] session 5 failed")
|
|
197
|
+
expect(combined_output).to be(:include?, "[htty] bootstrap detected \u2013 session 6")
|
|
198
|
+
expect(combined_output).to be(:include?, "session 10 \u2013 status: 200")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -10,41 +10,49 @@ require "protocol/htty"
|
|
|
10
10
|
|
|
11
11
|
describe Protocol::HTTY::Stream do
|
|
12
12
|
let(:writer) {StringIO.new}
|
|
13
|
-
let(:stream) {subject.
|
|
13
|
+
let(:stream) {subject.open(StringIO.new, writer)}
|
|
14
14
|
|
|
15
|
-
it "
|
|
15
|
+
it "writes raw bytes after bootstrap" do
|
|
16
|
+
stream.write_bootstrap
|
|
16
17
|
stream.write(Protocol::HTTP2::CONNECTION_PREFACE)
|
|
18
|
+
stream.flush
|
|
17
19
|
|
|
18
|
-
expect(writer.string
|
|
20
|
+
expect(writer.string).to be == "\eP+Hraw\e\\#{Protocol::HTTP2::CONNECTION_PREFACE}"
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
it "
|
|
22
|
-
|
|
23
|
+
it "consumes the bootstrap before reading raw bytes" do
|
|
24
|
+
writer.write("\eP+Hraw\e\\#{Protocol::HTTP2::CONNECTION_PREFACE}")
|
|
23
25
|
writer.rewind
|
|
24
|
-
|
|
25
|
-
reader = subject.
|
|
26
|
+
|
|
27
|
+
reader = subject.open(writer, StringIO.new, bootstrap: :read)
|
|
26
28
|
|
|
27
29
|
expect(reader.read(Protocol::HTTP2::CONNECTION_PREFACE.bytesize)).to be == Protocol::HTTP2::CONNECTION_PREFACE
|
|
28
|
-
reader.
|
|
29
|
-
expect(reader.read).to be_nil
|
|
30
|
+
expect(reader.read).to be == ""
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
it "
|
|
33
|
-
|
|
33
|
+
it "writes the bootstrap when opened in write mode" do
|
|
34
|
+
writer = StringIO.new
|
|
35
|
+
stream = subject.open(StringIO.new, writer, bootstrap: :write)
|
|
36
|
+
|
|
37
|
+
expect(writer.string).to be == "\eP+Hraw\e\\"
|
|
34
38
|
stream.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "returns all bytes when length is omitted" do
|
|
42
|
+
writer.write("hello")
|
|
35
43
|
writer.rewind
|
|
36
44
|
|
|
37
|
-
reader = subject.
|
|
45
|
+
reader = subject.open(writer, StringIO.new)
|
|
38
46
|
|
|
39
47
|
expect(reader.read).to be == "hello"
|
|
40
|
-
expect(reader.read).to
|
|
48
|
+
expect(reader.read).to be == ""
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
it "exposes the underlying output stream" do
|
|
44
52
|
expect(stream.io).to be(:is_a?, ::IO::Stream::Buffered)
|
|
45
53
|
end
|
|
46
54
|
|
|
47
|
-
it "flushes through the underlying
|
|
55
|
+
it "flushes through the underlying stream" do
|
|
48
56
|
stream.write("hello")
|
|
49
57
|
|
|
50
58
|
expect do
|
|
@@ -65,11 +73,29 @@ describe Protocol::HTTY::Stream do
|
|
|
65
73
|
|
|
66
74
|
expect(writer).not.to be(:closed?)
|
|
67
75
|
end
|
|
76
|
+
|
|
77
|
+
it "close_write closes writes but preserves readability" do
|
|
78
|
+
stream.close_write
|
|
79
|
+
|
|
80
|
+
expect(stream).to be(:closed?)
|
|
81
|
+
expect(stream).to be(:readable?)
|
|
82
|
+
expect(writer).not.to be(:closed?)
|
|
83
|
+
end
|
|
68
84
|
|
|
69
85
|
it "reports when the remote side is still readable" do
|
|
70
86
|
expect(stream).to be(:readable?)
|
|
71
87
|
end
|
|
72
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
|
+
|
|
73
99
|
it "rejects writes after the local side is closed" do
|
|
74
100
|
stream.close
|
|
75
101
|
|
|
@@ -80,7 +106,7 @@ describe Protocol::HTTY::Stream do
|
|
|
80
106
|
|
|
81
107
|
it "wraps raw IO handles using IO::Stream" do
|
|
82
108
|
Tempfile.create("protocol-htty") do |file|
|
|
83
|
-
io_stream = subject.
|
|
109
|
+
io_stream = subject.open(file, file).io
|
|
84
110
|
|
|
85
111
|
expect(io_stream).to be(:is_a?, ::IO::Stream::Buffered)
|
|
86
112
|
io_stream.close
|
|
@@ -89,7 +115,7 @@ describe Protocol::HTTY::Stream do
|
|
|
89
115
|
|
|
90
116
|
it "does not close wrapped raw IO handles when closed" do
|
|
91
117
|
Tempfile.create("protocol-htty") do |file|
|
|
92
|
-
wrapped_stream = subject.
|
|
118
|
+
wrapped_stream = subject.open(file, file)
|
|
93
119
|
|
|
94
120
|
wrapped_stream.close
|
|
95
121
|
|
data.tar.gz.sig
CHANGED
|
Binary file
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -38,20 +38,6 @@ cert_chain:
|
|
|
38
38
|
-----END CERTIFICATE-----
|
|
39
39
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
40
40
|
dependencies:
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: base64
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - ">="
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0'
|
|
48
|
-
type: :runtime
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - ">="
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0'
|
|
55
41
|
- !ruby/object:Gem::Dependency
|
|
56
42
|
name: protocol-http2
|
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -72,28 +58,28 @@ dependencies:
|
|
|
72
58
|
requirements:
|
|
73
59
|
- - ">="
|
|
74
60
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
61
|
+
version: 0.13.0
|
|
76
62
|
type: :runtime
|
|
77
63
|
prerelease: false
|
|
78
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
65
|
requirements:
|
|
80
66
|
- - ">="
|
|
81
67
|
- !ruby/object:Gem::Version
|
|
82
|
-
version:
|
|
68
|
+
version: 0.13.0
|
|
83
69
|
executables: []
|
|
84
70
|
extensions: []
|
|
85
71
|
extra_rdoc_files: []
|
|
86
72
|
files:
|
|
87
73
|
- lib/protocol/htty.rb
|
|
88
74
|
- lib/protocol/htty/error.rb
|
|
89
|
-
- lib/protocol/htty/framer.rb
|
|
90
75
|
- lib/protocol/htty/stream.rb
|
|
91
76
|
- lib/protocol/htty/version.rb
|
|
92
77
|
- license.md
|
|
93
78
|
- readme.md
|
|
94
79
|
- releases.md
|
|
95
80
|
- test/protocol/htty.rb
|
|
96
|
-
- test/protocol/htty/
|
|
81
|
+
- test/protocol/htty/bootstrap.rb
|
|
82
|
+
- test/protocol/htty/pty.rb
|
|
97
83
|
- test/protocol/htty/stream.rb
|
|
98
84
|
homepage: https://github.com/socketry/protocol-htty
|
|
99
85
|
licenses:
|
metadata.gz.sig
CHANGED
|
Binary file
|
data/lib/protocol/htty/framer.rb
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2026, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require "base64"
|
|
7
|
-
require "protocol/http2/framer"
|
|
8
|
-
|
|
9
|
-
module Protocol
|
|
10
|
-
module HTTY
|
|
11
|
-
# Encode and decode HTTY chunks on top of byte-oriented IO objects.
|
|
12
|
-
class Framer
|
|
13
|
-
ESC = "\e"
|
|
14
|
-
DCS = "#{ESC}P"
|
|
15
|
-
ST = "#{ESC}\\"
|
|
16
|
-
PREFIX = "HTTY;1;"
|
|
17
|
-
|
|
18
|
-
# Create a framer around the given input and output streams.
|
|
19
|
-
# @parameter input [Interface(:read)] The stream to read framed packets from.
|
|
20
|
-
# @parameter output [Interface(:write, :flush) | Nil] The stream to write framed packets to.
|
|
21
|
-
def initialize(input, output = input)
|
|
22
|
-
@input = input
|
|
23
|
-
@output = output
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
attr :input
|
|
27
|
-
attr :output
|
|
28
|
-
|
|
29
|
-
# Write a single HTTY chunk to the output stream.
|
|
30
|
-
# @parameter payload [String | Array(Integer)] The opaque bytes to encode.
|
|
31
|
-
# @returns [void]
|
|
32
|
-
def write_chunk(payload)
|
|
33
|
-
encoded = Base64.strict_encode64(payload.to_s.b)
|
|
34
|
-
@output.write("#{DCS}#{PREFIX}#{encoded}#{ST}")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Read the next HTTY chunk from the input stream.
|
|
38
|
-
# Non-HTTY terminal output is ignored until a valid chunk prefix is found.
|
|
39
|
-
# @returns [String | Nil] The decoded payload, or `nil` on end of stream.
|
|
40
|
-
# @raises [ProtocolError] If the chunk prefix or chunk structure is invalid.
|
|
41
|
-
# @raises [ArgumentError] If the packet payload is not valid base64.
|
|
42
|
-
# @raises [EOFError] If the chunk terminator is missing.
|
|
43
|
-
def read_chunk
|
|
44
|
-
while payload = read_payload
|
|
45
|
-
if payload.start_with?("HTTY;") && !payload.start_with?(PREFIX)
|
|
46
|
-
raise ProtocolError, "Unsupported HTTY chunk version: #{payload.inspect}"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
next unless payload.start_with?(PREFIX)
|
|
50
|
-
encoded = payload.delete_prefix(PREFIX)
|
|
51
|
-
return Base64.strict_decode64(encoded)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
return nil
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Flush the output stream if it supports flushing.
|
|
58
|
-
# @returns [void]
|
|
59
|
-
def flush
|
|
60
|
-
@output.flush if @output.respond_to?(:flush)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Close the wrapped input and output streams.
|
|
64
|
-
# If input and output are the same object, it is only closed once.
|
|
65
|
-
# @returns [void]
|
|
66
|
-
def close
|
|
67
|
-
@output.close if @output.respond_to?(:close)
|
|
68
|
-
@input.close if !@input.equal?(@output) && @input.respond_to?(:close)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Check whether the output stream has been closed.
|
|
72
|
-
# @returns [bool] True if the output stream reports that it is closed.
|
|
73
|
-
def closed?
|
|
74
|
-
@output.respond_to?(:closed?) && @output.closed?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
def read_payload
|
|
80
|
-
while prefix = @input.read(1)
|
|
81
|
-
next unless prefix == ESC
|
|
82
|
-
|
|
83
|
-
marker = @input.read(1)
|
|
84
|
-
return nil unless marker
|
|
85
|
-
next unless marker == "P"
|
|
86
|
-
|
|
87
|
-
return consume_packet
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
return nil
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def consume_packet
|
|
94
|
-
buffer = +""
|
|
95
|
-
|
|
96
|
-
while chunk = @input.read(1)
|
|
97
|
-
if chunk == ESC
|
|
98
|
-
terminator = @input.read(1)
|
|
99
|
-
return buffer if terminator == "\\"
|
|
100
|
-
|
|
101
|
-
buffer << chunk
|
|
102
|
-
buffer << terminator if terminator
|
|
103
|
-
else
|
|
104
|
-
buffer << chunk
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
raise EOFError, "Incomplete HTTY chunk!"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2026, by Samuel Williams.
|
|
5
|
-
|
|
6
|
-
require "stringio"
|
|
7
|
-
require "protocol/http2/framer"
|
|
8
|
-
require "protocol/htty"
|
|
9
|
-
|
|
10
|
-
describe Protocol::HTTY::Framer do
|
|
11
|
-
let(:input) {StringIO.new}
|
|
12
|
-
let(:output) {StringIO.new}
|
|
13
|
-
let(:framer) {subject.new(input, output)}
|
|
14
|
-
|
|
15
|
-
it "writes terminal-safe chunks" do
|
|
16
|
-
framer.write_chunk(Protocol::HTTP2::CONNECTION_PREFACE)
|
|
17
|
-
|
|
18
|
-
expect(output.string).to be == "\ePHTTY;1;UFJJICogSFRUUC8yLjANCg0KU00NCg0K\e\\"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
it "reads terminal-safe chunks" do
|
|
22
|
-
input.string = "hello\ePHTTY;1;UFJJICogSFRUUC8yLjANCg0KU00NCg0K\e\\world"
|
|
23
|
-
input.rewind
|
|
24
|
-
|
|
25
|
-
chunk = framer.read_chunk
|
|
26
|
-
|
|
27
|
-
expect(chunk).to be == Protocol::HTTP2::CONNECTION_PREFACE
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
it "raises on malformed chunks" do
|
|
31
|
-
input.string = "\ePHTTY;1\e\\"
|
|
32
|
-
input.rewind
|
|
33
|
-
|
|
34
|
-
expect do
|
|
35
|
-
framer.read_chunk
|
|
36
|
-
end.to raise_exception(Protocol::HTTY::ProtocolError)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
it "raises on incomplete chunks" do
|
|
40
|
-
input.string = "\ePHTTY;1;QUJD"
|
|
41
|
-
input.rewind
|
|
42
|
-
|
|
43
|
-
expect do
|
|
44
|
-
framer.read_chunk
|
|
45
|
-
end.to raise_exception(EOFError)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it "closes distinct input and output streams" do
|
|
49
|
-
framer.close
|
|
50
|
-
|
|
51
|
-
expect(input).to be(:closed?)
|
|
52
|
-
expect(output).to be(:closed?)
|
|
53
|
-
expect(framer).to be(:closed?)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
it "handles escaped bytes inside an invalid chunk payload" do
|
|
57
|
-
input.string = "\ePHTTY;1;QUJD\eXRA==\e\\"
|
|
58
|
-
input.rewind
|
|
59
|
-
|
|
60
|
-
expect do
|
|
61
|
-
framer.read_chunk
|
|
62
|
-
end.to raise_exception(ArgumentError)
|
|
63
|
-
end
|
|
64
|
-
end
|