There doesn't seem to be any use of decodePathSegments
or urlDecode
in Scotty so far.
This may make sense from the point of view of leaving decoding up to the user and allowing the specification of the route exactly, but there will probably be some surprise when the user tries to match:
get "/users/:user" $ do
user <- param "user"
if user == "John Doe" then (html "Hi John!")
else next
...but instead needs to test against "John%20Doe".
Similarly, if this is used for captured text, then there will be %xx codes throughout the HTML, DB, etc.
I'm not sure the best way to go about making the change for this so that it behaves sanely for RoutePatterns other than Keyword (probably leave decoding up to the user), but for Keword types, the best option I can see would be to pass another parameter to matchRoute - the result of decodePathSegments $ rawPathInfo req
.
This would eliminate the need for explicit recursion in matchRoute as well as passing decoded path segments to be matched against in order to have predictable behaviour.
Other RoutePattern types should probably be left up to the user to decode as urlDecoding the entire path without splitting on "/" would lead to the opportunity to insert extra slashes through %2F and similar concerns, breaking general matching logic.
Fun note - I came across this when trying to perform an HTML injection as a test by allowing direct insertion of url keywords with preEscapedLazyText
. It turns out this is harder than it should be.
Example Code:
route :: StdMethod -> RoutePattern -> ActionM () -> Middleware
route method path action app req =
if Right method == parseMethod (requestMethod req)
then case matchRoute path (strictByteStringToLazyText $ rawPathInfo req) (fmap TL.fromStrict $ decodePathSegments $ rawPathInfo req) of
Just captures -> do
env <- mkEnv req captures
res <- lift $ runAction env action
maybe tryNext return res
Nothing -> tryNext
else tryNext
where tryNext = app req
matchRoute (Keyword "/") _ [] = Just []
matchRoute (Keyword pat) _ segments = fmap catMaybes $ sequence $ zipWithLengthMatch go splitpat segments
where
splitpat = (drop 1 $ T.split (=='/') pat)
go key val = test (T.uncons key)
where
test Nothing | key == val = Just Nothing -- Empty val must equal empty key
test (Just (k,ks)) | k == ':' = Just $ Just (ks,val) -- Capture
| key == val = Just Nothing -- No capture
test _ = Nothing -- Fall back to nothing for all other cases
zipWithLengthMatch :: ( a -> b -> Maybe (Maybe c) ) -> [ a ] -> [ b ] -> [ Maybe (Maybe c) ]
zipWithLengthMatch _ [] [] = [ ]
zipWithLengthMatch f (x:xs) (y:ys) = f x y : zipWithLengthMatch f xs ys
zipWithLengthMatch _ _ _ = [ Nothing ] -- Fail