|
| 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 | + @debug "📡 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 | + # @debug "connect_tunnel: writing headers" |
| 82 | + writeheaders(io, request) |
| 83 | + # @debug "connect_tunnel: reading headers" |
| 84 | + readheaders(io, request.response) |
| 85 | + # @debug "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 | +""" |
| 93 | +Wrapper to try_with_timeout that optionally disables the timeout if given a non-positive duration. |
| 94 | +""" |
| 95 | +function try_with_timeout0(f, timeout, ::Type{T}=Any) where {T} |
| 96 | + if timeout > 0 |
| 97 | + try_with_timeout(f, timeout, T) |
| 98 | + else |
| 99 | + f(Ref(false)) # `f` may check its argument to see if the timeout was reached. |
| 100 | + end |
| 101 | +end |
| 102 | + |
| 103 | +istcptype(::Type{TCPSocket}) = true |
| 104 | +istcptype(::Type{<:IO}) = false |
| 105 | + |
| 106 | +end # module Tunnel |
0 commit comments