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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba295c47e02345ba7101c3958786fc21fd7187294abe6fef0f39a6677baf9be3
4
- data.tar.gz: 2fcb179db305375b86b5196ed149c0ef1eed4fb52c7d06447b0a5e68d4dd16a4
3
+ metadata.gz: 87a3aa4c350c134deb40265ae418e15fbd1158b70a8cf4c21e1748de514ca867
4
+ data.tar.gz: 506e9101a8df67e75aed817e3227919e06dc00ee5aa8d58d62c6d7139ae8d1ea
5
5
  SHA512:
6
- metadata.gz: 4132b7c507a0b285f772c91bca4ddbcb315d5faccfeafd28c6907df4553ad5a20190299e9d924c75a946819e656205b603bcae69b6bc61d1e20b08f5e437f60f
7
- data.tar.gz: 3149badd26c592217168140c69b654190fdad74d6884b9ef4e729ff9e8a1f4b342f284bec1b1759698d53a15c4c11858e666654d6b7de0cc47e07f44854d45bd
6
+ metadata.gz: 82d2c3b0fbbd56482711a10da4dad9e0e86177168d1cbc198d126e1d0c347e27dd3123e5d3982f9884e7bc2ee8b8f1be429164f06f16d5c3dde65ff5424750e3
7
+ data.tar.gz: cb61305b1df49d733cc5b4e0755d2d8962813eafc1e3fb2137122014643b7b7a90bfd44e2490edd428eb32b8bb9a7e31745bfeaa8b8b847ea1ad39d5c2f8aa25
checksums.yaml.gz.sig CHANGED
Binary file
@@ -15,8 +15,10 @@ module Protocol
15
15
  BOOTSTRAP_PREFIX = "+H"
16
16
  RAW_MODE = "raw"
17
17
 
18
- def self.open(stream, bootstrap: nil, mode: RAW_MODE)
19
- stream = self.new(::IO::Stream(stream))
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 a raw byte-preserving transport.
36
- # @parameter stream [IO::Stream] The duplex byte stream used after bootstrap.
37
- def initialize(stream)
38
- @stream = stream
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 :stream
47
+ attr :input
48
+ attr :output
43
49
 
44
- # Return the underlying duplex stream.
50
+ # Return the underlying output stream.
45
51
  def io
46
- @stream
52
+ @output
47
53
  end
48
54
 
49
55
  def write_bootstrap(mode = RAW_MODE)
50
- @stream.write("#{DCS}#{BOOTSTRAP_PREFIX}#{mode}#{ST}")
51
- @stream.flush
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
- return +"".b if length == 0
72
- return @stream.read(length)
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
- @stream.write(data.to_s.b)
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
- @stream.flush
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
- @stream.flush
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
- !@stream.closed?
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 = @stream.read(1)
175
+ while prefix = read_some(1)
120
176
  next unless prefix == ESC
121
177
 
122
- marker = @stream.read(1)
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 = @stream.read(1)
191
+ while chunk = read_some(1)
136
192
  if chunk == ESC
137
- terminator = @stream.read(1)
193
+ terminator = read_some(1)
138
194
  return buffer if terminator == "\\"
139
195
 
140
196
  buffer << chunk
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTY
8
- VERSION = "0.2.0"
8
+ VERSION = "0.3.0"
9
9
  end
10
10
  end
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(:io) {IO::Stream::Duplex(input, output)}
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(io.read(5)).to be == "world"
58
+ expect(stream.read(5)).to be == "world"
60
59
  end
61
60
 
62
61
  it "raises on unsupported bootstrap modes" do
@@ -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).read_bootstrap
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
- expect(Protocol::HTTY::Stream.new(stream).read_bootstrap).to be == "raw"
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(IO::Stream::Duplex(StringIO.new, writer))}
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(IO::Stream::Duplex(writer, StringIO.new), bootstrap: :read)
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(IO::Stream::Duplex(StringIO.new, writer), bootstrap: :write)
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(IO::Stream::Duplex(writer, StringIO.new))
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
@@ -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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file