on master help sign in
Merge pull request #575 from dbaynard/feat/extend-rewrite-middleware

Feat/extend rewrite middleware

commit8891eddd5b2545158c46896eca1590463a8d4b05 authorMichael Snoyman <michael@snoyman.com> dateFri 26 Aug 2016, 05:42 (10 years ago) parentf670250f021252
1 file changed+305−14
wai-extra/Network/Wai/Middleware/Rewrite.hs+305−14
wai-extra/Network/Wai/Middleware/Rewrite.hs+305−14
@@ -1,32 +1,323 @@
1+{-# LANGUAGE TupleSections #-}
2+{-# LANGUAGE CPP #-}
3+
14module Network.Wai.Middleware.Rewrite
2− ( rewrite, rewritePure
5+ ( -- * How to use this module
6+ -- $howto
7+
8+ -- ** A note on semantics
9+
10+ -- $semantics
11+
12+ -- ** Paths and Queries
13+
14+ -- $pathsandqueries
15+ PathsAndQueries
16+
17+ -- ** An example rewriting paths with queries
18+
19+ -- $takeover
20+
21+ -- ** Upgrading from wai-extra ≤ 3.0.16.1
22+
23+ -- $upgrading
24+
25+ -- * 'Middleware'
26+
27+ -- ** Recommended functions
28+ , rewriteWithQueries
29+ , rewritePureWithQueries
30+
31+ -- ** Deprecated
32+ , rewrite
33+ , rewritePure
34+
35+ -- * Operating on 'Request's
36+
37+ , rewriteRequest
38+ , rewriteRequestPure
339 ) where
440
541import Network.Wai
642import Control.Monad.IO.Class (liftIO)
743import Data.Text (Text)
44+import Data.Functor.Identity (Identity(..))
845import qualified Data.Text.Encoding as TE
946import qualified Data.Text as T
1047import Network.HTTP.Types as H
1148
49+-- GHC ≤ 7.10 does not export Applicative functions from the prelude.
50+#if __GLASGOW_HASKELL__ <= 710
51+import Control.Applicative
52+#endif
1253
13−-- | rewrite based on your own conversion rules
14−rewrite :: ([Text] -> H.RequestHeaders -> IO [Text]) -> Middleware
15−rewrite convert app req sendResponse = do
16− newPathInfo <- liftIO $ convert (pathInfo req) (requestHeaders req)
17− let rawPInfo = TE.encodeUtf8 $ T.intercalate "/" newPathInfo
18− app req { pathInfo = newPathInfo, rawPathInfo = rawPInfo } sendResponse
54+-- $howto
55+-- This module provides 'Middleware' to rewrite URL paths. It also provides
56+-- functions that will convert a 'Request' to a modified 'Request'.
57+-- Both operations require a function that takes URL parameters and
58+-- headers, and returns new URL parameters. Parameters are pieces of URL
59+-- paths and query parameters.
60+--
61+-- If you are a new user of the library, use 'rewriteWithQueries' or
62+-- 'rewritePureWithQueries' for middleware. For modifying 'Request's
63+-- directly, use 'rewriteRequest' or 'rewriteRequestPure'.
64+
65+-- $semantics
66+--
67+-- Versions of this library in wai-extra ≤ 3.0.16.1 exported only
68+-- 'rewrite' and 'rewritePure' and both modified 'rawPathInfo' of the
69+-- underlying requests. Such modification has been proscribed. The
70+-- semantics of these functions have not changed; instead the recommended
71+-- approach is to use 'rewriteWithQueries' and 'rewritePureWithQueries'.
72+-- The new functions are slightly different, as described in the section
73+-- on upgrading; code for previous library versions can be upgraded with
74+-- a single change, and as the type of the new function is different the
75+-- compiler will indicate where this change must be made.
76+--
77+-- The 'rewriteRequest' and 'rewriteRequestPure' functions use the new
78+-- semantics, too.
1979
20−-- | rewrite based on your own conversion rules
21−-- Example convert function:
80+-- $pathsandqueries
81+--
82+-- This library defines the type synonym `PathsAndQueries` to make code
83+-- handling paths and queries easier to read.
84+--
85+-- /e.g./ /\/foo\/bar/ would look like
86+--
87+-- > ["foo", "bar"] :: Text
88+--
89+-- /?bar=baz/ would look like
90+--
91+-- > [("bar", Just "baz")] :: QueryText
92+--
93+-- Together,
94+--
95+-- /\/foo?bar=baz/ would look like
96+--
97+-- > (["foo"],[("bar", Just "baz")]) :: PathsAndQueries
2298
23−-- staticConvert :: [Text] -> H.RequestHeaders -> [Text]
24−-- staticConvert pieces _ = piecesConvert pieces
99+-- $takeover
100+-- Let’s say we want to replace a website written in PHP with one written
101+-- using WAI. We’ll use the
102+-- <https://hackage.haskell.org/package/http-reverse-proxy http-reverse-proxy>
103+-- package to serve the old
104+-- site from the new site, but there’s a problem. The old site uses pages like
105+--
106+-- @
107+-- index.php?page=/page/
108+-- @
109+--
110+-- whereas the new site would look like
111+--
112+-- @
113+-- index\//page/
114+-- @
115+--
116+-- In doing this, we want to separate the migration code from our new
117+-- website. So we’d like to handle links internally using the path
118+-- formulation, but externally have the old links still work.
119+--
120+-- Therefore, we will use middleware ('rewritePureWithQueries') from this
121+-- module to rewrite incoming requests from the query formulation to the
122+-- paths formulation.
123+--
124+-- > {-# LANGUAGE ViewPatterns #-}
125+-- >
126+-- > rewritePathFromPhp :: Middleware
127+-- > rewritePathFromPhp = rewritePureWithQueries pathFromPhp
128+-- >
129+-- > pathFromPhp :: PathsAndQueries -> H.RequestHeaders -> PathsAndQueries
130+-- > pathFromPhp (pieces, queries) _ = piecesConvert pieces queries
131+-- > where
132+-- > piecesConvert :: [Text] -> H.Query -> PathsAndQueries
133+-- > piecesConvert ["index.php"] qs@(join . lookup "page" -> Just page) =
134+-- > ( ["index", TE.decodeUtf8With TE.lenientDecode page]
135+-- > , delete ("page", pure page) qs
136+-- > )
137+-- > piecesConvert ps qs = (ps, qs)
138+--
139+-- On the other side, we will use 'rewriteRequestPure' to rewrite outgoing
140+-- requests to the original website from the reverse proxy code (using the
141+-- 'Network.HTTP.ReverseProxy.WPRModifiedRequest' or
142+-- 'Network.HTTP.ReverseProxy.WPRModifiedRequestSecure' constructors. Note,
143+-- these links will only work if the haddock documentation for
144+-- <https://hackage.haskell.org/package/http-reverse-proxy http-reverse-proxy>
145+-- is installed).
146+--
147+-- > rewritePhpFromPath :: Request -> Request
148+-- > rewritePhpFromPath = rewriteRequestPure phpFromPath
149+-- >
150+-- > phpFromPath :: PathsAndQueries -> H.RequestHeaders -> PathsAndQueries
151+-- > phpFromPath (pieces, queries) _ = piecesConvert pieces queries
152+-- > where
153+-- > piecesConvert :: [Text] -> H.Query -> PathsAndQueries
154+-- > piecesConvert ["index", page] qs = ( ["index.php"], ("page", pure . TE.encodeUtf8 $ page) : qs )
155+-- > piecesConvert ps qs = (ps, qs)
156+--
157+-- For the whole example, see
158+-- <https://gist.github.com/dbaynard/c844d0df124f68ec8b6da152c581ce6d>.
159+
160+-- $upgrading
161+-- It is quite simple to upgrade from 'rewrite' and 'rewritePure', to
162+-- 'rewriteWithQueries' and 'rewritePureWithQueries'.
163+-- Insert 'Data.Bifunctor.first', which specialises to
164+--
165+-- @
166+-- 'Data.Bifunctor.first' :: (['Text'] -> ['Text']) -> 'PathsAndQueries' -> 'PathsAndQueries'
167+-- @
168+--
169+-- as the following example demonstrates.
170+--
171+-- Old versions of the libary could only handle path pieces, not queries.
172+-- This could have been supplied to 'rewritePure'.
173+--
174+-- @
175+-- staticConvert' :: [Text] -> H.RequestHeaders -> [Text]
176+-- staticConvert' pieces _ = piecesConvert pieces
25177-- where
26178-- piecesConvert [] = ["static", "html", "pages.html"]
27179-- piecesConvert route@("pages":_) = "static":"html":route
180+-- @
181+--
182+-- Instead, use this function, supplied to 'rewritePureWithQueries'.
183+--
184+-- @
185+-- staticConvert :: 'PathsAndQueries' -> H.RequestHeaders -> 'PathsAndQueries'
186+-- staticConvert pathsAndQueries _ = 'Data.Bifunctor.first' piecesConvert pathsAndQueries
187+-- where
188+-- piecesConvert [] = ["static", "html", "pages.html"]
189+-- piecesConvert route@("pages":_) = "static":"html":route
190+-- @
191+--
192+-- The former formulation is deprecated for two reasons:
193+--
194+-- 1. The original formulation of 'rewrite' modified 'rawPathInfo', which
195+-- is deprecated behaviour.
196+--
197+-- 2. The original formulation did not allow query parameters to
198+-- influence the path.
199+--
200+-- Concerning the first point, take care with semantics of your program when
201+-- upgrading as the upgraded functions no longer modify 'rawPathInfo'.
202+
203+--------------------------------------------------
204+-- * Types
205+--------------------------------------------------
206+
207+-- | A tuple of the path sections as ['Text'] and query parameters as
208+-- 'H.Query'. This makes writing type signatures for the conversion
209+-- function far more pleasant.
210+--
211+-- Note that this uses 'H.Query' not 'H.QueryText' to more accurately
212+-- reflect the paramaters that can be supplied in URLs. It may be safe to
213+-- treat parameters as text; use the 'H.queryToQueryText' and
214+-- 'H.queryTextToQuery' functions to interconvert.
215+type PathsAndQueries = ([Text], H.Query)
216+
217+--------------------------------------------------
218+-- * Rewriting 'Middleware'
219+--------------------------------------------------
220+
221+-- | Rewrite based on your own conversion function for paths only, to be
222+-- supplied by users of this library (with the conversion operating in 'IO').
223+--
224+-- For new code, use 'rewriteWithQueries' instead.
225+rewrite :: ([Text] -> H.RequestHeaders -> IO [Text]) -> Middleware
226+rewrite convert app req sendResponse = do
227+ let convertIO = pathsOnly . curry $ liftIO . uncurry convert
228+ newReq <- rewriteRequestRawM convertIO req
229+ app newReq sendResponse
230+{-# WARNING rewrite [
231+ "This modifies the 'rawPathInfo' field of a 'Request'."
232+ , " This is not recommended behaviour; it is however how"
233+ , " this function has worked in the past."
234+ , " Use 'rewriteWithQueries' instead"] #-}
235+
236+-- | Rewrite based on pure conversion function for paths only, to be
237+-- supplied by users of this library.
238+--
239+-- For new code, use 'rewritePureWithQueries' instead.
28240rewritePure :: ([Text] -> H.RequestHeaders -> [Text]) -> Middleware
29241rewritePure convert app req =
30− let pInfo = convert (pathInfo req) (requestHeaders req)
31− rawPInfo = TE.encodeUtf8 $ T.intercalate "/" pInfo
32− in app req { pathInfo = pInfo, rawPathInfo = rawPInfo }
242+ let convertPure = pathsOnly . curry $ Identity . uncurry convert
243+ newReq = runIdentity $ rewriteRequestRawM convertPure req
244+ in app newReq
245+{-# WARNING rewritePure [
246+ "This modifies the 'rawPathInfo' field of a 'Request'."
247+ , " This is not recommended behaviour; it is however how"
248+ , " this function has worked in the past."
249+ , " Use 'rewritePureWithQueries' instead"] #-}
250+
251+-- | Rewrite based on your own conversion function for paths and queries.
252+-- This function is to be supplied by users of this library, and operates
253+-- in 'IO'.
254+rewriteWithQueries :: (PathsAndQueries -> H.RequestHeaders -> IO PathsAndQueries)
255+ -> Middleware
256+rewriteWithQueries convert app req sendResponse = do
257+ newReq <- rewriteRequestM convert req
258+ app newReq sendResponse
259+
260+-- | Rewrite based on pure conversion function for paths and queries. This
261+-- function is to be supplied by users of this library.
262+rewritePureWithQueries :: (PathsAndQueries -> H.RequestHeaders -> PathsAndQueries)
263+ -> Middleware
264+rewritePureWithQueries convert app req = app $ rewriteRequestPure convert req
265+
266+--------------------------------------------------
267+-- * Modifying 'Request's directly
268+--------------------------------------------------
269+
270+-- | Modify a 'Request' using the supplied function in 'IO'. This is suitable for
271+-- the reverse proxy example.
272+rewriteRequest :: (PathsAndQueries -> H.RequestHeaders -> IO PathsAndQueries)
273+ -> Request -> IO Request
274+rewriteRequest convert req =
275+ let convertIO = curry $ liftIO . uncurry convert
276+ in rewriteRequestRawM convertIO req
277+
278+-- | Modify a 'Request' using the pure supplied function. This is suitable for
279+-- the reverse proxy example.
280+rewriteRequestPure :: (PathsAndQueries -> H.RequestHeaders -> PathsAndQueries)
281+ -> Request -> Request
282+rewriteRequestPure convert req =
283+ let convertPure = curry $ Identity . uncurry convert
284+ in runIdentity $ rewriteRequestRawM convertPure req
285+
286+--------------------------------------------------
287+-- * Helper functions
288+--------------------------------------------------
289+
290+-- | This helper function factors out the common behaviour of rewriting requests.
291+rewriteRequestM :: (Applicative m, Monad m)
292+ => (PathsAndQueries -> H.RequestHeaders -> m PathsAndQueries)
293+ -> Request -> m Request
294+rewriteRequestM convert req = do
295+ (pInfo, qByteStrings) <- curry convert (pathInfo req) (queryString req) (requestHeaders req)
296+ pure req {pathInfo = pInfo, queryString = qByteStrings}
297+
298+-- | This helper function preserves the semantics of wai-extra ≤ 3.0, in
299+-- which the rewrite functions modify the 'rawPathInfo' parameter. Note
300+-- that this has not been extended to modify the 'rawQueryInfo' as
301+-- modifying either of these values has been deprecated.
302+rewriteRequestRawM :: (Applicative m, Monad m)
303+ => (PathsAndQueries -> H.RequestHeaders -> m PathsAndQueries)
304+ -> Request -> m Request
305+rewriteRequestRawM convert req = do
306+ newReq <- rewriteRequestM convert req
307+ let rawPInfo = TE.encodeUtf8 . T.intercalate "/" . pathInfo $ newReq
308+ pure newReq { rawPathInfo = rawPInfo }
309+{-# WARNING rewriteRequestRawM [
310+ "This modifies the 'rawPathInfo' field of a 'Request'."
311+ , " This is not recommended behaviour; it is however how"
312+ , " this function has worked in the past."
313+ , " Use 'rewriteRequestM' instead"] #-}
314+
315+-- | Produce a function that works on 'PathsAndQueries' from one working
316+-- only on paths. This is not exported, as it is only needed to handle
317+-- code written for versions ≤ 3.0 of the library; see the
318+-- example above using 'Data.Bifunctor.first' to do something similar.
319+pathsOnly :: (Applicative m, Monad m)
320+ => ([Text] -> H.RequestHeaders -> m [Text])
321+ -> PathsAndQueries -> H.RequestHeaders -> m PathsAndQueries
322+pathsOnly convert psAndQs headers = (,[]) <$> convert (fst psAndQs) headers
323+{-# INLINE pathsOnly #-}