sourceafBedSheet::HttpRequest.fan

using afIoc::Inject
using afIoc::Registry
using web::WebReq
using web::WebUtil
using inet::IpAddr
using inet::SocketOptions
using concurrent

** (Service) - An injectable 'const' version of [WebReq]`web::WebReq`.
** 
** This class will always refer to the current web request.
const mixin HttpRequest {

    ** Returns 'true' if an 'XMLHttpRequest', as specified by the 'X-Requested-With' HTTP header.
    abstract Bool isXmlHttpRequest()
    
    ** The HTTP version of the request.
    ** 
    ** @see `web::WebReq.version`
    abstract Version httpVersion()
    
    ** The HTTP request method in uppercase. Example: GET, POST, PUT.
    ** 
    ** @see `web::WebReq.method`
    abstract Str httpMethod()

    ** The IP host address of the client socket making this request.
    ** 
    ** @see `web::WebReq.remoteAddr`
    abstract IpAddr remoteAddr()
    
    ** The IP port of the client socket making this request.
    ** 
    ** @see `web::WebReq.remotePort`
    abstract Int remotePort()

    ** The URL relative to `BedSheetWebMod`, includes query string and fragment. 
    ** Always starts with a '/'.
    ** 
    ** Examples:
    **   /a/b/index.html
    **   /a?q=bar
    ** 
    ** This is equivalent to a *local URL*. 
    ** 
    ** @see `web::WebReq.modRel`
    abstract Uri url()

    ** The url path component, cached.
    abstract Str[] urlPath()

    ** Returns the absolute request URL including the full authority, mod path, and the query string.  
    ** If defined, this is taken from the `BedSheetConfigIds.host` config value othereise
    ** efforts are made to restore the original HTTP header 'host' should it have been lost / replaced by a proxy.
    ** 
    ** Equivalent(ish) to:
    **   
    **   host() + WebReq.absUri
    ** 
    ** Examples:
    **   http://www.foo.com/a/b/index.html
    **   http://www.foo.com/a?q=bar
    ** 
    ** @see `host`
    ** @see `web::WebReq.absUri`
    abstract Uri urlAbs()

    ** Map of HTTP request headers. The map is readonly and case insensitive.
    ** 
    ** @see `web::WebReq.headers`
    ** 
    ** @see `http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests`
    abstract HttpRequestHeaders headers()

    ** Attempts to determine the original 'host' HTTP request header.
    ** 
    ** Proxy servers such as httpd or AWS ELB, often replace the originating client's 'Host' header with their own.
    ** This method attempts to untangle common proxy request headers to reform the original 'Host'.
    ** 
    ** The 'Host' can be useful to ensure clients contact the correct (sub) domain, and web apps may redirect them if not. 
    ** 
    ** The 'Host' value is formed by inspecting, in order:
    ** 
    **  1. the 'Forwarded' HTTP header as per [RFC 7239]`https://tools.ietf.org/html/rfc7239`
    **  1. the 'X-Forwarded-XXXX' de-facto standard header
    **  1. the 'Host' standard header
    ** 
    ** Typical responses may be:
    ** 
    **   http://fantom-lang.org/
    **   //fantom-lang.org/
    ** 
    ** The scheme may be missing if it can not be reliably obtained.
    ** 
    ** HTTP 1.0 requests are not required to send a host header, for which this method returns 'null'.
    abstract Uri? host()

    ** The accepted locales for this request based on the "Accept-Language" HTTP header. List is 
    ** sorted by preference, where 'locales.first' is best, and 'locales.last' is worst. This list 
    ** is guaranteed to contain Locale("en").
    ** 
    ** @see `web::WebReq.locales`
    abstract Locale[] locales()
    
    ** 'Stash' allows you to store temporary data on the request, to easily pass it between 
    ** services and objects.
    ** 
    ** It is good for a quick win, but if you find yourself consistently relying on it, consider 
    ** making a thread scoped service instead. 
    abstract Str:Obj? stash()

    ** Returns the request body.
    abstract HttpRequestBody body()
    
    ** This method will:
    **   1. Check that the content-type is form-data
    **   2. Get the boundary string
    **   3. Invoke the callback for each part
    **
    ** For each part in the stream this calls the given callback function with the part's 
    ** name, headers, and an input stream used to read the part's body.
    ** 
    ** @see `web::WebReq.parseMultiPartForm`
    abstract Void parseMultiPartForm(|Str partName, InStream in, Str:Str headers| callback)

    abstract SocketOptions socketOptions()
    
}

