use "collections"
use "net"
use "net_ssl"
class HTTPClient
"""
Manages a group of HTTP connections on behalf of a client application.
A client should create one instance of this class.
"""
let _auth: TCPConnectAuth
let _sslctx: SSLContext
let _pipeline: Bool
let _keepalive_timeout_secs: U32
let _sessions: Map[_HostService, _ClientConnection] = _sessions.create()
let _handlermaker: HandlerFactory val
new create(
auth: TCPConnectAuth,
handlermaker: HandlerFactory val,
sslctx: (SSLContext | None) = None,
pipeline: Bool = true,
keepalive_timeout_secs: U32 = 0)
=>
"""
Create the context in which all HTTP sessions will originate. The `handlermaker`
is used to create the `HTTPHandler` that is applied with each received
payload after making a request. All requests made with one client are created
using the same handler factory, if you need different handlers for different
requests, you need to create different clients.
Parameters:
- keepalive_timeout_secs: Use TCP Keepalive and check if the other side is down
every `keepalive_timeout_secs` seconds.
"""
_auth = auth
_sslctx = try
sslctx as SSLContext
else
recover
let newssl = SSLContext
newssl.set_client_verify(false)
newssl
end
end
_pipeline = pipeline
_keepalive_timeout_secs = keepalive_timeout_secs
_handlermaker = handlermaker
fun ref apply(request: Payload trn) : Payload val ? =>
"""
Schedule a request on an HTTP session. If a new connection is created,
a new instance of the application's Receive Handler will be created
for it. A `val` copy of the `Payload` is returned, and it can not be
modified after this point.
This is useful in Stream and Chunked transfer modes, so that the
application can follow up with calls to `Client.send_body`.
"""
let session = _get_session(request.url)?
let mode = request.transfer_mode
request.session = session
let valrequest: Payload val = consume request
session(valrequest)
valrequest
fun ref dispose() =>
"""
Disposes the sessions and cancels all pending requests.
"""
for s in _sessions.values() do
s.dispose()
end
_sessions.clear()
/*
fun ref cancel(request: Payload val) =>
"""
Cancel a request.
"""
match request.session
| let s _ClientConnection tag => s.cancel(request)
end
*/
fun ref _get_session(url: URL) : _ClientConnection ? =>
"""
Gets or creates an HTTP Session for the given URL. If a new session
is created, a new Receive Handler instance is created too.
"""
let hs = _HostService(url.scheme, url.host, url.port.string())
try
// Look for an existing session
_sessions(hs)?
else
// or create a new session of the correct type.
let session =
match url.scheme
| "http" =>
_ClientConnection(_auth, hs.host, hs.service,
None, _pipeline, _keepalive_timeout_secs, _handlermaker)
| "https" =>
_ClientConnection(_auth, hs.host, hs.service,
_sslctx, _pipeline, _keepalive_timeout_secs, _handlermaker)
else
error
end
_sessions(hs) = session
session
end
fun ref send_body(data: ByteSeq val, session: HTTPSession) =>
session.write(data)
class _SessionGuard
"""
Enforces the rule that an 'unsafe' request monopolizes the
HTTPSession. A 'safe' request does not modify a resource state on
the server, and such a request has no body.
"""
let _session: HTTPSession
let _sent: List[Payload val] = List[Payload val]
var _lastreqsafe: Bool = true
var current: (Payload val | None) = None
new iso create(session: HTTPSession) =>
_session = session
fun ref submit(request: Payload val) ? =>
"""
Accept a request for transmission to the server. This will fail if
the request is not "safe" and the HTTPSession is busy. Due to the
possibly large body size, these requests can not be queued.
"""
let thisreqsafe = request.is_safe()
// If the channel is idle, just send this request.
if _sent.size() == 0 then
_lastreqsafe = thisreqsafe
current = request
_session(request)
return
end
// Channel is not idle. But we can send it anyway if
// both the last request and this one are safe.
if _lastreqsafe and thisreqsafe then
_sent.push(request)
_session(request)
return
end
// Channel can not accept another request now.
error