New datatype FedURI for @id URIs

Using a dedicated type allows to record in the type the guarantees that we
provide, such as scheme being HTTPS and authority being present. Allows to
replace ugly `fromJust` and such with direct field access.
This commit is contained in:
fr33domlover 2019-02-07 23:08:28 +00:00
parent e325175a9c
commit 8ac559d064
12 changed files with 176 additions and 128 deletions

View file

@ -40,7 +40,7 @@ Person
UniquePersonEmail email UniquePersonEmail email
VerifKey VerifKey
ident URI ident FedURI
instance InstanceId instance InstanceId
expires UTCTime Maybe expires UTCTime Maybe
public PublicKey public PublicKey
@ -49,7 +49,7 @@ VerifKey
UniqueVerifKey ident UniqueVerifKey ident
RemoteSharer RemoteSharer
ident URI ident FedURI
instance InstanceId instance InstanceId
UniqueRemoteSharer ident UniqueRemoteSharer ident

View file

@ -1,5 +1,5 @@
VerifKey VerifKey
ident String ident Text
instance InstanceId instance InstanceId
expires UTCTime Maybe expires UTCTime Maybe
public ByteString public ByteString
@ -8,7 +8,7 @@ VerifKey
UniqueVerifKey ident UniqueVerifKey ident
RemoteSharer RemoteSharer
ident String ident Text
instance InstanceId instance InstanceId
UniqueRemoteSharer ident UniqueRemoteSharer ident

View file

@ -18,9 +18,6 @@ module Data.Aeson.Local
, toEither , toEither
, fromEither , fromEither
, frg , frg
, parseHttpsURI'
, parseHttpsURI
, renderURI
, (.=?) , (.=?)
) )
where where
@ -56,26 +53,6 @@ fromEither (Right y) = Right' y
frg :: Text frg :: Text
frg = "https://forgefed.angeley.es/ns#" frg = "https://forgefed.angeley.es/ns#"
parseHttpsURI' :: Text -> Either String URI
parseHttpsURI' t =
case parseURI $ T.unpack t of
Nothing -> Left "Invalid absolute URI"
Just u ->
if uriScheme u == "https:"
then case uriAuthority u of
Just a ->
if uriUserInfo a == "" && uriPort a == ""
then Right u
else Left "URI has userinfo or port"
Nothing -> Left "URI has empty authority"
else Left "URI scheme isn't https"
parseHttpsURI :: Text -> Parser URI
parseHttpsURI = either fail return . parseHttpsURI'
renderURI :: URI -> String
renderURI u = uriToString id u ""
infixr 8 .=? infixr 8 .=?
(.=?) :: ToJSON v => Text -> Maybe v -> Series (.=?) :: ToJSON v => Text -> Maybe v -> Series
_ .=? Nothing = mempty _ .=? Nothing = mempty

View file

