Skip to content

Commit 0753db1

Browse files
committed
Tunnel: newtunnelconnection
Creates a tunneled connection with a pool key that `aquire` can find on subsequent requests if the connection is kept alive
1 parent 1323619 commit 0753db1

File tree

5 files changed

+131
-70
lines changed

5 files changed

+131
-70
lines changed

src/Connections.jl

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -610,31 +610,6 @@ function sslconnection(::Type{SSLContext}, tcp::TCPSocket, host::AbstractString;
610610
return io
611611
end
612612

613-
function sslupgrade(::Type{IOType}, c::Connection{T},
614-
host::AbstractString;
615-
pool::Union{Nothing, Pool}=nothing,
616-
require_ssl_verification::Bool=NetworkOptions.verify_host(host, "SSL"),
617-
keepalive::Bool=true,
618-
readtimeout::Int=0,
619-
kw...)::Connection{IOType} where {T, IOType}
620-
# initiate the upgrade to SSL
621-
# if the upgrade fails, an error will be thrown and the original c will be closed
622-
# in ConnectionRequest
623-
tls = if readtimeout > 0
624-
try_with_timeout(readtimeout) do _
625-
sslconnection(IOType, c.io, host; require_ssl_verification=require_ssl_verification, keepalive=keepalive, kw...)
626-
end
627-
else
628-
sslconnection(IOType, c.io, host; require_ssl_verification=require_ssl_verification, keepalive=keepalive, kw...)
629-
end
630-
# success, now we turn it into a new Connection
631-
conn = Connection(host, "", 0, require_ssl_verification, keepalive, tls)
632-
# release the "old" one, but don't return the connection since we're hijacking the socket
633-
release(getpool(pool, T), connectionkey(c))
634-
# and return the new one
635-
return acquire(() -> conn, getpool(pool, IOType), connectionkey(conn); forcenew=true)
636-
end
637-
638613
function Base.show(io::IO, c::Connection)
639614
nwaiting = applicable(tcpsocket, c.io) ? bytesavailable(tcpsocket(c.io)) : 0
640615
print(

src/HTTP.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ include("Connections.jl") ;using .Connections
4242
const ConnectionPool = Connections
4343
include("StatusCodes.jl") ;using .StatusCodes
4444
include("Messages.jl") ;using .Messages
45+
include("Tunnel.jl") ;using .Tunnel
4546
include("cookies.jl") ;using .Cookies
4647
include("Streams.jl") ;using .Streams
4748

src/Tunnel.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
module Tunnel
2+
3+
export newtunnelconnection
4+
5+
using Sockets, LoggingExtras, NetworkOptions, URIs
6+
using ConcurrentUtilities: acquire, try_with_timeout
7+
8+
using ..Connections, ..Messages, ..Exceptions
9+
using ..Connections: connection_limit_warning, getpool, getconnection, sslconnection, connectionkey, connection_isvalid
10+
11+
function newtunnelconnection(;
12+
target_type::Type{<:IO},
13+
target_host::AbstractString,
14+
target_port::AbstractString,
15+
proxy_type::Type{<:IO},
16+
proxy_host::AbstractString,
17+
proxy_port::AbstractString,
18+
proxy_auth::AbstractString="",
19+
pool::Union{Nothing, Pool}=nothing,
20+
connection_limit=nothing,
21+
forcenew::Bool=false,
22+
idle_timeout=typemax(Int),
23+
connect_timeout::Int=30,
24+
readtimeout::Int=30,
25+
keepalive::Bool=true,
26+
kw...)
27+
connection_limit_warning(connection_limit)
28+
29+
if isempty(target_port)
30+
target_port = istcptype(target_type) ? "80" : "443"
31+
end
32+
33+
require_ssl_verification = get(kw, :require_ssl_verification, NetworkOptions.verify_host(target_host, "SSL"))
34+
host_key = proxy_host * "/" * target_host
35+
port_key = proxy_port * "/" * target_port
36+
key = (host_key, port_key, require_ssl_verification, keepalive, true)
37+
38+
return acquire(
39+
getpool(pool, target_type),
40+
key;
41+
forcenew=forcenew,
42+
isvalid=c->connection_isvalid(c, Int(idle_timeout))) do
43+
44+
conn = Connection(host_key, port_key, idle_timeout, require_ssl_verification, keepalive,
45+
try_with_timeout0(connect_timeout) do _
46+
getconnection(proxy_type, proxy_host, proxy_port; keepalive, kw...)
47+
end
48+
)
49+
try
50+
try_with_timeout0(readtimeout) do _
51+
connect_tunnel(conn, target_host, target_port, proxy_auth)
52+
end
53+
54+
if !istcptype(target_type)
55+
tls = try_with_timeout0(readtimeout) do _
56+
sslconnection(target_type, conn.io, target_host; keepalive, kw...)
57+
end
58+
59+
# success, now we turn it into a new Connection
60+
conn = Connection(host_key, port_key, idle_timeout, require_ssl_verification, keepalive, tls)
61+
end
62+
63+
@assert connectionkey(conn) === key
64+
65+
conn
66+
catch ex
67+
close(conn)
68+
rethrow()
69+
end
70+
end
71+
end
72+
73+
function connect_tunnel(io, target_host, target_port, proxy_auth)
74+
target = "$(URIs.hoststring(target_host)):$(target_port)"
75+
@debugv 1 "📡 CONNECT HTTPS tunnel to $target"
76+
headers = Dict("Host" => target)
77+
if (!isempty(proxy_auth))
78+
headers["Proxy-Authorization"] = proxy_auth
79+
end
80+
request = Request("CONNECT", target, headers)
81+
# @debugv 2 "connect_tunnel: writing headers"
82+
writeheaders(io, request)
83+
# @debugv 2 "connect_tunnel: reading headers"
84+
readheaders(io, request.response)
85+
# @debugv 2 "connect_tunnel: done reading headers"
86+
if request.response.status != 200
87+
throw(StatusError(request.response.status,
88+
request.method, request.target, request.response))
89+
end
90+
end
91+
92+
function try_with_timeout0(f, timeout, ::Type{T}=Any) where {T}
93+
if timeout > 0
94+
try_with_timeout(f, timeout, T)
95+
else
96+
f(Ref(false))
97+
end
98+
end
99+
100+
istcptype(::Type{TCPSocket}) = true
101+
istcptype(::Type{<:IO}) = false
102+
103+
end # module Tunnel

src/clientlayers/ConnectionRequest.jl

Lines changed: 25 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module ConnectionRequest
33
using URIs, Sockets, Base64, LoggingExtras, ConcurrentUtilities, ExceptionUnwrapping
44
using MbedTLS: SSLContext, SSLConfig
55
using OpenSSL: SSLStream
6-
using ..Messages, ..IOExtras, ..Connections, ..Streams, ..Exceptions
6+
using ..Messages, ..IOExtras, ..Connections, ..Streams, ..Exceptions, ..Tunnel
77
import ..SOCKET_TYPE_TLS
88

99
islocalhost(host::AbstractString) = host == "localhost" || host == "127.0.0.1" || host == "::1" || host == "0000:0000:0000:0000:0000:0000:0000:0001" || host == "0:0:0:0:0:0:0:1"
@@ -77,8 +77,31 @@ function connectionlayer(handler)
7777
IOType = sockettype(url, socket_type, socket_type_tls)
7878
start_time = time()
7979
try
80-
io = newconnection(IOType, url.host, url.port; readtimeout=readtimeout, connect_timeout=connect_timeout, kw...)
80+
if !isnothing(proxy) && req.url.scheme in ("https", "wss", "ws")
81+
target_IOType = sockettype(target_url, socket_type, socket_type_tls)
82+
83+
io = newtunnelconnection(;
84+
target_type=target_IOType,
85+
target_host=target_url.host,
86+
target_port=target_url.port,
87+
proxy_type=IOType,
88+
proxy_host=url.host,
89+
proxy_port=url.port,
90+
proxy_auth=header(req, "Proxy-Authorization"),
91+
connect_timeout,
92+
readtimeout,
93+
kw...
94+
)
95+
96+
req.headers = filter(x->x.first != "Proxy-Authorization", req.headers)
97+
else
98+
io = newconnection(IOType, url.host, url.port; readtimeout=readtimeout, connect_timeout=connect_timeout, kw...)
99+
end
81100
catch e
101+
if e isa StatusError
102+
return e.response
103+
end
104+
82105
if logerrors
83106
msg = current_exceptions_to_string()
84107
@error msg type=Symbol("HTTP.ConnectError") method=req.method url=req.url context=req.context logtag=logtag
@@ -91,31 +114,6 @@ function connectionlayer(handler)
91114

92115
shouldreuse = !(target_url.scheme in ("ws", "wss"))
93116
try
94-
if proxy !== nothing && target_url.scheme in ("https", "wss", "ws")
95-
shouldreuse = false
96-
# tunnel request
97-
if target_url.scheme in ("https", "wss")
98-
target_url = URI(target_url, port=443)
99-
elseif target_url.scheme in ("ws", ) && target_url.port == ""
100-
target_url = URI(target_url, port=80) # if there is no port info, connect_tunnel will fail
101-
end
102-
r = if readtimeout > 0
103-
try_with_timeout(readtimeout) do _
104-
connect_tunnel(io, target_url, req)
105-
end
106-
else
107-
connect_tunnel(io, target_url, req)
108-
end
109-
if r.status != 200
110-
close(io)
111-
return r
112-
end
113-
if target_url.scheme in ("https", "wss")
114-
io = Connections.sslupgrade(socket_type_tls, io, target_url.host; readtimeout=readtimeout, kw...)
115-
end
116-
req.headers = filter(x->x.first != "Proxy-Authorization", req.headers)
117-
end
118-
119117
stream = Stream(req.response, io)
120118
return handler(stream; readtimeout=readtimeout, logerrors=logerrors, logtag=logtag, kw...)
121119
catch e
@@ -153,20 +151,4 @@ end
153151

154152
sockettype(url::URI, tcp, tls) = url.scheme in ("wss", "https") ? tls : tcp
155153

156-
function connect_tunnel(io, target_url, req)
157-
target = "$(URIs.hoststring(target_url.host)):$(target_url.port)"
158-
@debugv 1 "📡 CONNECT HTTPS tunnel to $target"
159-
headers = Dict("Host" => target)
160-
if (auth = header(req, "Proxy-Authorization"); !isempty(auth))
161-
headers["Proxy-Authorization"] = auth
162-
end
163-
request = Request("CONNECT", target, headers)
164-
# @debugv 2 "connect_tunnel: writing headers"
165-
writeheaders(io, request)
166-
# @debugv 2 "connect_tunnel: reading headers"
167-
readheaders(io, request.response)
168-
# @debugv 2 "connect_tunnel: done reading headers"
169-
return request.response
170-
end
171-
172154
end # module ConnectionRequest

test/pool.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,14 @@ end
158158
@testset "Only one tunnel should be established with sequential requests" begin
159159
https_request_ip_through_proxy()
160160
https_request_ip_through_proxy()
161-
@test_broken connectcount == 1
161+
@test connectcount == 1
162162
end
163163

164164
@testset "parallell tunnels should be established with parallell requests" begin
165165
n_asyncgetters = 3
166166
asyncgetters = [@async https_request_ip_through_proxy() for _ in 1:n_asyncgetters]
167167
wait.(asyncgetters)
168-
@test_broken connectcount == n_asyncgetters
168+
@test connectcount == n_asyncgetters
169169
end
170170

171171
finally

0 commit comments

Comments
 (0)