sourceafButter::HttpTerminator.fan

using inet
using web::WebUtil

// TODO: Continue 100 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
// TODO: pipelining - use middleware, have it set the 'socket' via req.stash

** (Terminator) - A 'Butter' Terminator for making real HTTP requests. 
** When used in a chain, no other middleware should come after this one. (For they will not be called.)
** 
** To use a proxy, set the full proxy URL (as a 'Uri') in the request stash under the key 'afButter.proxy':
** 
**   syntax: fantom
**   req.stash[afButter.proxy] = `http://proxy.example.org:8069`
** 
class HttpTerminator : ButterMiddleware {

    SocketOptions? options
    
    ** Makes a real HTTP request.
    override ButterResponse sendRequest(Butter butter, ButterRequest req) {
        if (!req.url.isAbs || req.url.host == null)
            throw ButterErr(ErrMsgs.reqUriHasNoScheme(req.url))

        req._primeForSend

        proxyUrl := proxyUrl(req)
        socket   := req.stash["afButter.socket"] as TcpSocket ?: connect(req, proxyUrl)
        out      := socket.out

        try {
            reqOutStream := WebUtil.makeContentOutStream(req.headers.val, out)

            // request uri is absolute if proxy, relative otherwise
            reqPath := (proxyUrl != null ? req.url : req.url.relToAuth).encode
    
            // send request
            out.print("${req.method} ${reqPath} HTTP/${req.version}\r\n")
            req.headers.each |v, k| { out.print("${k}: ${v}\r\n") }
            out.print("\r\n")
            out.flush

            if (req.body.buf != null) {
                req.body.buf.seek(0).in.pipe(out)
                out.flush
            }

            return ButterResponse(socket.in)

        } finally {
            out.close
            socket.close
        }
    }
    
    internal TcpSocket connect(ButterRequest req, Uri? proxyUrl) {
        connUrl := proxyUrl ?: req.url
        isHttps := connUrl.scheme == "https"
        defPort := isHttps ? 443 : 80
        socket  := isHttps ? newTlsTcpSocket : TcpSocket()
        if (options != null) socket.options.copyFrom(this.options)

        socket.connect(IpAddr(connUrl.host), connUrl.port ?: defPort)
        return socket
    }

    ** Grabs the proxy URL from the request stash.
    internal Uri? proxyUrl(ButterRequest req) {
        proxyObj := req.stash["afButter.proxy"]
        if (proxyObj != null && proxyObj isnot Uri)
            Utils.getLog(this.typeof).warn(LogMsgs.httpTerminator_proxyNotUri(proxyObj))
        proxyUrl := proxyObj as Uri
        return proxyUrl
    }
    
    @NoDoc  // used by Bounce
    static Str normaliseHost(Uri url) {
        ButterRequest.normaliseHost(url)
    }
    
    ** Retain backwards compatibility with all recent versions of Fantom.
    private static TcpSocket newTlsTcpSocket() {
        Pod.find("inet").version >= Version("1.0.77")
            ? TcpSocket()->upgradeTls
            : TcpSocket#.method("makeTls").call
    }
}