From c90f8903980bc257446858a84fea4c984ccf9077 Mon Sep 17 00:00:00 2001 From: Juan Wajnerman Date: Wed, 24 Jun 2020 01:20:45 -0300 Subject: [PATCH 1/5] Allow HTTP socket to be any IO --- src/http/client.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/http/client.cr b/src/http/client.cr index 4bcf8f4e0526..a1c687875e99 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -105,17 +105,16 @@ class HTTP::Client # ``` {% if flag?(:without_openssl) %} getter! tls : Nil - @socket : TCPSocket | Nil alias TLSContext = Bool | Nil {% else %} getter! tls : OpenSSL::SSL::Context::Client - @socket : TCPSocket | OpenSSL::SSL::Socket | Nil alias TLSContext = OpenSSL::SSL::Context::Client | Bool | Nil {% end %} # Whether automatic compression/decompression is enabled. property? compress : Bool = true + @socket : IO? @dns_timeout : Float64? @connect_timeout : Float64? @read_timeout : Float64? @@ -148,6 +147,9 @@ class HTTP::Client @port = (port || (@tls ? 443 : 80)).to_i end + def initialize(@socket = IO, @host = "localhost", @port = 80) + end + private def check_host_only(string : String) # When parsing a URI with just a host # we end up with a URI with just a path From a4e46331921f6a44f2ad68dda2d7cf981535d3c8 Mon Sep 17 00:00:00 2001 From: Juan Wajnerman Date: Wed, 24 Jun 2020 11:12:03 -0300 Subject: [PATCH 2/5] Update src/http/client.cr Co-authored-by: Sijawusz Pur Rahnama --- src/http/client.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/client.cr b/src/http/client.cr index a1c687875e99..e002213a764c 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -147,7 +147,7 @@ class HTTP::Client @port = (port || (@tls ? 443 : 80)).to_i end - def initialize(@socket = IO, @host = "localhost", @port = 80) + def initialize(@socket : IO, @host = "localhost", @port = 80) end private def check_host_only(string : String) From bc04fb7a2951754b6d903bca1147b205f5a17b54 Mon Sep 17 00:00:00 2001 From: Juan Wajnerman Date: Wed, 24 Jun 2020 12:21:50 -0300 Subject: [PATCH 3/5] Disallow HTTP::Client reconnection when initialized with IO --- spec/std/http/client/client_spec.cr | 31 +++++++++++++++++++++++++++++ src/http/client.cr | 11 ++++++++++ 2 files changed, 42 insertions(+) diff --git a/spec/std/http/client/client_spec.cr b/spec/std/http/client/client_spec.cr index 1a80c07941cc..49e3268634e8 100644 --- a/spec/std/http/client/client_spec.cr +++ b/spec/std/http/client/client_spec.cr @@ -265,5 +265,36 @@ module HTTP request.host.should eq "other.example.com" end end + + it "works with IO" do + io_response = IO::Memory.new <<-RESPONSE.gsub('\n', "\r\n") + HTTP/1.1 200 OK + Content-Type: text/plain + Content-Length: 3 + + Hi! + RESPONSE + io_request = IO::Memory.new + io = IO::Stapled.new(io_response, io_request) + client = Client.new(io) + response = client.get("/") + response.body.should eq("Hi!") + end + + it "can specify host and port when initialized with IO" do + client = Client.new(IO::Memory.new, "host", 1234) + client.host.should eq("host") + client.port.should eq(1234) + end + + it "cannot reconnect when initialized with IO" do + io = IO::Memory.new + client = Client.new(io) + client.close + io.closed?.should be_true + expect_raises(Exception, "This HTTP::Client cannot be reconnected") do + client.get("/") + end + end end end diff --git a/src/http/client.cr b/src/http/client.cr index e002213a764c..ee1c5efa07a1 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -119,6 +119,7 @@ class HTTP::Client @connect_timeout : Float64? @read_timeout : Float64? @write_timeout : Float64? + @reconnect = true # Creates a new HTTP client with the given *host*, *port* and *tls* # configurations. If no port is given, the default one will @@ -147,7 +148,14 @@ class HTTP::Client @port = (port || (@tls ? 443 : 80)).to_i end + # Creates a new HTTP client bound to an existing `IO`. + # *host* and *port* can be specified and they will be used + # to conform the `Host` header on each request. + # Instances created with this constructor cannot be reconnected. Once + # `close` is called explicitly or if the connection doesn't support keep-alive, + # the next call to make a request will raise an exception. def initialize(@socket : IO, @host = "localhost", @port = 80) + @reconnect = false end private def check_host_only(string : String) @@ -759,6 +767,9 @@ class HTTP::Client private def socket socket = @socket return socket if socket + unless @reconnect + raise "This HTTP::Client cannot be reconnected" + end hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host socket = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout From 00ffe3aa6b53019f7c02b187d6a731b8715edfeb Mon Sep 17 00:00:00 2001 From: Juan Wajnerman Date: Wed, 24 Jun 2020 18:49:21 -0300 Subject: [PATCH 4/5] HTTP::Client send empty `Host` header by default when IO is used --- spec/std/http/client/client_spec.cr | 4 ++++ src/http/client.cr | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/std/http/client/client_spec.cr b/spec/std/http/client/client_spec.cr index 49e3268634e8..e7dc6443b0b7 100644 --- a/spec/std/http/client/client_spec.cr +++ b/spec/std/http/client/client_spec.cr @@ -279,6 +279,10 @@ module HTTP client = Client.new(io) response = client.get("/") response.body.should eq("Hi!") + + io_request.rewind + request = HTTP::Request.from_io(io_request).as(HTTP::Request) + request.host.should eq("") end it "can specify host and port when initialized with IO" do diff --git a/src/http/client.cr b/src/http/client.cr index ee1c5efa07a1..14dc1bd8d6d1 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -154,7 +154,7 @@ class HTTP::Client # Instances created with this constructor cannot be reconnected. Once # `close` is called explicitly or if the connection doesn't support keep-alive, # the next call to make a request will raise an exception. - def initialize(@socket : IO, @host = "localhost", @port = 80) + def initialize(@socket : IO, @host = "", @port = 80) @reconnect = false end From 861e550db60aa9f079f68e5a82ea3c1b17e38b0a Mon Sep 17 00:00:00 2001 From: Juan Wajnerman Date: Wed, 24 Jun 2020 18:56:24 -0300 Subject: [PATCH 5/5] Rename @socket to @io --- spec/std/http/server/server_spec.cr | 4 ++-- src/http/client.cr | 36 ++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index cc932a7f7b4d..6c39a8e21aff 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -493,7 +493,7 @@ describe "#remote_address" do HTTP::Client.new(URI.parse("http://#{address1}/")) do |client| client.get("/") - remote_address.should eq(client.@socket.as(IPSocket).local_address) + remote_address.should eq(client.@io.as(IPSocket).local_address) end end end @@ -516,7 +516,7 @@ describe "#remote_address" do uri: URI.parse("https://#{ip_address1}"), tls: client_context) do |client| client.get("/") - remote_address.should eq(client.@socket.as(OpenSSL::SSL::Socket).local_address) + remote_address.should eq(client.@io.as(OpenSSL::SSL::Socket).local_address) end end end diff --git a/src/http/client.cr b/src/http/client.cr index 14dc1bd8d6d1..bc032ac0b986 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -114,7 +114,7 @@ class HTTP::Client # Whether automatic compression/decompression is enabled. property? compress : Bool = true - @socket : IO? + @io : IO? @dns_timeout : Float64? @connect_timeout : Float64? @read_timeout : Float64? @@ -154,7 +154,7 @@ class HTTP::Client # Instances created with this constructor cannot be reconnected. Once # `close` is called explicitly or if the connection doesn't support keep-alive, # the next call to make a request will raise an exception. - def initialize(@socket : IO, @host = "", @port = 80) + def initialize(@io : IO, @host = "", @port = 80) @reconnect = false end @@ -595,7 +595,7 @@ class HTTP::Client private def exec_internal_single(request) decompress = send_request(request) - HTTP::Client::Response.from_io?(socket, ignore_body: request.ignore_body?, decompress: decompress) + HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?, decompress: decompress) end private def handle_response(response) @@ -642,7 +642,7 @@ class HTTP::Client private def exec_internal_single(request) decompress = send_request(request) - HTTP::Client::Response.from_io?(socket, ignore_body: request.ignore_body?, decompress: decompress) do |response| + HTTP::Client::Response.from_io?(io, ignore_body: request.ignore_body?, decompress: decompress) do |response| yield response end end @@ -657,8 +657,8 @@ class HTTP::Client private def send_request(request) decompress = set_defaults request run_before_request_callbacks(request) - request.to_io(socket) - socket.flush + request.to_io(io) + io.flush decompress end @@ -756,32 +756,32 @@ class HTTP::Client # Closes this client. If used again, a new connection will be opened. def close - @socket.try &.close - @socket = nil + @io.try &.close + @io = nil end private def new_request(method, path, headers, body : BodyType) HTTP::Request.new(method, path, headers, body) end - private def socket - socket = @socket - return socket if socket + private def io + io = @io + return io if io unless @reconnect raise "This HTTP::Client cannot be reconnected" end hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host - socket = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout - socket.read_timeout = @read_timeout if @read_timeout - socket.write_timeout = @write_timeout if @write_timeout - socket.sync = false + io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout + io.read_timeout = @read_timeout if @read_timeout + io.write_timeout = @write_timeout if @write_timeout + io.sync = false {% if !flag?(:without_openssl) %} if tls = @tls - tcp_socket = socket + tcp_socket = io begin - socket = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host) + io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host) rescue exc # don't leak the TCP socket when the SSL connection failed tcp_socket.close @@ -790,7 +790,7 @@ class HTTP::Client end {% end %} - @socket = socket + @io = io end private def host_header