servant icon indicating copy to clipboard operation
servant copied to clipboard

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

Open vdukhovni opened this issue 2 years ago • 1 comments

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.

vdukhovni avatar Sep 10 '23 19:09 vdukhovni

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.

tchoutri avatar Apr 30 '24 11:04 tchoutri

Fixed by https://github.com/haskell-servant/servant/pull/1800. @vdukhovni Please try it out. :)

tchoutri avatar Mar 05 '25 21:03 tchoutri