haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.82k stars 412 forks source link

Question: How to restrict an API to a particular virtual host (value of the request "Host:" header)? #1705

Open vdukhovni opened 1 year ago

vdukhovni commented 1 year ago

My server hosts:

  1. A redirect from the "/" path to a different host that describes the project
  2. The project's backend API under "/prefix"
  3. Some static content (paths other than "/" or the API prefix.

I am adding a variant of "3" which is a virtual host for another name that will serve static content under a different directory based on the incoming "Host:" header. This works.

However, I'd prefer to not serve either the redirect or the API to requests for the alternative name. How can this be done?

type Blank  = Headers '[Header "Location" String] NoContent
type Redirect = Verb 'GET 301 '[OctetStream] Blank  -- Redirect GET /
type Backend = "prefix" :> Capture "input" Text :> Get '[JSON] Result
type Files       = Header' '[Required, Strict] "Host" String :> Raw

type API  = Redirect
       :<|> Backend
       :<|> Files

This is currently served via the below, which is only host-specific for the static content. How would I restrict the redirect and the API to just the default vhost, with all other hosts seeing just the Files API (or perhaps some day a different host-specific router)?

router = redirect :<|> backend :<|> serveStatic rootPath
  where
    redirect              = pure $ addHeader redirURL NoContent

    backend input   = ...

    -- Choose a host-specific root if applicable, else use default.
    serveStatic :: forall m. FilePath -> String -> S.ServerT Raw m
    serveStatic root hostport
        | Just root' <- rootFor $ break (== ':') $ map toLower hostport =
            S.serveDirectoryWith
                $ W.webAppSettingsWithLookup root' $ getEtag maxAge
        | otherwise =
            S.serveDirectoryWith
                $ (W.webAppSettingsWithLookup root $ getEtag maxAge)
                    { W.ssMaxAge = maybe W.NoMaxAge W.MaxAgeSeconds maxAge }

P.S. I also didn't know how to reject malformed Host: headers with a 401 or suitable error code. So settled to just ignore them. Perhaps something I could do with RawM, but it is not immediately obvious how to use serveDirectoryWith as a handler for RawM.

tchoutri commented 5 months ago

Hmmm, it's an interesting problematic. I think my first reflex would be to do routing at the reverse proxy level (nginx, Apache, caddy, etc), and expose endpoints with specific audiences in mind.