@ -34,24 +34,10 @@ import Network.URI (URI, uriScheme, parseURI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Text as T (pack) import qualified Data.Text as T (pack)
import Data.Aeson.Local (renderURI)
instance (PersistField s, CI.FoldCase s) => PersistField (CI s) where instance (PersistField s, CI.FoldCase s) => PersistField (CI s) where
toPersistValue = toPersistValue . CI.original toPersistValue = toPersistValue . CI.original
fromPersistValue = fmap CI.mk . fromPersistValue fromPersistValue = fmap CI.mk . fromPersistValue
instance PersistField URI where
toPersistValue = toPersistValue . renderURI
fromPersistValue = parseHttpsURI <=< fromPersistValue
where
parseHttpsURI s =
case parseURI s of
Nothing -> Left "Invalid absolute URI"
Just u ->
if uriScheme u == "https:"
then Right u
else Left "URI scheme isn't https"
instance PersistField PublicKey where instance PersistField PublicKey where
toPersistValue = toPersistValue . convert' toPersistValue = toPersistValue . convert'
where where

View file

@ -25,19 +25,14 @@ import Data.ByteArray (convert)
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import Database.Persist.Sql import Database.Persist.Sql
import Network.URI (URI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Data.Aeson.Local (renderURI)
import Database.Persist.Class.Local () import Database.Persist.Class.Local ()
instance (PersistFieldSql s, CI.FoldCase s) => PersistFieldSql (CI s) where instance (PersistFieldSql s, CI.FoldCase s) => PersistFieldSql (CI s) where
sqlType = sqlType . fmap CI.original sqlType = sqlType . fmap CI.original
instance PersistFieldSql URI where
sqlType = sqlType . fmap renderURI
instance PersistFieldSql PublicKey where instance PersistFieldSql PublicKey where
sqlType = sqlType . fmap convert' sqlType = sqlType . fmap convert'
where where

101
src/Network/FedURI.hs Normal file
View file

@ -0,0 +1,101 @@
{- This file is part of Vervis.
-
- Written 2019 by fr33domlover <fr33domlover@riseup.net>.
-
- Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
module Network.FedURI
( FedURI (..)
, parseFedURI
, toURI
, renderFedURI
)
where
import Prelude
import Control.Monad ((<=<))
import Data.Aeson
import Data.Bifunctor (first)
import Data.Text (Text)
import Database.Persist.Class (PersistField (..))
import Database.Persist.Sql (PersistFieldSql (..))
import Network.URI
import qualified Data.Text as T (pack, unpack)
-- | An absolute URI with the following properties:
--
-- * The scheme is HTTPS
-- * The authority part is present
-- * The authority part doesn't have userinfo
-- * The authority part doesn't have a port number
-- * There is no query part
-- * A fragment part may be present
data FedURI = FedURI
{ furiHost :: Text
, furiPath :: Text
, furiFragment :: Text
}
deriving Eq
instance FromJSON FedURI where
parseJSON = withText "FedURI" $ either fail return . parseFedURI
instance ToJSON FedURI where
toJSON = error "toJSON FedURI"
toEncoding = toEncoding . renderFedURI
instance PersistField FedURI where
toPersistValue = toPersistValue . renderFedURI
fromPersistValue = first T.pack . parseFedURI <=< fromPersistValue
instance PersistFieldSql FedURI where
sqlType = sqlType . fmap renderFedURI
parseFedURI :: Text -> Either String FedURI
parseFedURI t = do
uri <- case parseURI $ T.unpack t of
Nothing -> Left "Invalid absolute URI"
Just u -> Right u
if uriScheme uri == "https:"
then Right ()
else Left "URI scheme isn't https"
URIAuth ui h p <- case uriAuthority uri of
Nothing -> Left "URI has empty authority"
Just a -> Right a
if ui == ""
then Right ()
else Left "URI has non-empty userinfo"
if p == ""
then Right ()
else Left "URI has non-empty port"
if uriQuery uri == ""
then Right ()
else Left "URI query is non-empty"
Right FedURI
{ furiHost = T.pack h
, furiPath = T.pack p
, furiFragment = T.pack $ uriFragment uri
}
toURI :: FedURI -> URI
toURI (FedURI h p f) = URI
{ uriScheme = "https:"
, uriAuthority = Just $ URIAuth "" (T.unpack h) ""
, uriPath = T.unpack p
, uriQuery = ""
, uriFragment = T.unpack f
}
renderFedURI :: FedURI -> Text
renderFedURI = T.pack . flip (uriToString id) "" . toURI

View file

@ -57,9 +57,9 @@ import Yesod.Mail.Send
import qualified Network.HTTP.Signature as S (Algorithm (..)) import qualified Network.HTTP.Signature as S (Algorithm (..))
import Network.FedURI
import Web.ActivityPub import Web.ActivityPub
import Data.Aeson.Local (parseHttpsURI')
import Text.Email.Local import Text.Email.Local
import Text.Jasmine.Local (discardm) import Text.Jasmine.Local (discardm)
@ -562,8 +562,8 @@ unsafeHandler = Unsafe.fakeHandlerGetLogger appLogger
-- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding -- https://github.com/yesodweb/yesod/wiki/i18n-messages-in-the-scaffolding
instance YesodHttpSig App where instance YesodHttpSig App where
data HttpSigVerResult App = HttpSigVerResult (Either String URI) data HttpSigVerResult App = HttpSigVerResult (Either String FedURI)
httpSigVerHeaders = const [hRequestTarget, hHost, "ActivityPub-Actor"] httpSigVerHeaders = const [hRequestTarget, hHost, hActivityPubActor]
httpSigVerSeconds = httpSigVerSeconds =
fromIntegral . toSeconds . appHttpSigTimeLimit . appSettings fromIntegral . toSeconds . appHttpSigTimeLimit . appSettings
where where
@ -576,20 +576,20 @@ instance YesodHttpSig App where
case algo of case algo of
S.AlgorithmEd25519 -> Right () S.AlgorithmEd25519 -> Right ()
S.AlgorithmOther _ -> Left "Unsupported algo in Sig header" S.AlgorithmOther _ -> Left "Unsupported algo in Sig header"
u <- ExceptT . pure $ case parseURI $ BC.unpack keyid of u <- ExceptT . pure $ case parseFedURI =<< (first displayException . decodeUtf8') keyid of
Nothing -> Left "keyId in Sig header isn't a valid absolute URI" Left e -> Left $ "keyId in Sig header isn't a valid FedURI: " ++ e
Just uri -> Right uri Right uri -> Right uri
signature <- ExceptT . pure $ do signature <- ExceptT . pure $ do
case signature sig of case signature sig of
CryptoPassed s -> Right s CryptoPassed s -> Right s
CryptoFailed e -> Left "Parsing Ed25519 signature failed" CryptoFailed e -> Left "Parsing Ed25519 signature failed"
muActorHeader <- do muActorHeader <- do
bs <- lookupHeaders "ActivityPub-Actor" bs <- lookupHeaders hActivityPubActor
case bs of case bs of
[] -> return Nothing [] -> return Nothing
[b] -> fmap Just . ExceptT . pure $ do [b] -> fmap Just . ExceptT . pure $ do
t <- first displayException $ decodeUtf8' b t <- first displayException $ decodeUtf8' b
parseHttpsURI' t parseFedURI t
_ -> throwE "Multiple ActivityPub-Actor headers" _ -> throwE "Multiple ActivityPub-Actor headers"
(mvkid, key, mexpires, uActor, host, shared) <- do (mvkid, key, mexpires, uActor, host, shared) <- do
ments <- lift $ runDB $ do ments <- lift $ runDB $ do
@ -612,7 +612,7 @@ instance YesodHttpSig App where
, verifKeyPublic vk , verifKeyPublic vk
, verifKeyExpires vk , verifKeyExpires vk
, ua , ua
, T.pack $ uriRegName $ fromJust $ uriAuthority uKey , furiHost uKey
, s , s
) )
Nothing -> do Nothing -> do

View file

@ -49,7 +49,6 @@ import Database.Persist (Entity (..))
import Network.HTTP.Client (Manager, HttpException, requestFromURI) import Network.HTTP.Client (Manager, HttpException, requestFromURI)
import Network.HTTP.Simple (httpJSONEither, getResponseBody, setRequestManager, addRequestHeader) import Network.HTTP.Simple (httpJSONEither, getResponseBody, setRequestManager, addRequestHeader)
import Network.HTTP.Types.Header (hDate, hHost) import Network.HTTP.Types.Header (hDate, hHost)
import Network.URI
import Text.Blaze.Html (Html) import Text.Blaze.Html (Html)
import UnliftIO.Exception (try) import UnliftIO.Exception (try)
import Yesod.Auth (requireAuth) import Yesod.Auth (requireAuth)
@ -75,8 +74,7 @@ import Yesod.HttpSignature (verifyRequestSignature)
import qualified Network.HTTP.Signature as S (Algorithm (..)) import qualified Network.HTTP.Signature as S (Algorithm (..))
import Data.Aeson.Local (parseHttpsURI') import Network.FedURI
import Web.ActivityPub import Web.ActivityPub
import Vervis.ActorKey import Vervis.ActorKey
@ -167,9 +165,9 @@ postInboxR = do
case M.lookup "actor" o of case M.lookup "actor" o of
Nothing -> Left "Activity has no actor member" Nothing -> Left "Activity has no actor member"
Just v -> case v of Just v -> case v of
String t -> case parseURI $ T.unpack t of String t -> case parseFedURI t of
Nothing -> Left "Activity actor URI parsing failed" Left e -> Left $ "Activity actor URI parsing failed: " ++ e
Just uri -> Right uri Right uri -> Right uri
_ -> Left "Activity actor isn't a JSON string" _ -> Left "Activity actor isn't a JSON string"
liftE $ if activityActor == uActor liftE $ if activityActor == uActor
then Right () then Right ()
@ -180,9 +178,9 @@ postInboxR = do
Object obj -> case M.lookup "actor" obj <|> M.lookup "attributedTo" obj of Object obj -> case M.lookup "actor" obj <|> M.lookup "attributedTo" obj of
Nothing -> Right () Nothing -> Right ()
Just v' -> case v' of Just v' -> case v' of
String t -> case parseURI $ T.unpack t of String t -> case parseFedURI t of
Nothing -> Left "Activity actor URI parsing failed" Left e -> Left $ "Activity actor URI parsing failed: " ++ e
Just uri -> Right uri ->
if uri == uActor if uri == uActor
then Right () then Right ()
else Left "Activity object's actor doesn't match activity's actor" else Left "Activity object's actor doesn't match activity's actor"
@ -200,12 +198,7 @@ activityForm :: Form Activity
activityForm = renderDivs $ areq jsonField "" $ Just defval activityForm = renderDivs $ areq jsonField "" $ Just defval
where where
defval = Activity defval = Activity
{ activityTo = { activityTo = FedURI "forge.angeley.es" "/p/aviva" ""
URI "https:"
(Just $ URIAuth "" "forge.angeley.es" "")
"/p/aviva"
""
""
, activityJSON = M.fromList , activityJSON = M.fromList
[ "@context" .= ("https://www.w3.org/ns/activitystreams" :: Text) [ "@context" .= ("https://www.w3.org/ns/activitystreams" :: Text)
, "type" .= ("Create" :: Text) , "type" .= ("Create" :: Text)
@ -292,7 +285,7 @@ postOutboxR = do
else (keyID2, akey2) else (keyID2, akey2)
sign b = (KeyId $ encodeUtf8 keyID, actorKeySign akey b) sign b = (KeyId $ encodeUtf8 keyID, actorKeySign akey b)
eres' <- httpPostAP manager (actorInbox actor) (hRequestTarget :| [hHost, hDate, hActivityPubActor]) sign actorID (updateAct act) eres' <- httpPostAP manager (actorInbox actor) (hRequestTarget :| [hHost, hDate, hActivityPubActor]) sign actorID (updateAct act)
case eres of case eres' of
Left e -> setMessage $ toHtml $ "Failed to POST to recipient's inbox: " <> T.pack (displayException e) Left e -> setMessage $ toHtml $ "Failed to POST to recipient's inbox: " <> T.pack (displayException e)
Right _ -> setMessage "Activity posted! You can go to the target server's /inbox to see the result." Right _ -> setMessage "Activity posted! You can go to the target server's /inbox to see the result."
defaultLayout $ activityWidget widget enctype defaultLayout $ activityWidget widget enctype
@ -304,7 +297,7 @@ getActorKey choose route = do
getsYesod appActorKeys getsYesod appActorKeys
renderUrl <- getUrlRender renderUrl <- getUrlRender
let route2uri r = let route2uri r =
case parseHttpsURI' $ renderUrl r of case parseFedURI $ renderUrl r of
Left e -> error e Left e -> error e
Right u -> u Right u -> u
selectRep $ selectRep $

View file

@ -28,7 +28,6 @@ import Vervis.Import hiding ((==.))
--import Prelude --import Prelude
import Database.Esqueleto hiding (isNothing, count) import Database.Esqueleto hiding (isNothing, count)
import Network.URI (uriFragment, parseAbsoluteURI)
import Vervis.Form.Person import Vervis.Form.Person
--import Model --import Model
import Text.Blaze.Html (toHtml) import Text.Blaze.Html (toHtml)
@ -41,6 +40,7 @@ import Yesod.Auth.Unverified (requireUnverifiedAuth)
import Text.Email.Local import Text.Email.Local
import Network.FedURI
import Web.ActivityPub import Web.ActivityPub
--import Vervis.ActivityStreams --import Vervis.ActivityStreams
@ -137,9 +137,9 @@ getPersonR shr = do
return p return p
renderUrl <- getUrlRender renderUrl <- getUrlRender
let route2uri route = let route2uri route =
case parseAbsoluteURI $ T.unpack $ renderUrl route of case parseFedURI $ renderUrl route of
Nothing -> error "getRenderUrl produced invalid URI!!!" Left e -> error $ "getRenderUrl produced invalid FedURI!!! " ++ e
Just u -> u Right u -> u
me = route2uri $ PersonR shr me = route2uri $ PersonR shr
selectRep $ do selectRep $ do
provideRep $ do provideRep $ do

View file

@ -23,12 +23,12 @@ import Yesod hiding (Header, parseTime)
import Crypto.PubKey.Ed25519 (PublicKey) import Crypto.PubKey.Ed25519 (PublicKey)
import Database.Persist.Quasi import Database.Persist.Quasi
import Database.Persist.Sql (fromSqlKey) import Database.Persist.Sql (fromSqlKey)
import Network.URI (URI)
import Text.Email.Validate (EmailAddress) import Text.Email.Validate (EmailAddress)
import Yesod.Auth.Account (PersistUserCredentials (..)) import Yesod.Auth.Account (PersistUserCredentials (..))
import Database.Persist.EmailAddress import Database.Persist.EmailAddress
import Database.Persist.Graph.Class import Database.Persist.Graph.Class
import Network.FedURI (FedURI)
import Vervis.Model.Group import Vervis.Model.Group
import Vervis.Model.Ident import Vervis.Model.Ident

View file

@ -77,6 +77,8 @@ import qualified Data.HashMap.Strict as M (lookup)
import qualified Data.Text as T (pack, unpack) import qualified Data.Text as T (pack, unpack)
import qualified Data.Vector as V (fromList, toList) import qualified Data.Vector as V (fromList, toList)
import Network.FedURI
import Data.Aeson.Local import Data.Aeson.Local
as2context :: Text as2context :: Text
@ -119,9 +121,9 @@ instance ToJSON Algorithm where
AlgorithmOther t -> t AlgorithmOther t -> t
data PublicKey = PublicKey data PublicKey = PublicKey
{ publicKeyId :: URI { publicKeyId :: FedURI
, publicKeyExpires :: Maybe UTCTime , publicKeyExpires :: Maybe UTCTime
, publicKeyOwner :: URI , publicKeyOwner :: FedURI
, publicKeyPem :: PEM , publicKeyPem :: PEM
, publicKeyAlgo :: Maybe Algorithm , publicKeyAlgo :: Maybe Algorithm
, publicKeyShared :: Bool , publicKeyShared :: Bool
@ -137,9 +139,9 @@ instance FromJSON PublicKey where
then return () then return ()
else fail "PublicKey @type isn't Key" else fail "PublicKey @type isn't Key"
PublicKey PublicKey
<$> (parseHttpsURI =<< o .: "id") <$> o .: "id"
<*> o .:? "expires" <*> o .:? "expires"
<*> (parseHttpsURI =<< o .: "owner") <*> o .: "owner"
<*> (parsePEM =<< o .: "publicKeyPem") <*> (parsePEM =<< o .: "publicKeyPem")
<*> o .:? (frg <> "algorithm") <*> o .:? (frg <> "algorithm")
<*> o .:? (frg <> "shared") .!= False <*> o .:? (frg <> "shared") .!= False
@ -157,16 +159,16 @@ instance ToJSON PublicKey where
toJSON = error "toJSON PublicKey" toJSON = error "toJSON PublicKey"
toEncoding (PublicKey id_ mexpires owner pem malgo shared) = toEncoding (PublicKey id_ mexpires owner pem malgo shared) =
pairs pairs
$ "id" .= renderURI id_ $ "id" .= id_
<> "expires" .=? mexpires <> "expires" .=? mexpires
<> "owner" .= renderURI owner <> "owner" .= owner
<> "publicKeyPem" .= decodeUtf8 (pemWriteBS pem) <> "publicKeyPem" .= decodeUtf8 (pemWriteBS pem)
<> (frg <> "algorithm") .=? malgo <> (frg <> "algorithm") .=? malgo
<> (frg <> "shared") .= shared <> (frg <> "shared") .= shared
data PublicKeySet = PublicKeySet data PublicKeySet = PublicKeySet
{ publicKey1 :: Either URI PublicKey { publicKey1 :: Either FedURI PublicKey
, publicKey2 :: Maybe (Either URI PublicKey) , publicKey2 :: Maybe (Either FedURI PublicKey)
} }
instance FromJSON PublicKeySet where instance FromJSON PublicKeySet where
@ -180,7 +182,7 @@ instance FromJSON PublicKeySet where
_ -> fail "More than 2 public keys isn't supported" _ -> fail "More than 2 public keys isn't supported"
_ -> PublicKeySet <$> parseKey v <*> pure Nothing _ -> PublicKeySet <$> parseKey v <*> pure Nothing
where where
parseKey = bitraverse parseHttpsURI pure . toEither <=< parseJSON parseKey = fmap toEither . parseJSON
instance ToJSON PublicKeySet where instance ToJSON PublicKeySet where
toJSON = error "toJSON PublicKeySet" toJSON = error "toJSON PublicKeySet"
@ -189,23 +191,23 @@ instance ToJSON PublicKeySet where
Nothing -> toEncoding $ renderKey k1 Nothing -> toEncoding $ renderKey k1
Just k2 -> toEncodingList [renderKey k1, renderKey k2] Just k2 -> toEncodingList [renderKey k1, renderKey k2]
where where
renderKey = fromEither . first renderURI renderKey = fromEither
data Actor = Actor data Actor = Actor
{ actorId :: URI { actorId :: FedURI
, actorType :: ActorType , actorType :: ActorType
, actorUsername :: Text , actorUsername :: Text
, actorInbox :: URI , actorInbox :: FedURI
, actorPublicKeys :: PublicKeySet , actorPublicKeys :: PublicKeySet
} }
instance FromJSON Actor where instance FromJSON Actor where
parseJSON = withObject "Actor" $ \ o -> parseJSON = withObject "Actor" $ \ o ->
Actor Actor
<$> (parseHttpsURI =<< o .: "id") <$> o .: "id"
<*> o .: "type" <*> o .: "type"
<*> o .: "preferredUsername" <*> o .: "preferredUsername"
<*> (parseHttpsURI =<< o .: "inbox") <*> o .: "inbox"
<*> o .: "publicKey" <*> o .: "publicKey"
instance ToJSON Actor where instance ToJSON Actor where
@ -213,10 +215,10 @@ instance ToJSON Actor where
toEncoding (Actor id_ typ username inbox pkeys) = toEncoding (Actor id_ typ username inbox pkeys) =
pairs pairs
$ "@context" .= actorContext $ "@context" .= actorContext
<> "id" .= renderURI id_ <> "id" .= id_
<> "type" .= typ <> "type" .= typ
<> "preferredUsername" .= username <> "preferredUsername" .= username
<> "inbox" .= renderURI inbox <> "inbox" .= inbox
<> "publicKey" .= pkeys <> "publicKey" .= pkeys
-- | This may seem trivial, but it exists for a good reason: In the 'FromJSON' -- | This may seem trivial, but it exists for a good reason: In the 'FromJSON'
@ -229,7 +231,7 @@ instance ToJSON Actor where
-- ByteString, but I guess it's okay for now, and it happens to guarantee the -- ByteString, but I guess it's okay for now, and it happens to guarantee the
-- JSON we POST has no extra whitespace. -- JSON we POST has no extra whitespace.
data Activity = Activity data Activity = Activity
{ activityTo :: URI { activityTo :: FedURI
, activityJSON :: Object , activityJSON :: Object
} }
@ -269,7 +271,7 @@ instance FromJSON Activity where
mto2 <- o .:? "to" mto2 <- o .:? "to"
to <- case mto <|> mto2 of to <- case mto <|> mto2 of
Nothing -> fail "to not provided" Nothing -> fail "to not provided"
Just t -> parseHttpsURI t Just u -> return u
return $ Activity to o return $ Activity to o
instance ToJSON Activity where instance ToJSON Activity where
@ -310,12 +312,11 @@ instance Exception APGetError
httpGetAP httpGetAP
:: (MonadIO m, FromJSON a) :: (MonadIO m, FromJSON a)
=> Manager => Manager
-> URI -> FedURI
-> m (Either APGetError (Response a)) -> m (Either APGetError (Response a))
httpGetAP manager uri = httpGetAP manager uri =
if uriScheme uri /= "https:" liftIO $
then return $ Left $ APGetErrorHTTP $ InvalidUrlException (show uri) "Scheme isn't https" mkResult <$> try (httpAPEither manager =<< requestFromURI (toURI uri))
else liftIO $ mkResult <$> try (httpAPEither manager =<< requestFromURI uri)
where where
lookup' x = map snd . filter ((== x) . fst) lookup' x = map snd . filter ((== x) . fst)
mkResult (Left e) = Left $ APGetErrorHTTP e mkResult (Left e) = Left $ APGetErrorHTTP e
@ -340,29 +341,27 @@ httpGetAP manager uri =
httpPostAP httpPostAP
:: (MonadIO m, ToJSON a) :: (MonadIO m, ToJSON a)
=> Manager => Manager
-> URI -> FedURI
-> NonEmpty HeaderName -> NonEmpty HeaderName
-> (ByteString -> (KeyId, Signature)) -> (ByteString -> (KeyId, Signature))
-> Text -> Text
-> a -> a
-> m (Either HttpException (Response ())) -> m (Either HttpException (Response ()))
httpPostAP manager uri headers sign uActor value = httpPostAP manager uri headers sign uActor value =
if uriScheme uri /= "https:" liftIO $ try $ do
then return $ Left $ InvalidUrlException (show uri) "Scheme isn't https" req <- requestFromURI $ toURI uri
else liftIO $ try $ do let req' =
req <- requestFromURI uri setRequestCheckStatus $
let req' = consHeader hContentType typeActivityStreams2LD $
setRequestCheckStatus $ consHeader hActivityPubActor (encodeUtf8 uActor) $
consHeader hContentType typeActivityStreams2LD $ req { method = "POST"
consHeader hActivityPubActor (encodeUtf8 uActor) $ , requestBody = RequestBodyLBS $ encode value
req { method = "POST" }
, requestBody = RequestBodyLBS $ encode value sign' b =
} let (k, s) = sign b
sign' b = in (Nothing, k, s)
let (k, s) = sign b req'' <- signRequest headers sign' Nothing req'
in (Nothing, k, s) httpNoBody req'' manager
req'' <- signRequest headers sign' Nothing req'
httpNoBody req'' manager
where where
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r } consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
@ -372,7 +371,7 @@ data Fetched = Fetched
-- ^ The Ed25519 public key corresponding to the URI we requested. -- ^ The Ed25519 public key corresponding to the URI we requested.
, fetchedKeyExpires :: Maybe UTCTime , fetchedKeyExpires :: Maybe UTCTime
-- ^ Optional expiration time declared for the key we received. -- ^ Optional expiration time declared for the key we received.
, fetchedActorId :: URI , fetchedActorId :: FedURI
-- ^ The @id URI of the actor for whom the key's signature applies. -- ^ The @id URI of the actor for whom the key's signature applies.
, fetchedHost :: Text , fetchedHost :: Text
-- ^ The domain name of the instance from which we got the key. -- ^ The domain name of the instance from which we got the key.
@ -389,25 +388,21 @@ fetchKey
:: MonadIO m :: MonadIO m
=> Manager => Manager
-> Bool -> Bool
-> Maybe URI -> Maybe FedURI
-> URI -> FedURI
-> m (Either String Fetched) -> m (Either String Fetched)
fetchKey manager sigAlgo muActor uKey = runExceptT $ do fetchKey manager sigAlgo muActor uKey = runExceptT $ do
let fetch :: (MonadIO m, FromJSON a) => URI -> ExceptT String m a let fetch :: (MonadIO m, FromJSON a) => FedURI -> ExceptT String m a
fetch u = ExceptT $ bimap displayException responseBody <$> httpGetAP manager u fetch u = ExceptT $ bimap displayException responseBody <$> httpGetAP manager u
obj <- fetch uKey obj <- fetch uKey
let inztance = uKey { uriPath = "", uriQuery = "", uriFragment = "" } let inztance = uKey { furiPath = "", furiFragment = "" }
authority =
case uriAuthority uKey of
Nothing -> error "BUG! We were supposed to verify URI authority is non-empty!"
Just a -> a
(actor, pkey, shared) <- (actor, pkey, shared) <-
case obj of case obj of
Left' pkey -> do Left' pkey -> do
if publicKeyId pkey == uKey if publicKeyId pkey == uKey
then return () then return ()
else throwE "Public key's ID doesn't match the keyid URI" else throwE "Public key's ID doesn't match the keyid URI"
if uriAuthority (publicKeyOwner pkey) == Just authority if furiHost (publicKeyOwner pkey) == furiHost uKey
then return () then return ()
else throwE "Actor and key on different domains, we reject" else throwE "Actor and key on different domains, we reject"
uActor <- uActor <-
@ -424,7 +419,7 @@ fetchKey manager sigAlgo muActor uKey = runExceptT $ do
then return (actor, pkey, publicKeyShared pkey) then return (actor, pkey, publicKeyShared pkey)
else throwE "Actor publicKey has no URI matching pkey @id" else throwE "Actor publicKey has no URI matching pkey @id"
Right' actor -> do Right' actor -> do
if actorId actor == uKey { uriFragment = "" } if actorId actor == uKey { furiFragment = "" }
then return () then return ()
else throwE "Actor ID doesn't match the keyid URI we fetched" else throwE "Actor ID doesn't match the keyid URI we fetched"
case muActor of case muActor of
@ -472,7 +467,7 @@ fetchKey manager sigAlgo muActor uKey = runExceptT $ do
{ fetchedPublicKey = k { fetchedPublicKey = k
, fetchedKeyExpires = publicKeyExpires pkey , fetchedKeyExpires = publicKeyExpires pkey
, fetchedActorId = actorId actor , fetchedActorId = actorId actor
, fetchedHost = T.pack $ uriRegName authority , fetchedHost = furiHost uKey
, fetchedKeyShared = shared , fetchedKeyShared = shared
} }
CryptoFailed _ -> Left "Parsing Ed25519 public key failed" CryptoFailed _ -> Left "Parsing Ed25519 public key failed"

View file

@ -75,6 +75,7 @@ library
Diagrams.IntransitiveDAG Diagrams.IntransitiveDAG
Formatting.CaseInsensitive Formatting.CaseInsensitive
Language.Haskell.TH.Quote.Local Language.Haskell.TH.Quote.Local
Network.FedURI
Network.HTTP.Client.Conduit.ActivityPub Network.HTTP.Client.Conduit.ActivityPub
Network.SSH.Local Network.SSH.Local
Text.Blaze.Local Text.Blaze.Local