From e54fe82635eb311eb01bd6d9fb59f0b5164155a4 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Wed, 18 Sep 2024 17:46:26 +1200 Subject: [PATCH] More fixes/coverage. --- lib/protocol/http1/connection.rb | 30 +++--- test/protocol/http1/connection.rb | 147 +++++++++++++++++++++++++----- 2 files changed, 139 insertions(+), 38 deletions(-) diff --git a/lib/protocol/http1/connection.rb b/lib/protocol/http1/connection.rb index dfea5bd..7213ba0 100644 --- a/lib/protocol/http1/connection.rb +++ b/lib/protocol/http1/connection.rb @@ -288,7 +288,7 @@ def read_request body = read_request_body(method, headers) - if body.nil? + unless body self.receive_end_stream! end @@ -306,7 +306,9 @@ def read_response_line end def read_response(method) - raise ProtocolError, "Cannot read response in #{@state}!" unless @state == :open + unless @state == :open or @state == :half_closed_local + raise ProtocolError, "Cannot read response in #{@state}!" + end version, status, reason = read_response_line @@ -316,6 +318,10 @@ def read_response(method) body = read_response_body(method, status, headers) + unless body + self.receive_end_stream! + end + @count += 1 return version, status, reason, headers, body @@ -474,24 +480,14 @@ def write_body_and_close(body, head) @stream.close_write end - def half_closed_local! - raise ProtocolError, "Cannot close local in #{@state}!" unless @state == :open - - @state = :half_closed_local - end - - def half_closed_remote! - raise ProtocolError, "Cannot close remote in #{@state}!" unless @state == :open - - @state = :half_closed_remote - end - def idle! @state = :idle end def closed! - raise ProtocolError, "Cannot close in #{@state}!" unless @state == :half_closed_local or @state == :half_closed_remote + unless @state == :half_closed_local or @state == :half_closed_remote + raise ProtocolError, "Cannot close in #{@state}!" + end if @persistent self.idle! @@ -502,7 +498,7 @@ def closed! def send_end_stream! if @state == :open - self.half_closed_local! + @state = :half_closed_local elsif @state == :half_closed_remote self.closed! else @@ -545,7 +541,7 @@ def write_body(version, body, head = false, trailer = nil) def receive_end_stream! if @state == :open - self.half_closed_remote! + @state = :half_closed_remote elsif @state == :half_closed_local self.closed! else diff --git a/test/protocol/http1/connection.rb b/test/protocol/http1/connection.rb index f4a45df..810795a 100644 --- a/test/protocol/http1/connection.rb +++ b/test/protocol/http1/connection.rb @@ -8,6 +8,7 @@ require "protocol/http1/connection" require "protocol/http/body/buffered" +require "protocol/http/body/writable" require "connection_context" @@ -548,26 +549,130 @@ end end - with "persistent connection" do - it "returns back to idle state" do - expect(client).to be(:idle?) - - client.write_request("localhost", "GET", "/", "HTTP/1.1", {}) - expect(client).to be(:open?) - client.write_body("HTTP/1.1", nil) - - expect(client).to be(:half_closed_local?) - - expect(server).to be(:idle?) - - request = server.read_request - expect(request).to be == ["localhost", "GET", "/", "HTTP/1.1", {}, nil] - expect(server).to be(:half_closed_remote?) - - server.write_response("HTTP/1.1", 200, {}, []) - server.write_body("HTTP/1.1", nil) - - expect(server).to be(:idle?) - end + it "enters half-closed (local) state after writing response body" do + expect(client).to be(:idle?) + client.write_request("localhost", "GET", "/", "HTTP/1.1", {}) + expect(client).to be(:open?) + body = Protocol::HTTP::Body::Buffered.new(["Hello World"]) + client.write_body("HTTP/1.1", body) + expect(client).to be(:half_closed_local?) + + expect(server).to be(:idle?) + request = server.read_request + server.write_response("HTTP/1.1", 200, {}, nil) + server.write_body("HTTP/1.1", nil) + expect(server).to be(:half_closed_local?) + end + + it "returns back to idle state" do + expect(client).to be(:idle?) + client.write_request("localhost", "GET", "/", "HTTP/1.1", {}) + expect(client).to be(:open?) + client.write_body("HTTP/1.1", nil) + expect(client).to be(:half_closed_local?) + + expect(server).to be(:idle?) + request = server.read_request + expect(request).to be == ["localhost", "GET", "/", "HTTP/1.1", {}, nil] + expect(server).to be(:half_closed_remote?) + + server.write_response("HTTP/1.1", 200, {}, []) + server.write_body("HTTP/1.1", nil) + expect(server).to be(:idle?) + + response = client.read_response("GET") + expect(client).to be(:idle?) + end + + it "transitions to the closed state when using connection: close response body" do + expect(client).to be(:idle?) + client.write_request("localhost", "GET", "/", "HTTP/1.0", {}) + expect(client).to be(:open?) + + client.write_body("HTTP/1.0", nil) + expect(client).to be(:half_closed_local?) + + expect(server).to be(:idle?) + request = server.read_request + expect(server).to be(:half_closed_remote?) + + server.write_response("HTTP/1.0", 200, {}, []) + + # Length is unknown, and HTTP/1.0 does not support chunked encoding, so this will close the connection: + body = Protocol::HTTP::Body::Writable.new + body.write "Hello World" + body.close_write + + server.write_body("HTTP/1.0", body) + expect(server).not.to be(:persistent) + expect(server).to be(:closed?) + + response = client.read_response("GET") + body = response.last + expect(body.join).to be == "Hello World" + expect(client).to be(:closed?) + end + + it "can't write a request in the closed state" do + client.state = :closed + + expect do + client.write_request("localhost", "GET", "/", "HTTP/1.0", {}) + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't read a response in the closed state" do + client.state = :closed + + expect do + client.read_response("GET") + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't write a response in the closed state" do + server.state = :closed + + expect do + server.write_response("HTTP/1.0", 200, {}, nil) + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't read a request in the closed state" do + server.state = :closed + + expect do + server.read_request + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't enter the closed state from the idle state" do + expect do + client.closed! + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't write response body without writing response" do + expect do + server.write_body("HTTP/1.0", nil) + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't write request body without writing request" do + expect do + client.write_body("HTTP/1.0", nil) + end.to raise_exception(Protocol::HTTP1::ProtocolError) + end + + it "can't read request body without reading request" do + # Fake empty chunked encoded body: + client.stream.write("0\r\n\r\n") + + body = server.read_request_body("POST", {"transfer-encoding" => ["chunked"]}) + + expect(body).to be_a(Protocol::HTTP1::Body::Chunked) + + expect do + body.join + end.to raise_exception(Protocol::HTTP1::ProtocolError) end end