From c2578825515b0a983ededf5a31aa6e16deb133d7 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Sun, 31 Mar 2024 00:32:52 +0100 Subject: [PATCH 1/7] Add tests for url-decoding in query and form parameters --- test/Web/ScottySpec.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Web/ScottySpec.hs b/test/Web/ScottySpec.hs index 80217af..bb538c0 100644 --- a/test/Web/ScottySpec.hs +++ b/test/Web/ScottySpec.hs @@ -255,6 +255,8 @@ spec = do withApp (Scotty.get "/search" $ queryParam "query" >>= text) $ do it "returns query parameter with given name" $ do get "/search?query=haskell" `shouldRespondWith` "haskell" + it "decodes URL-encoding" $ do + get "/search?query=Kurf%C3%BCrstendamm" `shouldRespondWith` "Kurfürstendamm" withApp (Scotty.matchAny "/search" (do v <- queryParam "query" json (v :: Int) )) $ do @@ -278,6 +280,9 @@ spec = do it "replaces non UTF-8 bytes with Unicode replacement character" $ do postForm "/search" "query=\xe9" `shouldRespondWith` "\xfffd" + + it "decodes URL-encoding" $ do + postForm "/search" "query=Kurf%C3%BCrstendamm" `shouldRespondWith` "Kurfürstendamm" withApp (Scotty.post "/search" (do v <- formParam "query" json (v :: Int))) $ do From 96b23f8fb305a1210fb5c0e74c2a75091fe94cdd Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Sun, 31 Mar 2024 01:29:00 +0100 Subject: [PATCH 2/7] Add formData for parsing forms into records Also add some tests for formData --- Web/Scotty.hs | 9 ++++++++- Web/Scotty/Action.hs | 22 ++++++++++++++++++++++ Web/Scotty/Internal/Types.hs | 1 + Web/Scotty/Trans.hs | 2 +- scotty.cabal | 2 ++ test/Web/ScottySpec.hs | 21 ++++++++++++++++++++- 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Web/Scotty.hs b/Web/Scotty.hs index 414eb3f..cfb0729 100644 --- a/Web/Scotty.hs +++ b/Web/Scotty.hs @@ -25,7 +25,7 @@ module Web.Scotty , capture, regex, function, literal -- ** Accessing the Request and its fields , request, header, headers, body, bodyReader - , jsonData + , jsonData, formData -- ** Accessing Path, Form and Query Parameters , param, params , pathParam, captureParam, formParam, queryParam @@ -71,6 +71,7 @@ import Network.Wai (Application, Middleware, Request, StreamingBody) import Network.Wai.Handler.Warp (Port) import qualified Network.Wai.Parse as W +import Web.FormUrlEncoded (FromForm) import Web.Scotty.Internal.Types (ScottyT, ActionT, ErrorHandler, Param, RoutePattern, Options, defaultOptions, File, Kilobytes, ScottyState, defaultScottyState, ScottyException, StatusError(..), Content(..)) import UnliftIO.Exception (Handler(..), catch) import Web.Scotty.Cookie (setSimpleCookie,getCookie,getCookies,deleteCookie,makeSimpleCookie) @@ -276,6 +277,12 @@ bodyReader = Trans.bodyReader jsonData :: FromJSON a => ActionM a jsonData = Trans.jsonData +-- | Parse the request body as @x-www-form-urlencoded@ form data and return it. Raises an exception if parse is unsuccessful. +-- +-- NB: uses 'body' internally +formData :: FromForm a => ActionM a +formData = Trans.formData + -- | Get a parameter. First looks in captures, then form data, then query parameters. -- -- * Raises an exception which can be caught by 'catch' if parameter is not found. diff --git a/Web/Scotty/Action.hs b/Web/Scotty/Action.hs index e739224..6c3ed4b 100644 --- a/Web/Scotty/Action.hs +++ b/Web/Scotty/Action.hs @@ -23,6 +23,7 @@ module Web.Scotty.Action , liftAndCatchIO , json , jsonData + , formData , next , param , pathParam @@ -101,6 +102,7 @@ import qualified Network.Wai.Parse as W (FileInfo(..), ParseRequestBodyOptions, import Numeric.Natural +import Web.FormUrlEncoded (FromForm, urlDecodeAsForm) import Web.Scotty.Internal.Types import Web.Scotty.Util (mkResponse, addIfNotPresent, add, replace, lazyTextToStrictByteString, decodeUtf8Lenient) import UnliftIO.Exception (Handler(..), catch, catches, throwIO) @@ -168,6 +170,13 @@ scottyExceptionHandler = Handler $ \case , "Body: " <> bs , "Error: " <> BL.fromStrict (encodeUtf8 err) ] + MalformedForm bs err -> do + status status400 + raw $ BL.unlines + [ "formData: malformed" + , "Body: " <> bs + , "Error: " <> BL.fromStrict (encodeUtf8 err) + ] PathParameterNotFound k -> do status status500 text $ T.unwords [ "Path parameter", k, "not found"] @@ -354,6 +363,19 @@ jsonData = do A.Error err -> throwIO $ FailedToParseJSON b $ T.pack err A.Success a -> return a +-- | Parse the request body as @x-www-form-urlencoded@ form data and return it. +-- +-- The form is parsed using 'urlDecodeAsForm'. If that returns 'Left', the +-- status is set to 400 and an exception is thrown. +-- +-- NB : Internally this uses 'body'. +formData :: (FromForm a, MonadIO m) => ActionT m a +formData = do + b <- body + case urlDecodeAsForm b of + Left err -> throwIO $ MalformedForm b err + Right value -> return value + -- | Get a parameter. First looks in captures, then form data, then query parameters. -- -- * Raises an exception which can be caught by 'catch' if parameter is not found. diff --git a/Web/Scotty/Internal/Types.hs b/Web/Scotty/Internal/Types.hs index d0e3571..c591241 100644 --- a/Web/Scotty/Internal/Types.hs +++ b/Web/Scotty/Internal/Types.hs @@ -147,6 +147,7 @@ data ScottyException = RequestTooLarge | MalformedJSON LBS8.ByteString T.Text | FailedToParseJSON LBS8.ByteString T.Text + | MalformedForm LBS8.ByteString T.Text | PathParameterNotFound T.Text | QueryParameterNotFound T.Text | FormFieldNotFound T.Text diff --git a/Web/Scotty/Trans.hs b/Web/Scotty/Trans.hs index 8188c0b..159b838 100644 --- a/Web/Scotty/Trans.hs +++ b/Web/Scotty/Trans.hs @@ -30,7 +30,7 @@ module Web.Scotty.Trans , capture, regex, function, literal -- ** Accessing the Request and its fields , request, Lazy.header, Lazy.headers, body, bodyReader - , jsonData + , jsonData, formData -- ** Accessing Path, Form and Query Parameters , param, params diff --git a/scotty.cabal b/scotty.cabal index bc85be8..81d58d5 100644 --- a/scotty.cabal +++ b/scotty.cabal @@ -77,6 +77,7 @@ Library case-insensitive >= 1.0.0.1 && < 1.3, cookie >= 0.4, exceptions >= 0.7 && < 0.11, + http-api-data >= 0.5.1, http-types >= 0.9.1 && < 0.13, monad-control >= 1.0.0.3 && < 1.1, mtl >= 2.1.2 && < 2.4, @@ -114,6 +115,7 @@ test-suite spec directory, hspec == 2.*, hspec-wai >= 0.6.3, + http-api-data, http-types, lifted-base, network, diff --git a/test/Web/ScottySpec.hs b/test/Web/ScottySpec.hs index bb538c0..956d9e6 100644 --- a/test/Web/ScottySpec.hs +++ b/test/Web/ScottySpec.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE OverloadedStrings, CPP, ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings, CPP, ScopedTypeVariables, DeriveGeneric #-} module Web.ScottySpec (main, spec) where import Test.Hspec @@ -9,18 +9,22 @@ import Control.Applicative import Control.Monad import Data.Char import Data.String +import Data.Text.Lazy (Text) import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TLE import Data.Time (UTCTime(..)) import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (secondsToDiffTime) +import GHC.Generics (Generic) + import Network.HTTP.Types import Network.Wai (Application, Request(queryString), responseLBS) import Network.Wai.Parse (defaultParseRequestBodyOptions) import qualified Control.Exception.Lifted as EL import qualified Control.Exception as E +import Web.FormUrlEncoded (FromForm) import Web.Scotty as Scotty hiding (get, post, put, patch, delete, request, options) import qualified Web.Scotty as Scotty import qualified Web.Scotty.Cookie as SC (getCookie, setSimpleCookie, deleteCookie) @@ -41,6 +45,13 @@ main = hspec spec availableMethods :: [StdMethod] availableMethods = [GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS] +data SearchForm = SearchForm + { sfQuery :: Text + , sfYear :: Int + } deriving (Generic) + +instance FromForm SearchForm where + spec :: Spec spec = do let withApp = with . scottyApp @@ -270,6 +281,14 @@ spec = do ) $ do it "catches a ScottyException" $ do get "/search?query=potato" `shouldRespondWith` 200 { matchBody = "z"} + + describe "formData" $ do + withApp (Scotty.post "/search" $ formData >>= (text . sfQuery)) $ do + it "decodes the form" $ do + postHtmlForm "/search" [("sfQuery", "Haskell"), ("sfYear", "2024")] `shouldRespondWith` "Haskell" + + it "returns 400 when the form can't is malformed" $ do + postHtmlForm "/search" [("sfQuery", "Haskell")] `shouldRespondWith` 400 describe "formParam" $ do let From 17d7a1c04ca9275a0170515373c3d29a39242a4d Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Sun, 31 Mar 2024 01:36:12 +0100 Subject: [PATCH 3/7] Add test for url-encoding in formData Also move postForm helper function to top level --- test/Web/.ScottySpec.hs.swp | Bin 0 -> 61440 bytes test/Web/ScottySpec.hs | 22 ++++++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 test/Web/.ScottySpec.hs.swp diff --git a/test/Web/.ScottySpec.hs.swp b/test/Web/.ScottySpec.hs.swp new file mode 100644 index 0000000000000000000000000000000000000000..5b2456316cc0be264885d353b1c8a457b6755baf GIT binary patch literal 61440 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, shouldRespondWith, postHtmlForm, matchHeaders, matchBody, matchStatus) +import Test.Hspec.Wai (WaiSession, with, request, get, post, put, patch, delete, options, (<:>), shouldRespondWith, matchHeaders, matchBody, matchStatus) import Test.Hspec.Wai.Extra (postMultipartForm, FileMeta(..)) import Control.Applicative @@ -21,6 +21,7 @@ import GHC.Generics (Generic) import Network.HTTP.Types import Network.Wai (Application, Request(queryString), responseLBS) import Network.Wai.Parse (defaultParseRequestBodyOptions) +import Network.Wai.Test (SResponse) import qualified Control.Exception.Lifted as EL import qualified Control.Exception as E @@ -34,6 +35,7 @@ import Control.Concurrent.Async (withAsync) import Control.Exception (bracketOnError) import qualified Data.ByteString as BS import Data.ByteString (ByteString) +import qualified Data.ByteString.Lazy as LBS import Network.Socket (Family(..), SockAddr(..), Socket, SocketOption(..), SocketType(..), bind, close, connect, listen, maxListenQueue, setSocketOption, socket) import Network.Socket.ByteString (send, recv) import System.Directory (removeFile) @@ -52,6 +54,9 @@ data SearchForm = SearchForm instance FromForm SearchForm where +postForm :: ByteString -> LBS.ByteString -> WaiSession st SResponse +postForm p = request "POST" p [("Content-Type","application/x-www-form-urlencoded")] + spec :: Spec spec = do let withApp = with . scottyApp @@ -285,14 +290,15 @@ spec = do describe "formData" $ do withApp (Scotty.post "/search" $ formData >>= (text . sfQuery)) $ do it "decodes the form" $ do - postHtmlForm "/search" [("sfQuery", "Haskell"), ("sfYear", "2024")] `shouldRespondWith` "Haskell" + postForm "/search" "sfQuery=Haskell&sfYear=2024" `shouldRespondWith` "Haskell" + + it "decodes URL-encoding" $ do + postForm "/search" "sfQuery=Kurf%C3%BCrstendamm&sfYear=2024" `shouldRespondWith` "Kurfürstendamm" it "returns 400 when the form can't is malformed" $ do - postHtmlForm "/search" [("sfQuery", "Haskell")] `shouldRespondWith` 400 + postForm "/search" "sfQuery=Haskell" `shouldRespondWith` 400 describe "formParam" $ do - let - postForm p bdy = request "POST" p [("Content-Type","application/x-www-form-urlencoded")] bdy withApp (Scotty.post "/search" $ formParam "query" >>= text) $ do it "returns form parameter with given name" $ do postForm "/search" "query=haskell" `shouldRespondWith` "haskell" @@ -378,7 +384,7 @@ spec = do describe "filesOpts" $ do let - postForm = postMultipartForm "/files" "ABC123" [ + postMpForm = postMultipartForm "/files" "ABC123" [ (FMFile "file1.txt", "text/plain;charset=UTF-8", "first_file", "xxx"), (FMFile "file2.txt", "text/plain;charset=UTF-8", "second_file", "yyy") ] @@ -388,13 +394,13 @@ spec = do withApp (Scotty.post "/files" processForm ) $ do it "loads uploaded files in memory" $ do - postForm `shouldRespondWith` 200 { matchBody = "2"} + postMpForm `shouldRespondWith` 200 { matchBody = "2"} context "preserves the body of a POST request even after 'next' (#147)" $ do withApp (do Scotty.post "/files" next Scotty.post "/files" processForm) $ do it "loads uploaded files in memory" $ do - postForm `shouldRespondWith` 200 { matchBody = "2"} + postMpForm `shouldRespondWith` 200 { matchBody = "2"} describe "text" $ do From 811fbcea315c654b839b27835a49ee62c0e6d349 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Mon, 1 Apr 2024 00:08:36 +0200 Subject: [PATCH 4/7] Remove vim swap file and fix typo --- test/Web/.ScottySpec.hs.swp | Bin 61440 -> 0 bytes test/Web/ScottySpec.hs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 test/Web/.ScottySpec.hs.swp diff --git a/test/Web/.ScottySpec.hs.swp b/test/Web/.ScottySpec.hs.swp deleted file mode 100644 index 5b2456316cc0be264885d353b1c8a457b6755baf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 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 Date: Tue, 2 Apr 2024 22:07:31 +0200 Subject: [PATCH 5/7] Add changes to changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 409c788..80575ef 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ * Fixed cookie example from `Cookie` module documentation. `getCookie` Function would return strict variant of `Text`. Will convert it into lazy variant using `fromStrict`. * Exposed simple functions of `Cookie` module via `Web.Scotty` & `Web.Scotty.Trans`. +* Add tests for URL encoding of query parameters and form parameters. Add `formData` action for decoding `FromForm` instances (#321). ### Breaking changes * Remove dependency on data-default class (#386). We have been exporting constants for default config values since 0.20, and this dependency was simply unnecessary. From 21899890e6bf34d099c22949cc7f43771555e3fc Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Thu, 11 Apr 2024 00:30:07 +0200 Subject: [PATCH 6/7] Reimplement `formData` through `formParams` Add `unordered-containers` dependency for working with `HashMap` --- Web/Scotty/Action.hs | 25 ++++++++++++++++--------- Web/Scotty/Internal/Types.hs | 2 +- scotty.cabal | 1 + 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Web/Scotty/Action.hs b/Web/Scotty/Action.hs index 6c3ed4b..580d841 100644 --- a/Web/Scotty/Action.hs +++ b/Web/Scotty/Action.hs @@ -80,7 +80,9 @@ import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.CaseInsensitive as CI import Data.Traversable (for) +import qualified Data.HashMap.Strict as HashMap import Data.Int +import Data.List (foldl') import Data.Maybe (maybeToList) import qualified Data.Text as T import Data.Text.Encoding as STE @@ -102,7 +104,7 @@ import qualified Network.Wai.Parse as W (FileInfo(..), ParseRequestBodyOptions, import Numeric.Natural -import Web.FormUrlEncoded (FromForm, urlDecodeAsForm) +import Web.FormUrlEncoded (Form(..), FromForm(..)) import Web.Scotty.Internal.Types import Web.Scotty.Util (mkResponse, addIfNotPresent, add, replace, lazyTextToStrictByteString, decodeUtf8Lenient) import UnliftIO.Exception (Handler(..), catch, catches, throwIO) @@ -170,11 +172,10 @@ scottyExceptionHandler = Handler $ \case , "Body: " <> bs , "Error: " <> BL.fromStrict (encodeUtf8 err) ] - MalformedForm bs err -> do + MalformedForm err -> do status status400 raw $ BL.unlines [ "formData: malformed" - , "Body: " <> bs , "Error: " <> BL.fromStrict (encodeUtf8 err) ] PathParameterNotFound k -> do @@ -367,14 +368,20 @@ jsonData = do -- -- The form is parsed using 'urlDecodeAsForm'. If that returns 'Left', the -- status is set to 400 and an exception is thrown. --- --- NB : Internally this uses 'body'. -formData :: (FromForm a, MonadIO m) => ActionT m a +formData :: (FromForm a, MonadUnliftIO m) => ActionT m a formData = do - b <- body - case urlDecodeAsForm b of - Left err -> throwIO $ MalformedForm b err + form <- paramListToForm <$> formParams + case fromForm form of + Left err -> throwIO $ MalformedForm err Right value -> return value + where + -- This rather contrived implementation uses cons and reverse to avoid quadratic complexity (e.g. using HashMap.insertWith (++)). + -- It iterates over all parameters, prepending values for duplicate keys and reverses all hashmap entries afterwards. + paramListToForm :: [Param] -> Form + paramListToForm = Form . fmap reverse . foldl' (\f (k, v) -> HashMap.alter (prependValue v) k f) HashMap.empty + + prependValue :: a -> Maybe [a] -> Maybe [a] + prependValue v = Just . maybe [v] (v :) -- | Get a parameter. First looks in captures, then form data, then query parameters. -- diff --git a/Web/Scotty/Internal/Types.hs b/Web/Scotty/Internal/Types.hs index c591241..588c0df 100644 --- a/Web/Scotty/Internal/Types.hs +++ b/Web/Scotty/Internal/Types.hs @@ -147,7 +147,7 @@ data ScottyException = RequestTooLarge | MalformedJSON LBS8.ByteString T.Text | FailedToParseJSON LBS8.ByteString T.Text - | MalformedForm LBS8.ByteString T.Text + | MalformedForm T.Text | PathParameterNotFound T.Text | QueryParameterNotFound T.Text | FormFieldNotFound T.Text diff --git a/scotty.cabal b/scotty.cabal index 81d58d5..bd311a2 100644 --- a/scotty.cabal +++ b/scotty.cabal @@ -90,6 +90,7 @@ Library transformers >= 0.3.0.0 && < 0.7, transformers-base >= 0.4.1 && < 0.5, unliftio >= 0.2, + unordered-containers >= 0.2.10.0 && < 0.3, wai >= 3.0.0 && < 3.3, wai-extra >= 3.1.14, warp >= 3.0.13 From cbdbe4f381ef0e7f3a15f6046fd97f8cd6f9c570 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Thu, 25 Apr 2024 23:38:56 +0200 Subject: [PATCH 7/7] Fix comment for paramListToForm --- Web/Scotty/Action.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Web/Scotty/Action.hs b/Web/Scotty/Action.hs index 580d841..b80a165 100644 --- a/Web/Scotty/Action.hs +++ b/Web/Scotty/Action.hs @@ -375,8 +375,10 @@ formData = do Left err -> throwIO $ MalformedForm err Right value -> return value where - -- This rather contrived implementation uses cons and reverse to avoid quadratic complexity (e.g. using HashMap.insertWith (++)). - -- It iterates over all parameters, prepending values for duplicate keys and reverses all hashmap entries afterwards. + -- This rather contrived implementation uses cons and reverse to avoid + -- quadratic complexity when constructing a Form from a list of Param. + -- It's equivalent to using HashMap.insertWith (++) which does have + -- quadratic complexity due to appending at the end of list. paramListToForm :: [Param] -> Form paramListToForm = Form . fmap reverse . foldl' (\f (k, v) -> HashMap.alter (prependValue v) k f) HashMap.empty