Skip to content

State machine tests. #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions lib/protocol/http1/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ def initialize(stream, persistent: true, state: :idle)
# State transition methods use a trailing "!".
attr_accessor :state

def idle?
@state == :idle
end

def open?
@state == :open
end

def half_closed_local?
@state == :half_closed_local
end

def half_closed_remote?
@state == :half_closed_remote
end

def closed?
@state == :closed
end

# The number of requests processed.
attr :count

Expand Down Expand Up @@ -185,7 +205,9 @@ def write_request(authority, method, path, version, headers)
end

def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
raise ProtocolError, "Cannot write response in #{@state}!" unless @state == :open
unless @state == :open or @state == :half_closed_remote
raise ProtocolError, "Cannot write response in #{@state}!"
end

# Safari WebSockets break if no reason is given:
@stream.write("#{version} #{status} #{reason}\r\n")
Expand All @@ -194,7 +216,9 @@ def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[statu
end

def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
raise ProtocolError, "Cannot write interim response!" unless @state == :open
unless @state == :open or @state == :half_closed_remote
raise ProtocolError, "Cannot write interim response in #{@state}!"
end

@stream.write("#{version} #{status} #{reason}\r\n")

Expand Down Expand Up @@ -268,6 +292,10 @@ def read_request

body = read_request_body(method, headers)

unless body
self.receive_end_stream!
end

@count += 1

return headers.delete(HOST), method, path, version, headers, body
Expand All @@ -281,18 +309,30 @@ def read_response_line
return version, status, reason
end

private def interim_status?(status)
status != 101 and status >= 100 and status < 200
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

headers = read_headers

@persistent = persistent?(version, method, headers)

body = read_response_body(method, status, headers)

@count += 1
unless interim_status?(status)
body = read_response_body(method, status, headers)

unless body
self.receive_end_stream!
end

@count += 1
end

return version, status, reason, headers, body
end
Expand Down Expand Up @@ -450,26 +490,16 @@ 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 self.persistent?
if @persistent
self.idle!
else
@state = :closed
Expand All @@ -478,7 +508,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
Expand Down Expand Up @@ -521,7 +551,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
Expand Down
136 changes: 136 additions & 0 deletions test/protocol/http1/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

require "protocol/http1/connection"
require "protocol/http/body/buffered"
require "protocol/http/body/writable"

require "connection_context"

Expand Down Expand Up @@ -547,4 +548,139 @@
end.to raise_exception(Protocol::HTTP1::BadHeader)
end
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

it "can't write interim response in the closed state" do
server.state = :closed

expect do
server.write_interim_response("HTTP/1.0", 100, {})
end.to raise_exception(Protocol::HTTP1::ProtocolError)
end
end
Loading