mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-06 16:28:21 +00:00
130 lines
5.9 KiB
Haskell
130 lines
5.9 KiB
Haskell
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Bots.BadgeService.FakeStore
|
|
( FakeStore (..),
|
|
newFakeStore,
|
|
settlePending,
|
|
setGoogleDown,
|
|
googleSupporterToken,
|
|
googlePendingToken,
|
|
googleUnreachableToken,
|
|
googleThrowingToken,
|
|
googleHangingToken,
|
|
googleSubscriptionToken,
|
|
googlePayment,
|
|
unsignedJWS,
|
|
)
|
|
where
|
|
|
|
import BadgeService.StoreReceipts
|
|
import Control.Concurrent (threadDelay)
|
|
import Control.Monad (forever)
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.ByteString.Base64.URL as B64U
|
|
import qualified Data.ByteString.Char8 as B
|
|
import qualified Data.ByteString.Lazy as LB
|
|
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
|
import Data.Text (Text)
|
|
import Simplex.Chat.PaymentService (ServicePayment (..), googlePurchaseRef)
|
|
import Simplex.Chat.PaymentService.Types (CurrencyAmount (..))
|
|
import Simplex.Messaging.Util (safeDecodeUtf8)
|
|
import System.FilePath ((</>))
|
|
|
|
fixtureDir :: FilePath
|
|
fixtureDir = "apps" </> "simplex-badge-service" </> "test-fixtures" </> "apple"
|
|
|
|
-- | The Apple JWS are the fixtures under no real signature, so they are accepted as these exact
|
|
-- strings only, never because a payload decodes - which a real verifier must refuse to do.
|
|
data FakeStore = FakeStore
|
|
{ appleSupporterJWS :: Text,
|
|
appleLegendJWS :: Text,
|
|
appleSandboxJWS :: Text,
|
|
appleThrowingJWS :: Text,
|
|
appleMisnamedJWS :: Text,
|
|
appleQuantityJWS :: Text,
|
|
pendingSettled :: IORef Bool,
|
|
googleDown :: IORef Bool,
|
|
fakeVerifier :: StoreVerifier
|
|
}
|
|
|
|
newFakeStore :: IO FakeStore
|
|
newFakeStore = do
|
|
appleSupporterJWS <- fixtureJWS "transaction-supporter.json"
|
|
appleLegendJWS <- fixtureJWS "transaction-legend.json"
|
|
appleSandboxJWS <- fixtureJWS "transaction-sandbox.json"
|
|
let appleThrowingJWS = unsignedJWS "{\"transactionId\":\"2000000812345679\",\"productId\":\"BADGE_SUPPORTER_01\"}"
|
|
appleMisnamedJWS = unsignedJWS "{\"transactionId\":\"2000000812345698\",\"productId\":\"BADGE_SUPPORTER_01\"}"
|
|
appleQuantityJWS = unsignedJWS "{\"transactionId\":\"2000000812345697\",\"productId\":\"BADGE_SUPPORTER_01\"}"
|
|
pendingSettled <- newIORef False
|
|
googleDown <- newIORef False
|
|
let appleReceipts =
|
|
[ (appleSupporterJWS, appleTransaction "2000000812345671" "BADGE_SUPPORTER_01" False 700),
|
|
(appleLegendJWS, appleTransaction "2000000812345672" "BADGE_LEGEND_01" False 7000),
|
|
(appleSandboxJWS, appleTransaction "2000000812345673" "BADGE_LEGEND_01" True 7000),
|
|
-- a verifier vouching for another transaction than the one the evidence names
|
|
(appleMisnamedJWS, appleTransaction "2000000812345671" "BADGE_SUPPORTER_01" False 700),
|
|
(appleQuantityJWS, (appleTransaction "2000000812345697" "BADGE_SUPPORTER_01" False 1400) {quantity = 2})
|
|
]
|
|
verifyApple jws
|
|
| jws == appleThrowingJWS = error "fake verifier bug"
|
|
| otherwise = maybe (Left "not a fake receipt") Right $ lookup jws appleReceipts
|
|
verifyGoogle = googleVerdict pendingSettled googleDown
|
|
pure
|
|
FakeStore
|
|
{ appleSupporterJWS,
|
|
appleLegendJWS,
|
|
appleSandboxJWS,
|
|
appleThrowingJWS,
|
|
appleMisnamedJWS,
|
|
appleQuantityJWS,
|
|
pendingSettled,
|
|
googleDown,
|
|
fakeVerifier = StoreVerifier {verifyApple = Just verifyApple, verifyGoogle = Just verifyGoogle, verifyTimeout = 500000}
|
|
}
|
|
where
|
|
fixtureJWS name = unsignedJWS <$> B.readFile (fixtureDir </> name)
|
|
appleTransaction transactionRef productId testPurchase cents =
|
|
VerifiedStoreTransaction {transactionRef, productId, quantity = 1, testPurchase, paid = Just (CurrencyAmount cents, "USD")}
|
|
|
|
googleVerdict :: IORef Bool -> IORef Bool -> Text -> Text -> IO (Either StoreRefusal VerifiedStoreTransaction)
|
|
googleVerdict pendingSettled googleDown productId token =
|
|
readIORef googleDown >>= \case
|
|
True -> pure $ Left $ SRUnreachable "fake store is down"
|
|
False
|
|
| (productId, token) == ("badge_supporter_01", googleSupporterToken) -> pure $ Right purchased
|
|
| (productId, token) == ("subscr_badge_supporter_01", googleSubscriptionToken) -> pure $ Right purchased
|
|
| (productId, token) == ("badge_supporter_01", googlePendingToken) -> do
|
|
settled <- readIORef pendingSettled
|
|
pure $ if settled then Right purchased else Left SRPending
|
|
| token == googleUnreachableToken -> pure $ Left $ SRUnreachable "fake store is down"
|
|
| token == googleThrowingToken -> ioError $ userError "fake connection reset"
|
|
| token == googleHangingToken -> forever $ threadDelay 1000000
|
|
| otherwise -> pure $ Left $ SRInvalid "not a fake purchase"
|
|
where
|
|
purchased = VerifiedStoreTransaction {transactionRef = googlePurchaseRef token, productId, quantity = 1, testPurchase = False, paid = Nothing}
|
|
|
|
settlePending :: FakeStore -> IO ()
|
|
settlePending FakeStore {pendingSettled} = writeIORef pendingSettled True
|
|
|
|
setGoogleDown :: FakeStore -> Bool -> IO ()
|
|
setGoogleDown FakeStore {googleDown} = writeIORef googleDown
|
|
|
|
googleSupporterToken, googlePendingToken, googleUnreachableToken, googleThrowingToken, googleHangingToken, googleSubscriptionToken :: Text
|
|
googleSupporterToken = "fake-play-token-supporter.AO-J1Oz9x2kqE7wYt3"
|
|
googlePendingToken = "fake-play-token-pending.AO-J1Oy8w1jpD6vXs2"
|
|
googleUnreachableToken = "fake-play-token-unreachable.AO-J1Ox7v0ioC5uWr1"
|
|
googleThrowingToken = "fake-play-token-throwing.AO-J1Ov5t8gmA3sUp9"
|
|
googleHangingToken = "fake-play-token-hanging.AO-J1Ou4s7flZ2rTo8"
|
|
googleSubscriptionToken = "fake-play-token-subscription.AO-J1Ow6u9hnB4tVq0"
|
|
|
|
googlePayment :: Text -> Text -> ServicePayment
|
|
googlePayment productId token = SPGoogle {productId, token}
|
|
|
|
unsignedJWS :: B.ByteString -> Text
|
|
unsignedJWS payload = safeDecodeUtf8 $ B.intercalate "." $ map B64U.encodeUnpadded [header, payload, "not a signature"]
|
|
where
|
|
header = LB.toStrict $ J.encode $ J.object ["alg" J..= ("ES256" :: Text), "x5c" J..= (["leaf", "intermediate", "root"] :: [Text])]
|