protocol-htty 0.1.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
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/htty/error.rb +18 -0
- data/lib/protocol/htty/framer.rb +112 -0
- data/lib/protocol/htty/stream.rb +124 -0
- data/lib/protocol/htty/version.rb +10 -0
- data/lib/protocol/htty.rb +9 -0
- data/license.md +21 -0
- data/readme.md +73 -0
- data/releases.md +3 -0
- data/test/protocol/htty/framer.rb +64 -0
- data/test/protocol/htty/stream.rb +100 -0
- data/test/protocol/htty.rb +12 -0
- data.tar.gz.sig +3 -0
- metadata +122 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c222bcf565b63477735114b057e88c45508674629d78646c0fa73ab34b1abbd9
|
|
4
|
+
data.tar.gz: fd7405a11f04ee961c218473951ef8f0051dde6f0c581c2ecc129a87f9fbfe37
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0cf629cde9cb0d76603be89e9290335087a540c92be10e0f433732e6f323306ec8ddd7ba5cbe104ea0b7ef9ac57983bd3b0017005742c4065af1ecceebeca34d
|
|
7
|
+
data.tar.gz: db74e7230fdd22dbc18f1f4e85526856d3ebdd9eb550c1f7c3fdd6c3e84fcfc45d9ccb97559f813c9985d39d693e0ba2ef21d28a337086f94f76367c8de6dbfc
|
checksums.yaml.gz.sig
ADDED
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
# @namespace
|
|
7
|
+
module Protocol
|
|
8
|
+
# @namespace
|
|
9
|
+
module HTTY
|
|
10
|
+
# The base class for HTTY transport errors.
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Raised when an HTTY control packet is malformed or unsupported.
|
|
15
|
+
class ProtocolError < Error
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "io/stream"
|
|
7
|
+
|
|
8
|
+
module Protocol
|
|
9
|
+
module HTTY
|
|
10
|
+
# Transport an opaque byte stream over HTTY chunks.
|
|
11
|
+
class Stream
|
|
12
|
+
# Since base64 encoding adds 33% overhead, we can fit 3KB of binary data into a single HTTY chunk without exceeding the typical MTU of 4KB:
|
|
13
|
+
PACKET_SIZE = 1024*3
|
|
14
|
+
|
|
15
|
+
# Create a stream on top of HTTY framed input and output.
|
|
16
|
+
# @parameter input [IO | IO::Stream] The source of framed HTTY chunks.
|
|
17
|
+
# @parameter output [IO | IO::Stream | Nil] The sink for framed HTTY chunks.
|
|
18
|
+
# @parameter packet_size [Integer] The maximum payload size for each chunk.
|
|
19
|
+
def initialize(input, output = input, packet_size: PACKET_SIZE)
|
|
20
|
+
@framer = Framer.new(::IO::Stream(input), ::IO::Stream(output))
|
|
21
|
+
@packet_size = packet_size
|
|
22
|
+
@buffer = +"".b
|
|
23
|
+
@local_closed = false
|
|
24
|
+
@remote_closed = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr :framer
|
|
28
|
+
|
|
29
|
+
# Return the writable IO object used by the underlying framer.
|
|
30
|
+
# @returns [IO | IO::Stream] The output side of the framed transport.
|
|
31
|
+
def io
|
|
32
|
+
@framer.output
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 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
|
+
def read(length = nil)
|
|
39
|
+
return +"".b if length == 0
|
|
40
|
+
|
|
41
|
+
fill(length)
|
|
42
|
+
|
|
43
|
+
return nil if @buffer.empty? && @remote_closed
|
|
44
|
+
return nil if @buffer.empty?
|
|
45
|
+
return nil if length && @buffer.bytesize < length && !@remote_closed
|
|
46
|
+
|
|
47
|
+
if length
|
|
48
|
+
return @buffer.slice!(0, length)
|
|
49
|
+
else
|
|
50
|
+
return @buffer.slice!(0, @buffer.bytesize)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Write application bytes as one or more HTTY chunks.
|
|
55
|
+
# @parameter data [String | Array(Integer)] The opaque bytes to send.
|
|
56
|
+
# @returns [self]
|
|
57
|
+
# @raises [IOError] If the local side of the transport is closed.
|
|
58
|
+
def write(data)
|
|
59
|
+
raise IOError, "HTTY stream is closed for writing!" if @local_closed
|
|
60
|
+
|
|
61
|
+
data = data.to_s.b
|
|
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
|
|
70
|
+
|
|
71
|
+
return self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Flush any buffered output through the underlying framer.
|
|
75
|
+
# @returns [void]
|
|
76
|
+
def flush
|
|
77
|
+
@framer.flush
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Close the local write side of this stream abstraction.
|
|
81
|
+
# HTTY does not define a close packet, and closing this object does not close the underlying terminal IO.
|
|
82
|
+
# @returns [void]
|
|
83
|
+
def close
|
|
84
|
+
unless @local_closed
|
|
85
|
+
@local_closed = true
|
|
86
|
+
@framer.flush
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check whether the local side of the transport is closed.
|
|
91
|
+
# @returns [bool] True if local writes have been closed.
|
|
92
|
+
def closed?
|
|
93
|
+
@local_closed
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check whether the remote side may still provide more data.
|
|
97
|
+
# @returns [bool] True if the remote side has not sent or implied a close.
|
|
98
|
+
def readable?
|
|
99
|
+
!@remote_closed
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def fill(length)
|
|
105
|
+
while needs_more_data?(length)
|
|
106
|
+
chunk = @framer.read_chunk
|
|
107
|
+
|
|
108
|
+
unless chunk
|
|
109
|
+
@remote_closed = true
|
|
110
|
+
break
|
|
111
|
+
end
|
|
112
|
+
@buffer << chunk
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def needs_more_data?(length)
|
|
117
|
+
return false if @remote_closed
|
|
118
|
+
return @buffer.empty? unless length
|
|
119
|
+
|
|
120
|
+
@buffer.bytesize < length
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2026, by Samuel Williams.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Protocol::HTTY
|
|
2
|
+
|
|
3
|
+
`protocol-htty` defines a small, terminal-safe framing layer for carrying an opaque byte stream over TTY side channels.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/socketry/protocol-htty/actions?workflow=Test)
|
|
6
|
+
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
Traditional terminal user interfaces are useful, but they are also a poor fit for many modern interactions. They are constrained by character-cell rendering, limited layout semantics, awkward input models, and a presentation layer that was never designed for rich documents or structured application state.
|
|
10
|
+
|
|
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
|
+
|
|
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 framing layer that can carry a normal plaintext HTTP/2 connection alongside terminal traffic, enabling applications to attach browser surfaces to a normal terminal session over HTTY.
|
|
14
|
+
|
|
15
|
+
## Design
|
|
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-safe chunks without introducing a second session protocol.
|
|
18
|
+
|
|
19
|
+
Each chunk is encoded as a DCS sequence:
|
|
20
|
+
|
|
21
|
+
``` text
|
|
22
|
+
ESC P HTTY;1;BASE64_CHUNK ESC \
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The framing layer intentionally stays small so it can be reimplemented in other runtimes.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Please see the [project documentation](https://socketry.github.io/protocol-htty/) for more details.
|
|
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 terminal-safe HTTP/2 byte stream transport.
|
|
32
|
+
|
|
33
|
+
- [HTTY Specification](https://socketry.github.io/protocol-htty/guides/specification/index) - This document specifies HTTY as a terminal-safe framing layer for carrying a plaintext HTTP/2 (`h2c`) byte stream over terminal side channels.
|
|
34
|
+
|
|
35
|
+
## Releases
|
|
36
|
+
|
|
37
|
+
Please see the [project releases](https://socketry.github.io/protocol-htty/releases/index) for all releases.
|
|
38
|
+
|
|
39
|
+
### v0.1.0
|
|
40
|
+
|
|
41
|
+
## Contributing
|
|
42
|
+
|
|
43
|
+
We welcome contributions to this project.
|
|
44
|
+
|
|
45
|
+
1. Fork it.
|
|
46
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
47
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
48
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
49
|
+
5. Create new Pull Request.
|
|
50
|
+
|
|
51
|
+
### Running Tests
|
|
52
|
+
|
|
53
|
+
To run the test suite:
|
|
54
|
+
|
|
55
|
+
``` shell
|
|
56
|
+
bundle exec sus
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Making Releases
|
|
60
|
+
|
|
61
|
+
To make a new release:
|
|
62
|
+
|
|
63
|
+
``` shell
|
|
64
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Developer Certificate of Origin
|
|
68
|
+
|
|
69
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
|
70
|
+
|
|
71
|
+
### Community Guidelines
|
|
72
|
+
|
|
73
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
data/releases.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "stringio"
|
|
7
|
+
require "tempfile"
|
|
8
|
+
require "protocol/http2/framer"
|
|
9
|
+
require "protocol/htty"
|
|
10
|
+
|
|
11
|
+
describe Protocol::HTTY::Stream do
|
|
12
|
+
let(:writer) {StringIO.new}
|
|
13
|
+
let(:stream) {subject.new(StringIO.new, writer, packet_size: 8)}
|
|
14
|
+
|
|
15
|
+
it "chunks opaque payload into HTTY chunks" do
|
|
16
|
+
stream.write(Protocol::HTTP2::CONNECTION_PREFACE)
|
|
17
|
+
|
|
18
|
+
expect(writer.string.scan(/\ePHTTY;1;/).size).to be > 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "reads back opaque bytes from HTTY chunks" do
|
|
22
|
+
stream.write(Protocol::HTTP2::CONNECTION_PREFACE)
|
|
23
|
+
writer.rewind
|
|
24
|
+
|
|
25
|
+
reader = subject.new(writer, StringIO.new)
|
|
26
|
+
|
|
27
|
+
expect(reader.read(Protocol::HTTP2::CONNECTION_PREFACE.bytesize)).to be == Protocol::HTTP2::CONNECTION_PREFACE
|
|
28
|
+
reader.close
|
|
29
|
+
expect(reader.read).to be_nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "returns all buffered bytes when length is omitted" do
|
|
33
|
+
stream.write("hello")
|
|
34
|
+
stream.close
|
|
35
|
+
writer.rewind
|
|
36
|
+
|
|
37
|
+
reader = subject.new(writer, StringIO.new)
|
|
38
|
+
|
|
39
|
+
expect(reader.read).to be == "hello"
|
|
40
|
+
expect(reader.read).to be_nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "exposes the underlying output stream" do
|
|
44
|
+
expect(stream.io).to be(:is_a?, ::IO::Stream::Buffered)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "flushes through the underlying framer" do
|
|
48
|
+
stream.write("hello")
|
|
49
|
+
|
|
50
|
+
expect do
|
|
51
|
+
stream.flush
|
|
52
|
+
end.not.to raise_exception
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "reports when the local side is closed" do
|
|
56
|
+
expect(stream).not.to be(:closed?)
|
|
57
|
+
|
|
58
|
+
stream.close
|
|
59
|
+
|
|
60
|
+
expect(stream).to be(:closed?)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "does not close the underlying output stream" do
|
|
64
|
+
stream.close
|
|
65
|
+
|
|
66
|
+
expect(writer).not.to be(:closed?)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "reports when the remote side is still readable" do
|
|
70
|
+
expect(stream).to be(:readable?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "rejects writes after the local side is closed" do
|
|
74
|
+
stream.close
|
|
75
|
+
|
|
76
|
+
expect do
|
|
77
|
+
stream.write("hello")
|
|
78
|
+
end.to raise_exception(IOError)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "wraps raw IO handles using IO::Stream" do
|
|
82
|
+
Tempfile.create("protocol-htty") do |file|
|
|
83
|
+
io_stream = subject.new(file, file).io
|
|
84
|
+
|
|
85
|
+
expect(io_stream).to be(:is_a?, ::IO::Stream::Buffered)
|
|
86
|
+
io_stream.close
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "does not close wrapped raw IO handles when closed" do
|
|
91
|
+
Tempfile.create("protocol-htty") do |file|
|
|
92
|
+
wrapped_stream = subject.new(file, file)
|
|
93
|
+
|
|
94
|
+
wrapped_stream.close
|
|
95
|
+
|
|
96
|
+
expect(file).not.to be(:closed?)
|
|
97
|
+
file.close
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "protocol/htty/version"
|
|
7
|
+
|
|
8
|
+
describe Protocol::HTTY do
|
|
9
|
+
it "has a version number" do
|
|
10
|
+
expect(Protocol::HTTY::VERSION).to be =~ /\d+\.\d+\.\d+/
|
|
11
|
+
end
|
|
12
|
+
end
|
data.tar.gz.sig
ADDED
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: protocol-htty
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Samuel Williams
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain:
|
|
10
|
+
- |
|
|
11
|
+
-----BEGIN CERTIFICATE-----
|
|
12
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
|
13
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
|
14
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
|
15
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
|
16
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
|
17
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
|
18
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
|
19
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
|
20
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
|
21
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
|
22
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
|
23
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
|
24
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
|
25
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
|
26
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
|
27
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
|
28
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
|
30
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
|
31
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
|
32
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
|
33
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
|
34
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
|
35
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
|
36
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
38
|
+
-----END CERTIFICATE-----
|
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: protocol-http2
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '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'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: io-stream
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
executables: []
|
|
84
|
+
extensions: []
|
|
85
|
+
extra_rdoc_files: []
|
|
86
|
+
files:
|
|
87
|
+
- lib/protocol/htty.rb
|
|
88
|
+
- lib/protocol/htty/error.rb
|
|
89
|
+
- lib/protocol/htty/framer.rb
|
|
90
|
+
- lib/protocol/htty/stream.rb
|
|
91
|
+
- lib/protocol/htty/version.rb
|
|
92
|
+
- license.md
|
|
93
|
+
- readme.md
|
|
94
|
+
- releases.md
|
|
95
|
+
- test/protocol/htty.rb
|
|
96
|
+
- test/protocol/htty/framer.rb
|
|
97
|
+
- test/protocol/htty/stream.rb
|
|
98
|
+
homepage: https://github.com/socketry/protocol-htty
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
documentation_uri: https://socketry.github.io/protocol-htty/
|
|
103
|
+
source_code_uri: https://github.com/socketry/protocol-htty.git
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '3.3'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 4.0.6
|
|
119
|
+
specification_version: 4
|
|
120
|
+
summary: A terminal-safe transport for carrying opaque HTTP/2 bytes over TTY side
|
|
121
|
+
channels.
|
|
122
|
+
test_files: []
|
metadata.gz.sig
ADDED
|
Binary file
|