using afIoc::Inject
using afIocConfig::Config
using afBeanUtils::ArgNotFoundErr
** (Service) - A Request Handler that maps URLs to file resources inside pods.
**
** To access a pod resource use URLs in the format:
**
** /<baseUrl>/<podName>/<fileName>
**
** By default the base url is '/pods/' which means you should always be able to access the flux icon.
**
** /pods/icons/x256/flux.png
**
** Change the base url in the application defaults:
**
** pre>
** @Contribute { serviceType=ApplicationDefaults# }
** static Void contributeAppDefaults(Configuration conf) {
** conf[BedSheetConfigIds.podHandlerBaseUrl] = `/some/other/url/`
** }
** <pre
**
** Set the base url to 'null' to disable the serving of pod resources.
**
** Because pods may contain sensitive data, the entire contents of all the pods are NOT available by default. Oh no!
** 'PodHandler' has a whitelist of Regexes that specify which pod files are allowed to be served.
** If a pod resource doesn't match a regex, it doesn't get served.
**
** By default only a handful of files with common web extensions are allowed. These include:
**
** pre>
** . web files: .css .htm .html .js
** image files: .bmp .gif .ico .jpg .png
** web font files: .eot .ttf .woff
** other files: .txt
** <pre
**
** To add or remove whitelist regexs, contribute to 'PodHandler':
**
** pre>
** @Contribute { serviceType=PodHandler# }
** static Void contributePodHandler(Configuration conf) {
** conf.remove(".txt") // prevent .txt files from being served
** conf["acmePodFiles"] = "^fan://acme/.*$" // serve all files from the acme pod
** }
** <pre
const mixin PodHandler {
** The local URL under which pod resources are served.
**
** Set by `BedSheetConfigIds.podHandlerBaseUrl`, defaults to '/pods/'.
abstract Uri? baseUrl()
** The Route handler method.
** Returns a 'FileAsset' as mapped from the HTTP request URL or null if not found.
@NoDoc // boring route handler method
abstract FileAsset? serviceRoute(Uri remainingUrl)
** Given a local URL (a simple URL relative to the WebMod), this returns a corresponding (cached) 'FileAsset'.
** Throws 'ArgErr' if the URL is not mapped.
abstract FileAsset fromLocalUrl(Uri localUrl)
** Given a pod resource file, this returns a corresponding (cached) 'FileAsset'.
** The URI must adhere to the 'fan://<pod>/<file>' scheme notation.
** Throws 'ArgErr' if the pod resource is not mapped or does not exist
abstract FileAsset fromPodResource(Uri podResource)
}
internal const class PodHandlerImpl : PodHandler {
@Config { id="afBedSheet.podHandler.baseUrl" }
@Inject override const Uri? baseUrl
@Inject private const FileAssetCache fileCache
private const Regex[] whitelistFilters
new make(Regex[] filters, |This|? in) {
this.whitelistFilters = filters
in?.call(this)
if (baseUrl == null)
return
if (!baseUrl.isPathOnly)
throw BedSheetErr(BsErrMsgs.urlMustBePathOnly(baseUrl, `/pods/`))
if (!baseUrl.isPathAbs)
throw BedSheetErr(BsErrMsgs.urlMustStartWithSlash(baseUrl, `/pods/`))
if (!baseUrl.isDir)
throw BedSheetErr(BsErrMsgs.urlMustEndWithSlash(baseUrl, `/pods/`))
}
override FileAsset? serviceRoute(Uri remainingUrl) {
try {
// use pathStr to knockout any unwanted query str
return fromPodResource(`fan://${remainingUrl.pathStr}`)
} catch
// don't bother making fromLocalUrl() checked, it's too much work for a 404!
// null means that 'Routes' didn't process the request, so it continues down the pipeline.
return null
}
override FileAsset fromLocalUrl(Uri localUrl) {
if (baseUrl == null)
throw Err(BsErrMsgs.podHandler_disabled)
Utils.validateLocalUrl(localUrl, `/pods/icons/x256/flux.png`)
if (!localUrl.toStr.startsWith(baseUrl.toStr))
throw ArgErr(BsErrMsgs.podHandler_urlNotMapped(localUrl, baseUrl))
remainingUrl := localUrl.relTo(baseUrl)
return fromPodResource(`fan://${remainingUrl}`)
}
override FileAsset fromPodResource(Uri podUrl) {
if (baseUrl == null)
throw Err(BsErrMsgs.podHandler_disabled)
if (podUrl.scheme != "fan")
throw ArgErr(BsErrMsgs.podHandler_urlNotFanScheme(podUrl))
resource := (Obj?) null
try resource = (podUrl).get
catch throw ArgErr(BsErrMsgs.podHandler_urlDoesNotResolve(podUrl))
if (resource isnot File) // WTF!?
throw ArgErr(BsErrMsgs.podHandler_urlNotFile(podUrl, resource))
podPath := ((File) resource).uri.toStr
if (!whitelistFilters.any { it.matches(podPath) })
throw ArgNotFoundErr(BsErrMsgs.podHandler_notInWhitelist(podPath), whitelistFilters)
return fileCache.getOrAddOrUpdate(resource) |File file->FileAsset| {
if (!file.exists)
throw ArgErr(BsErrMsgs.fileNotFound(file))
host := file.uri.host.toUri.plusSlash
path := file.uri.pathOnly.relTo(`/`)
localUrl := baseUrl + host + path
clientUrl := fileCache.toClientUrl(localUrl, file)
return FileAsset {
it.file = file
it.exists = file.exists
it.modified = file.modified?.floor(1sec)
it.size = file.size
it.etag = it.exists ? "${it.size?.toHex}-${it.modified?.ticks?.toHex}" : null
it.localUrl = localUrl
it.clientUrl = clientUrl
}
}
}
}