on master help sign in
Merge pull request #364 from yesodweb/approot-middleware

Add approot middleware

commit188db6cc95cd6f0faa79d96f11fc3c76c50a416e authorMichael Snoyman <michael@snoyman.com> dateSun 12 Apr 2015, 18:28 (12 years ago) parent9a69864731c328
5 files changed+182−1
wai-extra/ChangeLog.md+1 wai-extra/Network/Wai/Middleware/Approot.hs+123 wai-extra/Network/Wai/Request.hs+16−1 wai-extra/test/Network/Wai/Middleware/ApprootSpec.hs+37 wai-extra/wai-extra.cabal+5
wai-extra/ChangeLog.md+1
@@ -3,6 +3,7 @@
33* Add appearsSecure: check if a request appears to be using SSL even in the
44 presence of reverse proxies [#362](https://github.com/yesodweb/wai/pull/362)
55* Add ForceSSL middleware [#363](https://github.com/yesodweb/wai/pull/363)
6+* Add Approot middleware
67
78## 3.0.6.1
89
wai-extra/Network/Wai/Middleware/Approot.hsnew file+123
@@ -0,0 +1,123 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+-- | Middleware for establishing the root of the application.
4+--
5+-- Many application need the ability to create URLs referring back to the
6+-- application itself. For example: generate RSS feeds or sitemaps, giving
7+-- users copy-paste links, or sending emails. In many cases, the approot can be
8+-- determined correctly from the request headers. However, some things can
9+-- prevent this, especially reverse proxies. This module provides multiple ways
10+-- of configuring approot discovery, and functions for applications to get that
11+-- approot.
12+--
13+-- Approots are structured such that they can be prepended to a string such as
14+-- @/foo/bar?baz=bin@. For example, if your application is hosted on
15+-- example.com using HTTPS, the approot would be @https://example.com@. Note
16+-- the lack of a trailing slash.
17+module Network.Wai.Middleware.Approot
18+ ( -- * Middleware
19+ approotMiddleware
20+ -- * Common providers
21+ , envFallback
22+ , envFallbackNamed
23+ , hardcoded
24+ , fromRequest
25+ -- * Functions for applications
26+ , getApproot
27+ , getApprootMay
28+ ) where
29+
30+import Control.Exception (Exception, throw)
31+import Data.ByteString (ByteString)
32+import qualified Data.ByteString as S
33+import qualified Data.ByteString.Char8 as S8
34+import Data.Maybe (fromMaybe)
35+import Data.Typeable (Typeable)
36+import qualified Data.Vault.Lazy as V
37+import Network.Wai (Request, vault, Middleware, requestHeaderHost)
38+import Network.Wai.Request (appearsSecure, guessApproot)
39+import System.Environment (getEnvironment)
40+import System.IO.Unsafe (unsafePerformIO)
41+
42+approotKey :: V.Key ByteString
43+approotKey = unsafePerformIO V.newKey
44+{-# NOINLINE approotKey #-}
45+
46+-- | The most generic version of the middleware, allowing you to provide a
47+-- function to get the approot for each request. For many use cases, one of the
48+-- helper functions provided by this module will give the necessary
49+-- functionality more conveniently.
50+--
51+-- Since 3.0.7
52+approotMiddleware :: (Request -> IO ByteString) -- ^ get the approot
53+ -> Middleware
54+approotMiddleware getRoot app req respond = do
55+ ar <- getRoot req
56+ let req' = req { vault = V.insert approotKey ar $ vault req }
57+ app req' respond
58+
59+-- | Same as @'envFallbackNamed' "APPROOT"@.
60+--
61+-- The environment variable @APPROOT@ is used by Keter, School of Haskell, and yesod-devel.
62+--
63+-- Since 3.0.7
64+envFallback :: IO Middleware
65+envFallback = envFallbackNamed "APPROOT"
66+
67+-- | Produce a middleware that takes the approot from the given environment
68+-- variable, falling back to the behavior of 'fromRequest' if the variable is
69+-- not set.
70+--
71+-- Since 3.0.7
72+envFallbackNamed :: String -> IO Middleware
73+envFallbackNamed name = do
74+ env <- getEnvironment
75+ case lookup name env of
76+ Just s -> return $ hardcoded $ S8.pack s
77+ Nothing -> return fromRequest
78+
79+-- | Hard-code the given value as the approot.
80+--
81+-- Since 3.0.7
82+hardcoded :: ByteString -> Middleware
83+hardcoded ar = approotMiddleware (const $ return ar)
84+
85+-- | Get the approot by analyzing the request. This is not a full-proof
86+-- approach, but in many common cases will work. Situations that can break this
87+-- are:
88+--
89+-- * Requests which spoof headers and imply the connection is over HTTPS
90+--
91+-- * Reverse proxies that change ports in surprising ways
92+--
93+-- * Invalid Host headers
94+--
95+-- * Reverse proxies which modify the path info
96+--
97+-- Normally trusting headers in this way is insecure, however in the case of
98+-- approot, the worst that can happen is that the client will get an incorrect
99+-- URL. If you are relying on the approot for some security-sensitive purpose,
100+-- it is highly recommended to use @hardcoded@, which cannot be spoofed.
101+--
102+-- Since 3.0.7
103+fromRequest :: Middleware
104+fromRequest = approotMiddleware (return . guessApproot)
105+
106+data ApprootMiddlewareNotSetup = ApprootMiddlewareNotSetup
107+ deriving (Show, Typeable)
108+instance Exception ApprootMiddlewareNotSetup
109+
110+-- | Get the approot set by the middleware. If the middleware is not in use,
111+-- then this function will return an exception. For a total version of the
112+-- function, see 'getApprootMay'.
113+--
114+-- Since 3.0.7
115+getApproot :: Request -> ByteString
116+getApproot = fromMaybe (throw ApprootMiddlewareNotSetup) . getApprootMay
117+
118+-- | A total version of 'getApproot', which returns 'Nothing' if the middleware
119+-- is not in use.
120+--
121+-- Since 3.0.7
122+getApprootMay :: Request -> Maybe ByteString
123+getApprootMay req = V.lookup approotKey $ vault req
wai-extra/Network/Wai/Request.hs+16−1
@@ -3,12 +3,15 @@
33
44module Network.Wai.Request
55 ( appearsSecure
6+ , guessApproot
67 ) where
78
89import Data.ByteString (ByteString)
10+import Data.Maybe (fromMaybe)
911import Network.HTTP.Types (HeaderName)
10−import Network.Wai (Request, isSecure, requestHeaders)
12+import Network.Wai (Request, isSecure, requestHeaders, requestHeaderHost)
1113
14+import qualified Data.ByteString as S
1215import qualified Data.ByteString.Char8 as C
1316
1417-- | Does this request appear to have been made over an SSL connection?
@@ -34,3 +37,15 @@ appearsSecure request = isSecure request || any (uncurry matchHeader)
3437 where
3538 matchHeader :: HeaderName -> (ByteString -> Bool) -> Bool
3639 matchHeader h f = maybe False f $ lookup h $ requestHeaders request
40+
41+-- | Guess the \"application root\" based on the given request.
42+--
43+-- The application root is the basis for forming URLs pointing at the current
44+-- application. For more information and relevant caveats, please see
45+-- "Network.Wai.Middleware.Approot".
46+--
47+-- Since 3.0.7
48+guessApproot :: Request -> ByteString
49+guessApproot req =
50+ (if appearsSecure req then "https://" else "http://") `S.append`
51+ (fromMaybe "localhost" $ requestHeaderHost req)
wai-extra/test/Network/Wai/Middleware/ApprootSpec.hsnew file+37
@@ -0,0 +1,37 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Network.Wai.Middleware.ApprootSpec
3+ ( main
4+ , spec
5+ ) where
6+
7+import Test.Hspec
8+
9+import Network.Wai.Middleware.Approot
10+import Network.Wai.Test
11+import Network.Wai
12+import Network.HTTP.Types
13+import Data.ByteString (ByteString)
14+
15+main :: IO ()
16+main = hspec spec
17+
18+spec :: Spec
19+spec = do
20+ let test name host secure headers expected = it name $ do
21+ resp <- runApp host secure headers
22+ simpleHeaders resp `shouldBe` [("Approot", expected)]
23+ test "respects host header" "foobar" False [] "http://foobar"
24+ test "respects isSecure" "foobar" True [] "https://foobar"
25+ test "respects SSL headers" "foobar" False
26+ [("HTTP_X_FORWARDED_SSL", "on")] "https://foobar"
27+
28+runApp :: ByteString -> Bool -> RequestHeaders -> IO SResponse
29+runApp host secure headers = runSession
30+ (request defaultRequest
31+ { requestHeaderHost = Just host
32+ , isSecure = secure
33+ , requestHeaders = headers
34+ }) $ fromRequest app
35+
36+ where
37+ app req respond = respond $ responseLBS status200 [("Approot", getApproot req)] ""
wai-extra/wai-extra.cabal+5
@@ -109,6 +109,7 @@ Library
109109 , streaming-commons
110110 , unix-compat
111111 , cookie
112+ , vault
112113
113114 if os(windows)
114115 cpp-options: -DWINDOWS
@@ -119,6 +120,7 @@ Library
119120 Network.Wai.Handler.SCGI
120121 Network.Wai.Middleware.AcceptOverride
121122 Network.Wai.Middleware.AddHeaders
123+ Network.Wai.Middleware.Approot
122124 Network.Wai.Middleware.Autohead
123125 Network.Wai.Middleware.CleanPath
124126 Network.Wai.Middleware.Local
@@ -147,6 +149,9 @@ test-suite spec
147149 main-is: Spec.hs
148150 other-modules: Network.Wai.TestSpec
149151 Network.Wai.ParseSpec
152+ Network.Wai.RequestSpec
153+ Network.Wai.Middleware.ApprootSpec
154+ Network.Wai.Middleware.ForceSSLSpec
150155 WaiExtraSpec
151156 build-depends: base >= 4 && < 5
152157 , wai-extra