diff --git a/src/PaymentServer/Main.hs b/src/PaymentServer/Main.hs index 941d4c245cc13a5b6f9a76aaaccf75cf2a405122..ad66ee0d9996434a36b3747aa1f1a48b5cc91e38 100644 --- a/src/PaymentServer/Main.hs +++ b/src/PaymentServer/Main.hs @@ -53,6 +53,7 @@ import Options.Applicative , ParserInfo , strOption , option + , many , auto , str , optional @@ -87,12 +88,13 @@ data Database = deriving (Show, Eq, Ord, Read) data ServerConfig = ServerConfig - { issuer :: Issuer - , signingKeyPath :: Maybe FilePath - , database :: Database - , databasePath :: Maybe Text - , endpoint :: Endpoint - , stripeKeyPath :: FilePath + { issuer :: Issuer + , signingKeyPath :: Maybe FilePath + , database :: Database + , databasePath :: Maybe Text + , endpoint :: Endpoint + , stripeKeyPath :: FilePath + , allowedChargeOrigins :: [Text] } deriving (Show, Eq) @@ -165,6 +167,13 @@ sample = ServerConfig <*> option str ( long "stripe-key-path" <> help "Path to Stripe Secret key" ) + <*> many + ( option str + ( long "allow-origin" + <> help "For the charge endpoint, a CORS origin to allow." + <> showDefault + ) + ) opts :: ParserInfo ServerConfig opts = info (sample <**> helper) @@ -230,6 +239,6 @@ getApp config = Right getDB -> do db <- getDB key <- B.readFile (stripeKeyPath config) - let app = paymentServerApp key issuer db + let app = paymentServerApp (allowedChargeOrigins config) key issuer db logger <- mkRequestLogger (def { outputFormat = Detailed True}) return $ logger app diff --git a/src/PaymentServer/Processors/Stripe.hs b/src/PaymentServer/Processors/Stripe.hs index 57137eda62fd1e9437607a15f47546f93b1cb603..004bf367d14e08ced6715dc3e25d4d835d9e6b15 100644 --- a/src/PaymentServer/Processors/Stripe.hs +++ b/src/PaymentServer/Processors/Stripe.hs @@ -7,6 +7,7 @@ module PaymentServer.Processors.Stripe , stripeServer , getVoucher , StripeSecretKey + , Origin ) where import Control.Monad.IO.Class @@ -39,11 +40,21 @@ import Servant , err500 , ServerError(errBody) , throwError + , NoContent(NoContent) + , Headers + , Header + , Header' + , Required + , Strict + , addHeader + , noHeader ) import Servant.API ( ReqBody + , StdMethod(OPTIONS) , JSON , Post + , Verb , (:>) , (:<|>)((:<|>)) ) @@ -79,6 +90,14 @@ import PaymentServer.Persistence , VoucherDatabase(payForVoucher) ) +-- | The type of an origin value in the context of HTTP / CORS. +type Origin = Text + +-- | OPTIONS is a standard HTTP/1.1 verb and it is used by the CORS mechanism +-- to exchange information about what cross-origin requests should be allowed. +-- Here we define it as a verb for use later in the type of our server. +type Options = Verb 'OPTIONS 204 + type StripeSecretKey = ByteString data Acknowledgement = Ok @@ -98,9 +117,48 @@ getVoucher (MetaData []) = Nothing getVoucher (MetaData (("Voucher", value):xs)) = Just value getVoucher (MetaData (x:xs)) = getVoucher (MetaData xs) -stripeServer :: VoucherDatabase d => StripeSecretKey -> d -> Server StripeAPI -stripeServer key d = webhook d - :<|> charge d key +-- | stripeServer serves responses to the Stripe portion of the API defined in +-- this module. +stripeServer + :: VoucherDatabase d + => [Origin] -- ^ A list of origins which are allowed to use the + -- charge endpoint. + -> StripeSecretKey + -> d + -> Server StripeAPI +stripeServer allowedChargeOrigins key d = + webhook d + :<|> charge d key + :<|> cors allowedChargeOrigins + +-- | Respond to a CORS OPTIONS preflight request for the charge endpoint in +-- such a way as to allow some cross-origin POSTs to that endpoint. This is a +-- partial implementation of the CORS rules only. +cors + :: [Origin] -- ^ A list of origins which are allowed to use the + -- charge endpoint. + -> Origin -- ^ The value of the Origin header in the request, + -- for comparison against the allowed origins. + -> Handler CORSResponse +cors allowedOrigins requestOrigin = + let + addHeaders = + if any (== requestOrigin) allowedOrigins then + -- The origin is allowed. Return the rest of the information. + addHeader requestOrigin + . addHeader "OPTIONS, POST" + . addHeader "Content-Type" + . addHeader (60 * 60 * 24) + else + -- The origin is not allowed. Per + -- <https://www.w3.org/TR/2014/REC-cors-20140116/#resource-preflight-requests>, + -- section 6.2, rule 2, add no further CORS headers to the response. + noHeader + . noHeader + . noHeader + . noHeader + in + return $ addHeaders $ addHeader "Origin" NoContent -- | Process charge succeeded events webhook :: VoucherDatabase d => d -> Event -> Handler Acknowledgement @@ -125,10 +183,24 @@ webhook d _ = return Ok +-- | The type of the response to a CORS OPTIONS preflight request. It would +-- be nice to use more expressive types than Text for some of these. +type CORSResponse = Headers + '[ Header "Access-Control-Allow-Origin" Text + , Header "Access-Control-Allow-Methods" Text + , Header "Access-Control-Allow-Headers" Text + , Header "Access-Control-Max-Age" Int + , Header "Vary" Text + ] + NoContent + + -- | Browser facing API that takes token, voucher and a few other information -- and calls stripe charges API. If payment succeeds, then the voucher is stored -- in the voucher database. -type ChargesAPI = "charge" :> ReqBody '[JSON] Charges :> Post '[JSON] Acknowledgement +type ChargesAPI = "charge" :> (CreateChargeAPI :<|> CORSPreflight) +type CreateChargeAPI = ReqBody '[JSON] Charges :> Post '[JSON] Acknowledgement +type CORSPreflight = Header' '[Required, Strict] "Origin" Text :> Options '[JSON] CORSResponse data Charges = Charges { token :: Text -- ^ The text of a Stripe tokenized payment method. diff --git a/src/PaymentServer/Server.hs b/src/PaymentServer/Server.hs index feef0b051330a58568fa88ded64667b74719049a..e6ff9d9b21fac611e41f0e791f12766c879ea888 100644 --- a/src/PaymentServer/Server.hs +++ b/src/PaymentServer/Server.hs @@ -16,7 +16,8 @@ import Servant , (:<|>)((:<|>)) ) import PaymentServer.Processors.Stripe - ( StripeAPI + ( Origin + , StripeAPI , StripeSecretKey , stripeServer ) @@ -37,9 +38,15 @@ type PaymentServerAPI :<|> "v1" :> "redeem" :> RedemptionAPI -- | Create a server which uses the given database. -paymentServer :: VoucherDatabase d => StripeSecretKey -> Issuer -> d -> Server PaymentServerAPI -paymentServer key issuer database = - stripeServer key database +paymentServer + :: VoucherDatabase d + => [Origin] + -> StripeSecretKey + -> Issuer + -> d + -> Server PaymentServerAPI +paymentServer allowedChargeOrigins key issuer database = + stripeServer allowedChargeOrigins key database :<|> redemptionServer issuer database paymentServerAPI :: Proxy PaymentServerAPI @@ -47,5 +54,11 @@ paymentServerAPI = Proxy -- | Create a Servant Application which serves the payment server API using -- the given database. -paymentServerApp :: VoucherDatabase d => StripeSecretKey -> Issuer -> d -> Application -paymentServerApp key issuer = serve paymentServerAPI . paymentServer key issuer +paymentServerApp + :: VoucherDatabase d + => [Origin] + -> StripeSecretKey + -> Issuer + -> d + -> Application +paymentServerApp allowedChargeOrigins key issuer = serve paymentServerAPI . paymentServer allowedChargeOrigins key issuer