using afIoc::Inject
using afIoc::RegistryMeta
using afIocConfig::ConfigSource
using web::WebReq
using web::WebUtil
using concurrent
** (Service) -
** Information about the BedSheet server.
const mixin BedSheetServer {
** The pod that contains the initial 'AppModule'.
abstract Pod? appPod()
** The 'AppModule'.
abstract Type? appModule()
** Returns 'proj.name' from the application's pod meta, or the pod name if not defined.
abstract Str appName()
** The port BedSheet is listening to.
abstract Int port()
** The public facing domain (including scheme & port) used to create absolute URLs.
**
** If set, this is taken from the `BedSheetConfigIds.host` config value.
**
** If not, then an attempt is made to get this from the HTTP request via the following (in order):
** 1. The 'Forwarded' HTTP header - see [RFC 7239]`https://tools.ietf.org/html/rfc7239`
** 2. The 'X-Forwarded-XXXX' HTTP headers
** 3. The 'host' HTTP header
**
** If all else fails, example a HTTP 1.0. request, then 'http://localhost:${port}/' is returned.
**
** Example:
**
** syntax: fantom
** bedSheetServer.host() // --> http://www.fantomfactory.org/
abstract Uri host()
** The Registry options BedSheet was started with.
abstract [Str:Obj] options()
** Returns a list of modules loaded by this BedSheet's IoC
abstract Type[] moduleTypes()
** Returns a unique list of pods that contain modules loaded by this BedSheet's IoC.
**
** Useful for gaining a list of pods used in an application, should you wish to *scan* for
** classes.
abstract Pod[] modulePods()
** The request path to this BedSheet 'WebMod'.
** Only really relevant should BedSheet be started in a [RouteMod]`webmod::RouteMod`.
**
** Starts and ends with a '/'. Example, '`/pub/`'
**
** Returns '`/`' should BedSheet be the root 'WebMod' (the usual case).
**
** @see `web::WebReq.modBase`
abstract Uri path()
** Prepends any extra 'WebMod' path info to the given URL so it may be used by clients and browsers.
** The given 'WebMod' local URL should be relative to the BedSheet 'WebMod' and may, or may not, start with a '/'.
abstract Uri toClientUrl(Uri localUrl)
** Creates an absolute URL for public use; including scheme and authority (host).
** The given 'clientUrl' should be relative to the host and start with a '/'.
**
** The scheme and authority in the generated URL are taken from the 'host()' method.
abstract Uri toAbsoluteUrl(Uri clientUrl)
}
internal const class BedSheetServerImpl : BedSheetServer {
// nullable for testing
@Inject private const RegistryMeta? regMeta
@Inject private const ConfigSource? configSrc
@Inject private const Log? log
new make(|This|in) { in(this) }
override Pod? appPod() {
regMeta[BsConstants.meta_appPod]
}
override Type? appModule() {
regMeta[BsConstants.meta_appModule]
}
override Str appName() {
regMeta[BsConstants.meta_appName]
}
override Int port() {
regMeta[BsConstants.meta_appPort]
}
override [Str:Obj] options() {
regMeta.options
}
override Type[] moduleTypes() {
regMeta.moduleTypes
}
override Pod[] modulePods() {
regMeta.modulePods
}
override Uri path() {
// default to root for testing
webReq?.modBase ?: `/`
}
override Uri host() {
// if someone has gone to the trouble of setting a config value - then let's use it
// as it's probably the normalised host of multiple sites
// we get host this way 'cos BedSheetServer is used (in a round about way by Pillow) in a
// DependencyProvider, so @Config is not available for injection
// host is validated on startup, so we know it's okay
bedSheetHost := configSrc.get(BedSheetConfigIds.host, Uri#)
if (!bedSheetHost.toStr.startsWith("http://localhost:"))
return bedSheetHost
// otherwise, lets parse the web req
webReq := webReq
if (webReq != null) {
host := hostViaHeaders(webReq.headers)
if (host != null)
return host
}
return bedSheetHost
}
override Uri toClientUrl(Uri localUrl) {
// if we stop throwing an Err here, then we need to update ColdFeet
if (localUrl.host != null || !localUrl.isRel) // can't use Uri.isPathOnly because we allow QueryStrs and Fragments...?
throw ArgErr(BsErrMsgs.urlMustBePathOnly(localUrl, `/css/myStyles.css`))
return path + localUrl.relTo(`/`)
}
override Uri toAbsoluteUrl(Uri clientUrl) {
Utils.validateLocalUrl(clientUrl, `/css/myStyles.css`)
return host + clientUrl.relTo(`/`)
}
internal Uri? hostViaHeaders(Str:Str headers) {
forwarded := headers["Forwarded"]
try {
if (forwarded != null) {
forHost := null as Str
forProt := null as Str
splits := forwarded.split(';')
splits.each {
vals := it.split('=')
if (vals.first.equalsIgnoreCase("proto"))
forProt = vals.last.startsWith("\"") ? WebUtil.fromQuotedStr(vals.last) : vals.last
if (vals.first.equalsIgnoreCase("host"))
forHost = vals.last.startsWith("\"") ? WebUtil.fromQuotedStr(vals.last) : vals.last
}
if (forHost != null && forProt != null)
return `${forProt}://${forHost}`
}
} catch {
log.warn("Dodgy 'Forwarded' HTTP header value: Forwarded = ${forwarded}")
}
proxyScheme := headers["X-Forwarded-Proto"]
proxyHost := headers["X-Forwarded-Host"]
proxyPort := headers["X-Forwarded-Port"]
try {
if (proxyScheme != null && proxyHost != null) {
if (proxyHost.endsWith(":"))
proxyHost = proxyHost[0..<-1]
if (proxyPort != null && !proxyHost.contains(":"))
return `${proxyScheme}://${proxyHost}:${proxyPort}`
else
return `${proxyScheme}://${proxyHost}`
}
} catch {
log.warn("Dodgy 'X-Forwarded-XXXX' HTTP header values:\n X-Forwarded-Proto= ${proxyScheme}\n X-Forwarded-Host = ${proxyHost}\n X-Forwarded-Port = ${proxyPort}")
}
host := headers["Host"]
try {
return `http://${host}/`
} catch {
log.warn("Dodgy 'Host' HTTP header value: Host = ${host}")
}
return null
}
private WebReq? webReq() {
// use Actor.locals (and not reg.serviceById) to avoid Errs being thrown during testing
Actor.locals["web.req"]
}
}