on master help sign in
Middleware.ForceSSL

Redirect any non-secure requests to https.

commit1aaa871318f1ab50c05e37ba424728cf3e5408f2 authorpatrick brisbin <pbrisbin@gmail.com> dateWed 8 Apr 2015, 11:17 (12 years ago) parent3a2a7da
4 files changed+100−2
wai-extra/Network/Wai/Middleware/ForceSSL.hs+38 wai-extra/Network/Wai/Test.hs+5−2 wai-extra/test/Network/Wai/Middleware/ForceSSLSpec.hs+56 wai-extra/wai-extra.cabal+1
wai-extra/Network/Wai/Middleware/ForceSSL.hsnew file+38
@@ -0,0 +1,38 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+-- | Redirect non-SSL requests to https
4+module Network.Wai.Middleware.ForceSSL
5+ ( forceSSL
6+ ) where
7+
8+import Network.Wai
9+import Network.Wai.Request
10+
11+#if __GLASGOW_HASKELL__ < 710
12+import Control.Applicative ((<$>))
13+#endif
14+
15+import Data.Monoid ((<>))
16+import Network.HTTP.Types (hLocation, methodGet, status301, status307)
17+
18+import qualified Data.ByteString.Char8 as C
19+
20+-- | For requests that don't appear secure, redirect to https
21+forceSSL :: Middleware
22+forceSSL app req sendResponse =
23+ case (appearsSecure req, redirectResponse req) of
24+ (False, Just resp) -> sendResponse resp
25+ _ -> app req sendResponse
26+
27+redirectResponse :: Request -> Maybe Response
28+redirectResponse req = do
29+ (host:_) <- C.split ':' <$> requestHeaderHost req
30+
31+ return $ responseBuilder status [(hLocation, location host)] mempty
32+
33+ where
34+ location h = "https://" <> h <> rawPathInfo req <> rawQueryString req
35+
36+ status
37+ | requestMethod req == methodGet = status301
38+ | otherwise = status307
wai-extra/Network/Wai/Test.hs+5−2
@@ -32,9 +32,13 @@ module Network.Wai.Test
3232 , WaiTestFailure (..)
3333 ) where
3434
35+#if __GLASGOW_HASKELL__ < 710
36+import Control.Applicative ((<$>))
37+import Data.Monoid (mempty, mappend)
38+#endif
39+
3540import Network.Wai
3641import Network.Wai.Internal (ResponseReceived (ResponseReceived))
37−import Control.Applicative ((<$>))
3842import Control.Monad.IO.Class (liftIO)
3943import Control.Monad.Trans.Class (lift)
4044import qualified Control.Monad.Trans.State as ST
@@ -58,7 +62,6 @@ import qualified Data.ByteString as S
5862import qualified Data.Text as T
5963import qualified Data.Text.Encoding as TE
6064import Data.IORef
61−import Data.Monoid (mempty, mappend)
6265import Data.Time.Clock (getCurrentTime)
6366
6467type Session = ReaderT Application (ST.StateT ClientState IO)
wai-extra/test/Network/Wai/Middleware/ForceSSLSpec.hsnew file+56
@@ -0,0 +1,56 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Network.Wai.Middleware.ForceSSLSpec
3+ ( main
4+ , spec
5+ ) where
6+
7+import Test.Hspec
8+
9+import Network.Wai.Middleware.ForceSSL
10+
11+import Data.ByteString (ByteString)
12+import Data.Monoid ((<>))
13+import Network.HTTP.Types (methodPost, status200, status301, status307)
14+import Network.Wai
15+import Network.Wai.Test
16+
17+main :: IO ()
18+main = hspec spec
19+
20+spec :: Spec
21+spec = describe "forceSSL" $ do
22+ let host = "example.com"
23+
24+ it "redirects non-https requests to https" $ do
25+ resp <- runApp host forceSSL defaultRequest
26+
27+ simpleStatus resp `shouldBe` status301
28+ simpleHeaders resp `shouldBe` [("Location", "https://" <> host)]
29+
30+ it "redirects with 307 in the case of a non-GET request" $ do
31+ resp <- runApp host forceSSL defaultRequest
32+ { requestMethod = methodPost }
33+
34+ simpleStatus resp `shouldBe` status307
35+ simpleHeaders resp `shouldBe` [("Location", "https://" <> host)]
36+
37+ it "does not redirect already-secure requests" $ do
38+ resp <- runApp host forceSSL defaultRequest { isSecure = True }
39+
40+ simpleStatus resp `shouldBe` status200
41+
42+ it "preserves the original path and query string" $ do
43+ resp <- runApp host forceSSL defaultRequest
44+ { rawPathInfo = "/foo/bar"
45+ , rawQueryString = "?baz=bat"
46+ }
47+
48+ simpleHeaders resp `shouldBe`
49+ [("Location", "https://" <> host <> "/foo/bar?baz=bat")]
50+
51+runApp :: ByteString -> Middleware -> Request -> IO SResponse
52+runApp host mw req = runSession
53+ (request req { requestHeaderHost = Just $ host <> ":80" }) $ mw app
54+
55+ where
56+ app _ respond = respond $ responseLBS status200 [] ""
wai-extra/wai-extra.cabal+1
@@ -131,6 +131,7 @@ Library
131131 Network.Wai.Middleware.Vhost
132132 Network.Wai.Middleware.HttpAuth
133133 Network.Wai.Middleware.StreamFile
134+ Network.Wai.Middleware.ForceSSL
134135 Network.Wai.Parse
135136 Network.Wai.Request
136137 Network.Wai.UrlMap