internal const class HttpRequestImpl : HttpRequest {    
    static   const Log                  log         := HttpRequestImpl#.pod.log
    @Inject  const |->RequestState|?    reqState    // nullable for testing
    @Inject  const |->BedSheetServer|?  bedServer   // nullable for testing

    new make(|This|? in := null) { 
        in?.call(this) 
    }
    override HttpRequestHeaders headers() {
        reqState().requestHeaders
    }
    override Bool isXmlHttpRequest() {
        headers.get("X-Requested-With")?.equalsIgnoreCase("XMLHttpRequest") ?: false
    }
    override Version httpVersion() {
        webReq.version
    }
    override Str httpMethod() {
        webReq.method
    }   
    override IpAddr remoteAddr() {
        webReq.remoteAddr
    }
    override Int remotePort() {
        webReq.remotePort       
    }
    override Uri url() {
        rel := webReq.modRel
        
        // sometimes Wisp passes dodgy URLs like `//dev/` - usually from hack attempts, e.g. //wp-admin/install.php
        // don't bother logging it, just sort it so an appropriate error page can be served (e.g. 404)
        while (rel.toStr.startsWith("//"))
            rel = rel.toStr[1..-1].toUri
        
        // see [Inconsistent WebReq::modRel()]`http://fantom.org/sidewalk/topic/2237`
        return rel.isPathAbs ? rel : `/` + rel
    }
    override Str[] urlPath() {
        webReq  := webReq
        stash   := webReq.stash
        if (stash.containsKey("afBedSheet.urlPath") == false)
            stash["afBedSheet.urlPath"] = webReq.modRel.path
        return stash["afBedSheet.urlPath"]
    }
    override Uri urlAbs() {
        host := bedServer().host
        if (host.scheme == null)
            host = `http:${host}`
        return host + webReq.uri
    }
    override Uri? host() {
        hostViaHeaders(headers.val)
    }
    override Locale[] locales() {
        webReq.locales
    }
    override Str:Obj? stash() {
        webReq.stash
    }
    override HttpRequestBody body() {
        reqState().requestBody
    }
    override SocketOptions socketOptions()  {
        webReq.socketOptions
    }
    override Void parseMultiPartForm(|Str, InStream, Str:Str| cb) {
        // copied from 'webReq.parseMultiPartForm()' but uses body.in.
        mime := MimeType(this.headers["Content-Type"])
        if (mime.subType != "form-data") throw Err("Invalid content-type: $mime")
        boundary := mime.params["boundary"] ?: throw Err("Missing boundary param: $mime")
        WebUtil.parseMultiPart(body.in, boundary) |partHeaders, partIn| {
            cd          := partHeaders["Content-Disposition"] ?: throw Err("Multi-part missing Content-Disposition")
            semi        := cd.index(";") ?: throw Err("Expected semicolon; Content-Disposition: $cd")
            params      := MimeType.parseParams(cd[cd.index(";")+1..-1])
            formName    := params["name"] ?: throw Err("Expected name param; Content-Disposition: $cd")
            cb(formName, partIn, partHeaders)
            try { partIn.skip(Int.maxVal) } catch {} // drain stream
        }
    }
    override Str toStr() {
        "$httpMethod $url"
    }
    private WebReq? webReq(Bool checked := true) {
        // let's simplify and optimise, no point in querying IoC for this.
        webReq := Actor.locals["web.req"]
        if (webReq == null && checked)
            throw Err("No web request active in thread")
        return webReq
    }
    static Uri? hostViaHeaders(Str:Str headers) {
        proto   := null as Str
        host    := null as Str
        port    := null as Str
        
        forwarded   := headers["Forwarded"]
        try {
            // note rfc7239 doesn't define a port number for the host
            if (forwarded != null) {
                splits  := forwarded.split(';')
                splits.each {
                    vals := it.split('=')
                    if (vals.first.equalsIgnoreCase("proto"))
                        proto = vals.last.startsWith("\"") ? WebUtil.fromQuotedStr(vals.last) : vals.last
                    if (vals.first.equalsIgnoreCase("host"))
                        host  = vals.last.startsWith("\"") ? WebUtil.fromQuotedStr(vals.last) : vals.last
                }
            }
        } catch {
            log.warn("Dodgy 'Forwarded' HTTP header value:  Forwarded = ${forwarded}")
        }
        
        if (forwarded == null) {
            proxyProto  := headers["X-Forwarded-Proto"]
            proxyHost   := headers["X-Forwarded-Host"]
            proxyPort   := headers["X-Forwarded-Port"]
            try {
                if (proxyHost != null) {
                    if (proxyHost.endsWith(":"))
                        proxyHost = proxyHost[0..<-1]
                    if (proxyHost.contains(":")) {
                        i := proxyHost.index(":")
                        if (proxyPort == null)
                            proxyPort = proxyHost[i+1..-1]
                        proxyHost = proxyHost[0..<i]
                    }
                }
                
                proto   = proxyProto
                host    = proxyHost
                port    = proxyPort
            } catch {
                log.warn("Dodgy 'X-Forwarded-XXXX' HTTP header values:\n  X-Forwarded-Proto=${proxyProto}\n  X-Forwarded-Host=${proxyHost}\n  X-Forwarded-Port=${proxyPort}")
            }
        }
        
        if (host == null)
            host = headers["host"]

        try {
            if (host == null)
                return null
            
            if (proto != null && port != null)
                return `${proto}://${host}:${port}/`
                
            if (proto != null)
                return `${proto}://${host}/`

            if (port != null)
                return `//${host}:${port}/`

            return `//${host}/`
        } catch {
            log.warn("Dodgy 'proto' & 'Host' values: proto=${proto}  host=${host}  port=${port}")
        }

        return null
    }
}