mirror of
https://code.sup39.dev/repos/Wqawg
synced 2024-12-29 01:34:52 +09:00
5df8965488
* No more full URIs, all terms are used as short non-prefixed names * Some terms support parsing full URI form for compatibility with objects in DB * No more @context checking when parsing * Use the new ForgeFed context URI specified in the spec draft * Use an extension context URI for all custom properties not specific to forges * Rename "events" property to "history", thanks cjslep for suggesting this name
1225 lines
43 KiB
Haskell
1225 lines
43 KiB
Haskell
{- This file is part of Vervis.
|
|
-
|
|
- Written in 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 Web.ActivityPub
|
|
( -- * Type-safe manipulation tools
|
|
--
|
|
-- Types and functions that make handling URIs and JSON-LD contexts less
|
|
-- error-prone and safer by recording safety checks in the type and
|
|
-- placing the checks in a single clear place.
|
|
ActivityPub (..)
|
|
, Doc (..)
|
|
|
|
-- * Actor
|
|
--
|
|
-- ActivityPub actor document including a public key, with a 'FromJSON'
|
|
-- instance for fetching and a 'ToJSON' instance for publishing.
|
|
, ActorType (..)
|
|
--, Algorithm (..)
|
|
, Owner (..)
|
|
, PublicKey (..)
|
|
, Actor (..)
|
|
, Project (..)
|
|
, CollectionType (..)
|
|
, Collection (..)
|
|
, CollectionPageType (..)
|
|
, CollectionPage (..)
|
|
, Recipient (..)
|
|
|
|
-- * Content objects
|
|
, Note (..)
|
|
, TextHtml (..)
|
|
, TextPandocMarkdown (..)
|
|
, TicketLocal (..)
|
|
, Ticket (..)
|
|
|
|
-- * Activity
|
|
, Accept (..)
|
|
, Create (..)
|
|
, Follow (..)
|
|
, Offer (..)
|
|
, Reject (..)
|
|
, Audience (..)
|
|
, SpecificActivity (..)
|
|
, Activity (..)
|
|
|
|
-- * Utilities
|
|
, publicURI
|
|
, hActivityPubActor
|
|
, provideAP
|
|
, APGetError (..)
|
|
, httpGetAP
|
|
, APPostError (..)
|
|
, hActivityPubForwarder
|
|
, hForwardingSignature
|
|
, hForwardedSignature
|
|
, httpPostAP
|
|
, httpPostAPBytes
|
|
, Fetched (..)
|
|
, fetchAPID
|
|
, fetchAPID'
|
|
, fetchRecipient
|
|
, keyListedByActor
|
|
, fetchUnknownKey
|
|
, fetchKnownPersonalKey
|
|
, fetchKnownSharedKey
|
|
)
|
|
where
|
|
|
|
import Prelude
|
|
|
|
import Control.Applicative ((<|>), optional)
|
|
import Control.Exception (Exception, displayException, try)
|
|
import Control.Monad (when, unless, (<=<), join)
|
|
import Control.Monad.IO.Class
|
|
import Control.Monad.Trans.Except
|
|
import Control.Monad.Trans.Writer (Writer)
|
|
import Crypto.Hash hiding (Context)
|
|
import Data.Aeson
|
|
import Data.Aeson.Encoding (pair)
|
|
import Data.Aeson.Types (Parser, typeMismatch, listEncoding)
|
|
import Data.Bifunctor
|
|
import Data.Bitraversable (bitraverse)
|
|
import Data.ByteString (ByteString)
|
|
import Data.Foldable (for_)
|
|
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
|
|
import Data.Proxy
|
|
import Data.PEM
|
|
import Data.Semigroup (Endo, First (..))
|
|
import Data.Text (Text)
|
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
|
|
import Data.Time.Clock (UTCTime)
|
|
import Data.Traversable
|
|
import Data.Vector (Vector)
|
|
import Network.HTTP.Client hiding (Proxy, proxy)
|
|
import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither)
|
|
import Network.HTTP.Simple (JSONException)
|
|
import Network.HTTP.Types.Header (HeaderName, hContentType)
|
|
import Network.URI
|
|
import Text.HTML.SanitizeXSS
|
|
import Yesod.Core.Content (ContentType)
|
|
import Yesod.Core.Handler (ProvidedRep, provideRepType)
|
|
|
|
import Network.HTTP.Client.Signature
|
|
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.ByteString.Base64 as B64
|
|
import qualified Data.ByteString.Char8 as BC
|
|
import qualified Data.ByteString.Lazy as BL
|
|
import qualified Data.HashMap.Strict as M
|
|
import qualified Data.Text as T (pack, unpack)
|
|
import qualified Data.Vector as V
|
|
import qualified Network.HTTP.Signature as S
|
|
|
|
import Crypto.PublicVerifKey
|
|
import Network.FedURI
|
|
import Network.HTTP.Digest
|
|
|
|
import Data.Aeson.Local
|
|
|
|
proxy :: a -> Proxy a
|
|
proxy _ = Proxy
|
|
|
|
as2Context :: FedURI
|
|
as2Context = FedURI "www.w3.org" "/ns/activitystreams" ""
|
|
|
|
secContext :: FedURI
|
|
secContext = FedURI "w3id.org" "/security/v1" ""
|
|
|
|
forgeContext :: FedURI
|
|
forgeContext = FedURI "forgefed.peers.community" "/ns" ""
|
|
|
|
extContext :: FedURI
|
|
extContext = FedURI "angeley.es" "/as2-ext" ""
|
|
|
|
publicURI :: FedURI
|
|
publicURI = FedURI "www.w3.org" "/ns/activitystreams" "#Public"
|
|
|
|
publicT :: Text
|
|
publicT = renderFedURI publicURI
|
|
|
|
class ActivityPub a where
|
|
jsonldContext :: Proxy a -> [FedURI]
|
|
parseObject :: Object -> Parser (Text, a)
|
|
toSeries :: Text -> a -> Series
|
|
|
|
data Doc a = Doc
|
|
{ docHost :: Text
|
|
, docValue :: a
|
|
}
|
|
|
|
instance ActivityPub a => FromJSON (Doc a) where
|
|
parseJSON = withObject "Doc" $ \ o -> uncurry Doc <$> parseObject o
|
|
|
|
instance ActivityPub a => ToJSON (Doc a) where
|
|
toJSON = error "toJSON Doc"
|
|
toEncoding (Doc h v) =
|
|
pairs
|
|
$ context (jsonldContext $ proxy v)
|
|
<> toSeries h v
|
|
where
|
|
context [] = mempty
|
|
context [t] = "@context" .= t
|
|
context ts = "@context" .= ts
|
|
|
|
data ActorType = ActorTypePerson | ActorTypeProject | ActorTypeOther Text
|
|
deriving Eq
|
|
|
|
instance FromJSON ActorType where
|
|
parseJSON = withText "ActorType" $ pure . parse
|
|
where
|
|
parse t
|
|
| t == "Person" = ActorTypePerson
|
|
| t == "Project" = ActorTypeProject
|
|
| otherwise = ActorTypeOther t
|
|
|
|
instance ToJSON ActorType where
|
|
toJSON = error "toJSON ActorType"
|
|
toEncoding at =
|
|
toEncoding $ case at of
|
|
ActorTypePerson -> "Person"
|
|
ActorTypeProject -> "Project"
|
|
ActorTypeOther t -> t
|
|
|
|
data Owner = OwnerInstance | OwnerActor LocalURI
|
|
|
|
ownerShared :: Owner -> Bool
|
|
ownerShared OwnerInstance = True
|
|
ownerShared (OwnerActor _) = False
|
|
|
|
data PublicKey = PublicKey
|
|
{ publicKeyId :: LocalURI
|
|
, publicKeyExpires :: Maybe UTCTime
|
|
, publicKeyOwner :: Owner
|
|
, publicKeyMaterial :: PublicVerifKey
|
|
}
|
|
|
|
instance ActivityPub PublicKey where
|
|
jsonldContext _ = [secContext, extContext]
|
|
parseObject o = do
|
|
mtyp <- optional $ o .: "@type" <|> o .: "type"
|
|
for_ mtyp $ \ t ->
|
|
when (t /= ("Key" :: Text)) $
|
|
fail "PublicKey @type isn't Key"
|
|
(host, id_) <- f2l <$> (o .: "@id" <|> o .: "id")
|
|
shared <- o .:|? "isShared" .!= False
|
|
fmap (host,) $
|
|
PublicKey id_
|
|
<$> o .:? "expires"
|
|
<*> (mkOwner shared =<< withHost host o "owner")
|
|
<*> (either fail return . decodePublicVerifKeyPEM =<<
|
|
o .: "publicKeyPem"
|
|
)
|
|
where
|
|
withHost h o t = do
|
|
(h', lu) <- f2l <$> o .: t
|
|
if h == h'
|
|
then return lu
|
|
else fail "URI host mismatch"
|
|
mkOwner True (LocalURI "" "") = return OwnerInstance
|
|
mkOwner True _ = fail "Shared key but owner isn't instance URI"
|
|
mkOwner False lu = return $ OwnerActor lu
|
|
toSeries host (PublicKey id_ mexpires owner mat)
|
|
= "@id" .= l2f host id_
|
|
<> "expires" .=? mexpires
|
|
<> "owner" .= mkOwner host owner
|
|
<> "publicKeyPem" .= encodePublicVerifKeyPEM mat
|
|
<> "isShared" .= ownerShared owner
|
|
where
|
|
mkOwner h OwnerInstance = FedURI h "" ""
|
|
mkOwner h (OwnerActor lu) = l2f h lu
|
|
|
|
parsePublicKeySet :: Value -> Parser (Text, [Either LocalURI PublicKey])
|
|
parsePublicKeySet v =
|
|
case v of
|
|
Array a ->
|
|
case V.toList a of
|
|
[] -> fail "No public keys"
|
|
k : ks -> do
|
|
(h, e) <- parseKey k
|
|
es <- traverse (withHost h . parseKey) ks
|
|
return (h, e : es)
|
|
_ -> second (: []) <$> parseKey v
|
|
where
|
|
parseKey (String t) = second Left . f2l <$> either fail return (parseFedURI t)
|
|
parseKey (Object o) = second Right <$> parseObject o
|
|
parseKey v = typeMismatch "PublicKeySet Item" v
|
|
|
|
encodePublicKeySet :: Text -> [Either LocalURI PublicKey] -> Encoding
|
|
encodePublicKeySet host es =
|
|
case es of
|
|
[e] -> renderKey e
|
|
_ -> listEncoding renderKey es
|
|
where
|
|
renderKey (Left lu) = toEncoding $ l2f host lu
|
|
renderKey (Right pk) = pairs $ toSeries host pk
|
|
|
|
data Actor = Actor
|
|
{ actorId :: LocalURI
|
|
, actorType :: ActorType
|
|
, actorUsername :: Maybe Text
|
|
, actorName :: Maybe Text
|
|
, actorSummary :: Maybe Text
|
|
, actorInbox :: LocalURI
|
|
, actorOutbox :: Maybe LocalURI
|
|
, actorFollowers :: Maybe LocalURI
|
|
, actorPublicKeys :: [Either LocalURI PublicKey]
|
|
}
|
|
|
|
instance ActivityPub Actor where
|
|
jsonldContext _ = [as2Context, secContext, extContext]
|
|
parseObject o = do
|
|
(host, id_) <- f2l <$> o .: "id"
|
|
fmap (host,) $
|
|
Actor id_
|
|
<$> o .: "type"
|
|
<*> o .:? "preferredUsername"
|
|
<*> o .:? "name"
|
|
<*> o .:? "summary"
|
|
<*> withHost host (f2l <$> o .: "inbox")
|
|
<*> withHostMaybe host (fmap f2l <$> o .:? "outbox")
|
|
<*> withHostMaybe host (fmap f2l <$> o .:? "followers")
|
|
<*> withHost host (parsePublicKeySet =<< o .: "publicKey")
|
|
toSeries host
|
|
(Actor id_ typ musername mname msummary inbox outbox followers pkeys)
|
|
= "id" .= l2f host id_
|
|
<> "type" .= typ
|
|
<> "preferredUsername" .=? musername
|
|
<> "name" .=? mname
|
|
<> "summary" .=? msummary
|
|
<> "inbox" .= l2f host inbox
|
|
<> "outbox" .=? (l2f host <$> outbox)
|
|
<> "followers" .=? (l2f host <$> followers)
|
|
<> "publicKey" `pair` encodePublicKeySet host pkeys
|
|
|
|
data Project = Project
|
|
{ projectActor :: Actor
|
|
, projectTeam :: LocalURI
|
|
}
|
|
|
|
instance ActivityPub Project where
|
|
jsonldContext _ = [as2Context, secContext, forgeContext, extContext]
|
|
parseObject o = do
|
|
(h, a) <- parseObject o
|
|
unless (actorType a == ActorTypeProject) $
|
|
fail "Actor type isn't Project"
|
|
fmap (h,) $
|
|
Project a
|
|
<$> withHost h (f2l <$> o .:| "team")
|
|
toSeries host (Project actor team)
|
|
= toSeries host actor
|
|
<> "team" .= l2f host team
|
|
|
|
data CollectionType = CollectionTypeUnordered | CollectionTypeOrdered
|
|
|
|
instance FromJSON CollectionType where
|
|
parseJSON = withText "CollectionType" parse
|
|
where
|
|
parse "Collection" = pure CollectionTypeUnordered
|
|
parse "OrderedCollection" = pure CollectionTypeOrdered
|
|
parse t = fail $ "Unknown collection type: " ++ T.unpack t
|
|
|
|
instance ToJSON CollectionType where
|
|
toJSON = error "toJSON CollectionType"
|
|
toEncoding ct =
|
|
toEncoding $ case ct of
|
|
CollectionTypeUnordered -> "Collection" :: Text
|
|
CollectionTypeOrdered -> "OrderedCollection"
|
|
|
|
data Collection a = Collection
|
|
{ collectionId :: LocalURI
|
|
, collectionType :: CollectionType
|
|
, collectionTotalItems :: Maybe Int
|
|
, collectionCurrent :: Maybe LocalURI
|
|
, collectionFirst :: Maybe LocalPageURI
|
|
, collectionLast :: Maybe LocalPageURI
|
|
, collectionItems :: [a]
|
|
}
|
|
|
|
instance (FromJSON a, ToJSON a) => ActivityPub (Collection a) where
|
|
jsonldContext _ = [as2Context, forgeContext, extContext]
|
|
parseObject o = do
|
|
(host, id_) <- f2l <$> o .: "id"
|
|
fmap (host,) $
|
|
Collection id_
|
|
<$> o .: "type"
|
|
<*> o .:? "totalItems"
|
|
<*> withHostMaybe host (fmap f2l <$> o .:? "current")
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "first")
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "last")
|
|
<*> optional (o .: "items" <|> o .: "orderedItems") .!= []
|
|
toSeries host (Collection id_ typ total curr firzt last items)
|
|
= "id" .= l2f host id_
|
|
<> "type" .= typ
|
|
<> "totalItems" .=? total
|
|
<> "current" .=? (l2f host <$> curr)
|
|
<> "first" .=? (lp2fp host <$> firzt)
|
|
<> "last" .=? (lp2fp host <$> last)
|
|
<> "items" .=% items
|
|
|
|
data CollectionPageType
|
|
= CollectionPageTypeUnordered
|
|
| CollectionPageTypeOrdered
|
|
|
|
instance FromJSON CollectionPageType where
|
|
parseJSON = withText "CollectionPageType" parse
|
|
where
|
|
parse "CollectionPage" = pure CollectionPageTypeUnordered
|
|
parse "OrderedCollectionPage" = pure CollectionPageTypeOrdered
|
|
parse t = fail $ "Unknown collection page type: " ++ T.unpack t
|
|
|
|
instance ToJSON CollectionPageType where
|
|
toJSON = error "toJSON CollectionPageType"
|
|
toEncoding ct =
|
|
toEncoding $ case ct of
|
|
CollectionPageTypeUnordered -> "CollectionPage" :: Text
|
|
CollectionPageTypeOrdered -> "OrderedCollectionPage"
|
|
|
|
data CollectionPage a = CollectionPage
|
|
{ collectionPageId :: LocalPageURI
|
|
, collectionPageType :: CollectionPageType
|
|
, collectionPageTotalItems :: Maybe Int
|
|
, collectionPageCurrent :: Maybe LocalPageURI
|
|
, collectionPageFirst :: Maybe LocalPageURI
|
|
, collectionPageLast :: Maybe LocalPageURI
|
|
, collectionPagePartOf :: LocalURI
|
|
, collectionPagePrev :: Maybe LocalPageURI
|
|
, collectionPageNext :: Maybe LocalPageURI
|
|
, collectionPageStartIndex :: Maybe Int
|
|
, collectionPageItems :: [a]
|
|
}
|
|
|
|
instance (FromJSON a, ToJSON a) => ActivityPub (CollectionPage a) where
|
|
jsonldContext _ = [as2Context, forgeContext, extContext]
|
|
parseObject o = do
|
|
(host, id_) <- fp2lp <$> o .: "id"
|
|
fmap (host,) $
|
|
CollectionPage id_
|
|
<$> o .: "type"
|
|
<*> o .:? "totalItems"
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "current")
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "first")
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "last")
|
|
<*> withHost host (f2l <$> o .: "partOf")
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "prev")
|
|
<*> withHostMaybe host (fmap fp2lp <$> o .:? "next")
|
|
<*> o .:? "startIndex"
|
|
<*> optional (o .: "items" <|> o .: "orderedItems") .!= []
|
|
toSeries host (CollectionPage id_ typ total curr firzt last partOf prev next ind items)
|
|
= "id" .= lp2fp host id_
|
|
<> "type" .= typ
|
|
<> "totalItems" .=? total
|
|
<> "current" .=? (lp2fp host <$> curr)
|
|
<> "first" .=? (lp2fp host <$> firzt)
|
|
<> "last" .=? (lp2fp host <$> last)
|
|
<> "partOf" .= (l2f host partOf)
|
|
<> "prev" .=? (lp2fp host <$> prev)
|
|
<> "next" .=? (lp2fp host <$> next)
|
|
<> "startIndex" .=? ind
|
|
<> "items" .=% items
|
|
|
|
data Recipient = RecipientActor Actor | RecipientCollection (Collection FedURI)
|
|
|
|
instance ActivityPub Recipient where
|
|
jsonldContext _ = [as2Context, secContext, forgeContext, extContext]
|
|
parseObject o =
|
|
second RecipientActor <$> parseObject o <|>
|
|
second RecipientCollection <$> parseObject o
|
|
toSeries h (RecipientActor a) = toSeries h a
|
|
toSeries h (RecipientCollection c) = toSeries h c
|
|
|
|
data Audience = Audience
|
|
{ audienceTo :: [FedURI]
|
|
, audienceBto :: [FedURI]
|
|
, audienceCc :: [FedURI]
|
|
, audienceBcc :: [FedURI]
|
|
, audienceGeneral :: [FedURI]
|
|
, audienceNonActors :: [FedURI]
|
|
}
|
|
|
|
newtype AdaptAudience = AdaptAudience
|
|
{ unAdapt :: FedURI
|
|
}
|
|
|
|
instance FromJSON AdaptAudience where
|
|
parseJSON = fmap AdaptAudience . parseJSON . adapt
|
|
where
|
|
adapt v =
|
|
case v of
|
|
String t
|
|
| t == "Public" -> String publicT
|
|
| t == "as:Public" -> String publicT
|
|
_ -> v
|
|
|
|
parseAudience :: Object -> Parser Audience
|
|
parseAudience o =
|
|
Audience
|
|
<$> o .:& "to"
|
|
<*> o .:& "bto"
|
|
<*> o .:& "cc"
|
|
<*> o .:& "bcc"
|
|
<*> o .:& "audience"
|
|
<*> o .:|& "nonActors"
|
|
where
|
|
obj .:& key = do
|
|
l <- obj .:? key .!= []
|
|
return $ map unAdapt l
|
|
obj .:|& key = do
|
|
l <- obj .:|? key .!= []
|
|
return $ map unAdapt l
|
|
|
|
encodeAudience :: Audience -> Series
|
|
encodeAudience (Audience to bto cc bcc aud nons)
|
|
= "to" .=% to
|
|
<> "bto" .=% bto
|
|
<> "cc" .=% cc
|
|
<> "bcc" .=% bcc
|
|
<> "audience" .=% aud
|
|
<> "nonActors" .=% nons
|
|
|
|
data Note = Note
|
|
{ noteId :: Maybe LocalURI
|
|
, noteAttrib :: LocalURI
|
|
, noteAudience :: Audience
|
|
, noteReplyTo :: Maybe FedURI
|
|
, noteContext :: Maybe FedURI
|
|
, notePublished :: Maybe UTCTime
|
|
, noteSource :: Text
|
|
, noteContent :: Text
|
|
}
|
|
|
|
withHost h a = do
|
|
(h', v) <- a
|
|
if h == h'
|
|
then return v
|
|
else fail "URI host mismatch"
|
|
|
|
withHostMaybe h a = do
|
|
mp <- a
|
|
for mp $ \ (h', v) ->
|
|
if h == h'
|
|
then return v
|
|
else fail "URI host mismatch"
|
|
|
|
instance ActivityPub Note where
|
|
jsonldContext _ = [as2Context, extContext]
|
|
parseObject o = do
|
|
typ <- o .: "type"
|
|
unless (typ == ("Note" :: Text)) $
|
|
fail "type isn't Note"
|
|
|
|
mediaType <- o .: "mediaType"
|
|
unless (mediaType == ("text/html" :: Text)) $
|
|
fail "mediaType isn't HTML"
|
|
|
|
source <- o .: "source"
|
|
sourceType <- source .: "mediaType"
|
|
unless (sourceType == ("text/markdown; variant=Pandoc" :: Text)) $
|
|
fail "source mediaType isn't Pandoc Markdown"
|
|
|
|
(h, attrib) <- f2l <$> o .: "attributedTo"
|
|
fmap (h,) $
|
|
Note
|
|
<$> withHostMaybe h (fmap f2l <$> o .:? "id")
|
|
<*> pure attrib
|
|
<*> parseAudience o
|
|
<*> o .:? "inReplyTo"
|
|
<*> o .:? "context"
|
|
<*> o .:? "published"
|
|
<*> source .: "content"
|
|
<*> (sanitizeBalance <$> o .: "content")
|
|
toSeries host (Note mid attrib aud mreply mcontext mpublished src content)
|
|
= "type" .= ("Note" :: Text)
|
|
<> "id" .=? (l2f host <$> mid)
|
|
<> "attributedTo" .= l2f host attrib
|
|
<> encodeAudience aud
|
|
<> "inReplyTo" .=? mreply
|
|
<> "context" .=? mcontext
|
|
<> "published" .=? mpublished
|
|
<> "source" .= object
|
|
[ "content" .= src
|
|
, "mediaType" .= ("text/markdown; variant=Pandoc" :: Text)
|
|
]
|
|
<> "content" .= content
|
|
<> "mediaType" .= ("text/html" :: Text)
|
|
|
|
newtype TextHtml = TextHtml
|
|
{ unTextHtml :: Text
|
|
}
|
|
deriving (FromJSON, ToJSON)
|
|
|
|
newtype TextPandocMarkdown = TextPandocMarkdown
|
|
{ unTextPandocMarkdown :: Text
|
|
}
|
|
deriving (FromJSON, ToJSON)
|
|
|
|
data TicketLocal = TicketLocal
|
|
{ ticketId :: LocalURI
|
|
, ticketContext :: LocalURI
|
|
, ticketReplies :: LocalURI
|
|
, ticketParticipants :: LocalURI
|
|
, ticketTeam :: LocalURI
|
|
, ticketEvents :: LocalURI
|
|
}
|
|
|
|
parseTicketLocal :: Object -> Parser (Maybe (Text, TicketLocal))
|
|
parseTicketLocal o = do
|
|
mid <- fmap f2l <$> o .:? "id"
|
|
case mid of
|
|
Nothing -> do
|
|
verifyNothing "context"
|
|
verifyNothing "replies"
|
|
verifyNothing "participants"
|
|
verifyNothing "team"
|
|
verifyNothing "history"
|
|
return Nothing
|
|
Just (h, id_) ->
|
|
fmap (Just . (h,)) $
|
|
TicketLocal
|
|
<$> pure id_
|
|
<*> withHost h (f2l <$> o .: "context")
|
|
<*> withHost h (f2l <$> o .: "replies")
|
|
<*> withHost h (f2l <$> o .: "participants")
|
|
<*> withHost h (f2l <$> o .: "team")
|
|
<*> withHost h (f2l <$> o .: "history")
|
|
where
|
|
verifyNothing t =
|
|
if t `M.member` o
|
|
then fail $ T.unpack t ++ " field found, expected none"
|
|
else return ()
|
|
|
|
encodeTicketLocal :: Text -> TicketLocal -> Series
|
|
encodeTicketLocal h (TicketLocal id_ context replies participants team events)
|
|
= "id" .= l2f h id_
|
|
<> "context" .= l2f h context
|
|
<> "replies" .= l2f h replies
|
|
<> "participants" .= l2f h participants
|
|
<> "team" .= l2f h team
|
|
<> "history" .= l2f h events
|
|
|
|
data Ticket = Ticket
|
|
{ ticketLocal :: Maybe (Text, TicketLocal)
|
|
, ticketAttributedTo :: LocalURI
|
|
, ticketPublished :: Maybe UTCTime
|
|
, ticketUpdated :: Maybe UTCTime
|
|
, ticketName :: Maybe Text
|
|
, ticketSummary :: TextHtml
|
|
, ticketContent :: TextHtml
|
|
, ticketSource :: TextPandocMarkdown
|
|
, ticketAssignedTo :: Maybe FedURI
|
|
, ticketIsResolved :: Bool
|
|
, ticketDependsOn :: [FedURI]
|
|
, ticketDependedBy :: [FedURI]
|
|
}
|
|
|
|
instance ActivityPub Ticket where
|
|
jsonldContext _ = [as2Context, forgeContext, extContext]
|
|
parseObject o = do
|
|
typ <- o .: "type"
|
|
unless (typ == ("Ticket" :: Text)) $
|
|
fail "type isn't Ticket"
|
|
|
|
mediaType <- o .: "mediaType"
|
|
unless (mediaType == ("text/html" :: Text)) $
|
|
fail "mediaType isn't HTML"
|
|
|
|
source <- o .: "source"
|
|
sourceType <- source .: "mediaType"
|
|
unless (sourceType == ("text/markdown; variant=Pandoc" :: Text)) $
|
|
fail "source mediaType isn't Pandoc Markdown"
|
|
|
|
(h, attributedTo) <- f2l <$> o .: "attributedTo"
|
|
|
|
fmap (h,) $
|
|
Ticket
|
|
<$> parseTicketLocal o
|
|
<*> pure attributedTo
|
|
<*> o .:? "published"
|
|
<*> o .:? "updated"
|
|
<*> o .:? "name"
|
|
<*> (TextHtml . sanitizeBalance <$> o .: "summary")
|
|
<*> (TextHtml . sanitizeBalance <$> o .: "content")
|
|
<*> source .: "content"
|
|
<*> o .:? "assignedTo"
|
|
<*> o .: "isResolved"
|
|
<*> o .:? "dependsOn" .!= []
|
|
<*> o .:? "dependedBy" .!= []
|
|
|
|
toSeries host
|
|
(Ticket local attributedTo published updated name summary content
|
|
source assignedTo isResolved dependsOn dependedBy)
|
|
|
|
= maybe mempty (uncurry encodeTicketLocal) local
|
|
<> "type" .= ("Ticket" :: Text)
|
|
<> "attributedTo" .= l2f host attributedTo
|
|
<> "published" .=? published
|
|
<> "updated" .=? updated
|
|
<> "name" .=? name
|
|
<> "summary" .= summary
|
|
<> "content" .= content
|
|
<> "mediaType" .= ("text/html" :: Text)
|
|
<> "source" .= object
|
|
[ "content" .= source
|
|
, "mediaType" .= ("text/markdown; variant=Pandoc" :: Text)
|
|
]
|
|
<> "assignedTo" .=? assignedTo
|
|
<> "isResolved" .= isResolved
|
|
<> "dependsOn" .=% dependsOn
|
|
<> "dependedBy" .=% dependedBy
|
|
|
|
data Accept = Accept
|
|
{ acceptObject :: FedURI
|
|
}
|
|
|
|
parseAccept :: Object -> Parser Accept
|
|
parseAccept o = Accept <$> o .: "object"
|
|
|
|
encodeAccept :: Accept -> Series
|
|
encodeAccept (Accept obj) = "object" .= obj
|
|
|
|
data Create = Create
|
|
{ createObject :: Note
|
|
}
|
|
|
|
parseCreate :: Object -> Text -> LocalURI -> Parser Create
|
|
parseCreate o h luActor = do
|
|
note <- withHost h $ parseObject =<< o .: "object"
|
|
unless (luActor == noteAttrib note) $ fail "Create actor != Note attrib"
|
|
return $ Create note
|
|
|
|
encodeCreate :: Text -> LocalURI -> Create -> Series
|
|
encodeCreate host actor (Create obj) =
|
|
"object" `pair` pairs (toSeries host obj)
|
|
|
|
data Follow = Follow
|
|
{ followObject :: FedURI
|
|
, followHide :: Bool
|
|
}
|
|
|
|
parseFollow :: Object -> Parser Follow
|
|
parseFollow o =
|
|
Follow
|
|
<$> o .: "object"
|
|
<*> o .: "hide"
|
|
|
|
encodeFollow :: Follow -> Series
|
|
encodeFollow (Follow obj hide)
|
|
= "object" .= obj
|
|
<> "hide" .= hide
|
|
|
|
data Offer = Offer
|
|
{ offerObject :: Ticket
|
|
, offerTarget :: FedURI
|
|
}
|
|
|
|
parseOffer :: Object -> Text -> LocalURI -> Parser Offer
|
|
parseOffer o h luActor = do
|
|
ticket <- withHost h $ parseObject =<< o .: "object"
|
|
unless (luActor == ticketAttributedTo ticket) $
|
|
fail "Offer actor != Ticket attrib"
|
|
target <- o .: "target"
|
|
for_ (ticketLocal ticket) $ \ (host, local) -> do
|
|
let (hTarget, luTarget) = f2l target
|
|
unless (hTarget == host) $
|
|
fail "Offer target host != Ticket local host"
|
|
unless (luTarget == ticketContext local) $
|
|
fail "Offer target != Ticket context"
|
|
return $ Offer ticket target
|
|
|
|
encodeOffer :: Text -> LocalURI -> Offer -> Series
|
|
encodeOffer host actor (Offer obj target)
|
|
= "object" `pair` pairs (toSeries host obj)
|
|
<> "target" .= target
|
|
|
|
data Reject = Reject
|
|
{ rejectObject :: FedURI
|
|
}
|
|
|
|
parseReject :: Object -> Parser Reject
|
|
parseReject o = Reject <$> o .: "object"
|
|
|
|
encodeReject :: Reject -> Series
|
|
encodeReject (Reject obj) = "object" .= obj
|
|
|
|
data SpecificActivity
|
|
= AcceptActivity Accept
|
|
| CreateActivity Create
|
|
| FollowActivity Follow
|
|
| OfferActivity Offer
|
|
| RejectActivity Reject
|
|
|
|
data Activity = Activity
|
|
{ activityId :: LocalURI
|
|
, activityActor :: LocalURI
|
|
, activityAudience :: Audience
|
|
, activitySpecific :: SpecificActivity
|
|
}
|
|
|
|
instance ActivityPub Activity where
|
|
jsonldContext _ = [as2Context, forgeContext, extContext]
|
|
parseObject o = do
|
|
(h, id_) <- f2l <$> o .: "id"
|
|
actor <- withHost h $ f2l <$> o .: "actor"
|
|
fmap (h,) $
|
|
Activity id_ actor
|
|
<$> parseAudience o
|
|
<*> do
|
|
typ <- o .: "type"
|
|
case typ of
|
|
"Accept" -> AcceptActivity <$> parseAccept o
|
|
"Create" -> CreateActivity <$> parseCreate o h actor
|
|
"Follow" -> FollowActivity <$> parseFollow o
|
|
"Offer" -> OfferActivity <$> parseOffer o h actor
|
|
"Reject" -> RejectActivity <$> parseReject o
|
|
_ ->
|
|
fail $
|
|
"Unrecognized activity type: " ++ T.unpack typ
|
|
toSeries host (Activity id_ actor audience specific)
|
|
= "type" .= activityType specific
|
|
<> "id" .= l2f host id_
|
|
<> "actor" .= l2f host actor
|
|
<> encodeAudience audience
|
|
<> encodeSpecific host actor specific
|
|
where
|
|
activityType :: SpecificActivity -> Text
|
|
activityType (AcceptActivity _) = "Accept"
|
|
activityType (CreateActivity _) = "Create"
|
|
activityType (FollowActivity _) = "Follow"
|
|
activityType (OfferActivity _) = "Offer"
|
|
activityType (RejectActivity _) = "Reject"
|
|
encodeSpecific _ _ (AcceptActivity a) = encodeAccept a
|
|
encodeSpecific h u (CreateActivity a) = encodeCreate h u a
|
|
encodeSpecific _ _ (FollowActivity a) = encodeFollow a
|
|
encodeSpecific h u (OfferActivity a) = encodeOffer h u a
|
|
encodeSpecific _ _ (RejectActivity a) = encodeReject a
|
|
|
|
typeActivityStreams2 :: ContentType
|
|
typeActivityStreams2 = "application/activity+json"
|
|
|
|
typeActivityStreams2LD :: ContentType
|
|
typeActivityStreams2LD =
|
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
|
|
|
hActivityPubActor :: HeaderName
|
|
hActivityPubActor = "ActivityPub-Actor"
|
|
|
|
provideAP :: (Monad m, ToJSON a) => m a -> Writer (Endo [ProvidedRep m]) ()
|
|
provideAP mk =
|
|
-- let enc = toEncoding v
|
|
-- provideRepType typeActivityStreams2 $ return enc
|
|
provideRepType typeActivityStreams2LD $ toEncoding <$> mk
|
|
|
|
data APGetError
|
|
= APGetErrorHTTP HttpException
|
|
| APGetErrorJSON JSONException
|
|
| APGetErrorContentType Text
|
|
deriving Show
|
|
|
|
instance Exception APGetError
|
|
|
|
-- | Perform an HTTP GET request to fetch an ActivityPub object.
|
|
--
|
|
-- * Verify the URI scheme is _https:_ and authority part is present
|
|
-- * Set _Accept_ request header
|
|
-- * Perform the GET request
|
|
-- * Verify the _Content-Type_ response header
|
|
-- * Parse the JSON response body
|
|
httpGetAP
|
|
:: (MonadIO m, FromJSON a)
|
|
=> Manager
|
|
-> FedURI
|
|
-> m (Either APGetError (Response a))
|
|
httpGetAP manager uri =
|
|
liftIO $
|
|
mkResult <$> try (httpAPEither manager =<< requestFromURI (toURI uri))
|
|
where
|
|
lookup' x = map snd . filter ((== x) . fst)
|
|
mkResult (Left e) = Left $ APGetErrorHTTP e
|
|
mkResult (Right r) =
|
|
case lookup' hContentType $ responseHeaders r of
|
|
[] -> Left $ APGetErrorContentType "No Content-Type"
|
|
[b] -> if b == typeActivityStreams2LD || b == typeActivityStreams2
|
|
then case responseBody r of
|
|
Left e -> Left $ APGetErrorJSON e
|
|
Right v -> Right $ v <$ r
|
|
else Left $ APGetErrorContentType $ "Non-AP Content-Type: " <> decodeUtf8 b
|
|
_ -> Left $ APGetErrorContentType "Multiple Content-Type"
|
|
|
|
data APPostError
|
|
= APPostErrorSig S.HttpSigGenError
|
|
| APPostErrorHTTP HttpException
|
|
deriving Show
|
|
|
|
instance Exception APPostError
|
|
|
|
hActivityPubForwarder :: HeaderName
|
|
hActivityPubForwarder = "ActivityPub-Forwarder"
|
|
|
|
hForwardingSignature :: HeaderName
|
|
hForwardingSignature = "Forwarding-Signature"
|
|
|
|
hForwardedSignature :: HeaderName
|
|
hForwardedSignature = "Forwarded-Signature"
|
|
|
|
-- | Perform an HTTP POST request to submit an ActivityPub object.
|
|
--
|
|
-- * Verify the URI scheme is _https:_ and authority part is present
|
|
-- * Set _Content-Type_ request header
|
|
-- * Set _ActivityPub-Actor_ request header
|
|
-- * Set _Digest_ request header using SHA-256 hash
|
|
-- * If recipient is given, set _ActivityPub-Forwarder_ header and compute
|
|
-- _Forwarding-Signature_ header
|
|
-- * If forwarded signature is given, set set _ActivityPub-Forwarder_ and
|
|
-- _Forwarded-Signature_ headers
|
|
-- * Compute HTTP signature and add _Signature_ request header
|
|
-- * Perform the POST request
|
|
-- * Verify the response status is 2xx
|
|
httpPostAP
|
|
:: (MonadIO m, ToJSON a)
|
|
=> Manager
|
|
-> FedURI
|
|
-> NonEmpty HeaderName
|
|
-> S.KeyId
|
|
-> (ByteString -> S.Signature)
|
|
-> Text
|
|
-> Maybe (Either FedURI ByteString)
|
|
-> a
|
|
-> m (Either APPostError (Response ()))
|
|
httpPostAP manager uri headers keyid sign uSender mfwd value =
|
|
httpPostAPBytes manager uri headers keyid sign uSender mfwd $ encode value
|
|
|
|
-- | Like 'httpPostAP', except it takes the object as a raw lazy
|
|
-- 'BL.ByteString'. It's your responsibility to make sure it's valid JSON.
|
|
httpPostAPBytes
|
|
:: MonadIO m
|
|
=> Manager
|
|
-> FedURI
|
|
-> NonEmpty HeaderName
|
|
-> S.KeyId
|
|
-> (ByteString -> S.Signature)
|
|
-> Text
|
|
-> Maybe (Either FedURI ByteString)
|
|
-> BL.ByteString
|
|
-> m (Either APPostError (Response ()))
|
|
httpPostAPBytes manager uri headers keyid sign uSender mfwd body =
|
|
liftIO $ runExceptT $ do
|
|
req <- requestFromURI $ toURI uri
|
|
let digest = formatHttpBodyDigest SHA256 "SHA-256" $ hashlazy body
|
|
req' =
|
|
setRequestCheckStatus $
|
|
consHeader hContentType typeActivityStreams2LD $
|
|
consHeader hActivityPubActor (encodeUtf8 uSender) $
|
|
consHeader hDigest digest $
|
|
req { method = "POST"
|
|
, requestBody = RequestBodyLBS body
|
|
}
|
|
req'' <- tryExceptT APPostErrorSig $ signRequest headers Nothing keyid sign Nothing req'
|
|
req''' <-
|
|
case mfwd of
|
|
Nothing -> return req''
|
|
Just (Left uRecip) ->
|
|
tryExceptT APPostErrorSig $
|
|
signRequestInto hForwardingSignature (hDigest :| [hActivityPubForwarder]) Nothing keyid sign Nothing $ consHeader hActivityPubForwarder (encodeUtf8 $ renderFedURI uRecip) req''
|
|
Just (Right sig) ->
|
|
return $
|
|
consHeader hForwardedSignature sig $
|
|
consHeader hActivityPubForwarder (encodeUtf8 uSender)
|
|
req''
|
|
tryExceptT APPostErrorHTTP $ httpNoBody req''' manager
|
|
where
|
|
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
|
|
tryExceptT adapt action = ExceptT $ first adapt <$> try action
|
|
|
|
-- | Result of GETing the keyId URI and processing the JSON document.
|
|
data Fetched = Fetched
|
|
{ fetchedPublicKey :: PublicVerifKey
|
|
-- ^ The Ed25519 or RSA public key corresponding to the URI we requested.
|
|
, fetchedKeyExpires :: Maybe UTCTime
|
|
-- ^ Optional expiration time declared for the key we received.
|
|
, fetchedActorId :: LocalURI
|
|
-- ^ The @id URI of the actor for whom the key's signature applies.
|
|
, fetchedActorName :: Maybe Text
|
|
-- ^ Name of the actor for whom the key's signature applies.
|
|
, fetchedActorInbox :: LocalURI
|
|
-- ^ The inbox URI of the actor for whom the key's signature applies.
|
|
, fetchedKeyShared :: Bool
|
|
-- ^ Whether the key we received is shared. A shared key can sign
|
|
-- requests for any actor on the same instance, while a personal key is
|
|
-- only for one actor. Knowing whether the key is shared will allow us
|
|
-- when receiving more requests, whether to accept signatures made on
|
|
-- different actors, or allow only a single permanent actor for the key
|
|
-- we received.
|
|
}
|
|
|
|
fetchAP' :: (MonadIO m, FromJSON a) => Manager -> FedURI -> ExceptT APGetError m a
|
|
fetchAP' m u = ExceptT $ second responseBody <$> httpGetAP m u
|
|
|
|
fetchAP :: (MonadIO m, FromJSON a) => Manager -> FedURI -> ExceptT String m a
|
|
fetchAP m u = withExceptT displayException $ fetchAP' m u
|
|
|
|
{-
|
|
fetchAPH :: (MonadIO m, ActivityPub a) => Manager -> Text -> LocalURI -> ExceptT String m a
|
|
fetchAPH m h lu = do
|
|
Doc h' v <- fetchAP m $ l2f h lu
|
|
if h == h'
|
|
then return v
|
|
else throwE "Object @id URI's host doesn't match the URI we fetched"
|
|
-}
|
|
|
|
fetchAPID' :: (MonadIO m, ActivityPub a) => Manager -> (a -> LocalURI) -> Text -> LocalURI -> m (Either (Maybe APGetError) a)
|
|
fetchAPID' m getId h lu = runExceptT $ do
|
|
Doc h' v <- withExceptT Just $ fetchAP' m $ l2f h lu
|
|
if h == h' && getId v == lu
|
|
then return v
|
|
else throwE Nothing
|
|
|
|
fetchRecipient :: MonadIO m => Manager -> Text -> LocalURI -> m (Either (Maybe APGetError) Recipient)
|
|
fetchRecipient m = fetchAPID' m getId
|
|
where
|
|
getId (RecipientActor a) = actorId a
|
|
getId (RecipientCollection c) = collectionId c
|
|
|
|
fetchAPID :: (MonadIO m, ActivityPub a) => Manager -> (a -> LocalURI) -> Text -> LocalURI -> m (Either String a)
|
|
fetchAPID m getId h lu = first showError <$> fetchAPID' m getId h lu
|
|
where
|
|
showError Nothing = "Object @id doesn't match the URI we fetched"
|
|
showError (Just e) = displayException e
|
|
|
|
data FetchAPError
|
|
= FetchAPErrorGet APGetError
|
|
-- Object @id doesn't match the URI we fetched
|
|
| FetchAPErrorIdMismatch
|
|
-- Object @id URI's host doesn't match the URI we fetched
|
|
| FetchAPErrorHostMismatch
|
|
deriving Show
|
|
|
|
fetchAPIDOrH'
|
|
:: (MonadIO m, ActivityPub a, ActivityPub b)
|
|
=> Manager
|
|
-> (a -> LocalURI)
|
|
-> Text
|
|
-> LocalURI
|
|
-> ExceptT FetchAPError m (Either a b)
|
|
fetchAPIDOrH' m getId h lu = do
|
|
e <- withExceptT FetchAPErrorGet $ fetchAP' m $ l2f h lu
|
|
case e of
|
|
Left' (Doc h' x) ->
|
|
if h == h' && getId x == lu
|
|
then return $ Left x
|
|
else throwE FetchAPErrorIdMismatch
|
|
Right' (Doc h' y) ->
|
|
if h == h'
|
|
then return $ Right y
|
|
else throwE FetchAPErrorHostMismatch
|
|
|
|
fetchAPIDOrH
|
|
:: (MonadIO m, ActivityPub a, ActivityPub b)
|
|
=> Manager
|
|
-> (a -> LocalURI)
|
|
-> Text
|
|
-> LocalURI
|
|
-> ExceptT String m (Either a b)
|
|
fetchAPIDOrH m getId h lu = withExceptT show $ fetchAPIDOrH' m getId h lu
|
|
|
|
-- | Fetches the given actor and checks whether it lists the given key (as a
|
|
-- URI, not as an embedded object). If it does, returns 'Right' the fetched
|
|
-- actor. Otherwise, or if an error occurs during fetching, returns 'Left' an
|
|
-- error message.
|
|
keyListedByActor :: MonadIO m => Manager -> Text -> LocalURI -> LocalURI -> m (Either String Actor)
|
|
keyListedByActor manager host luKey luActor = runExceptT $ do
|
|
actor <- ExceptT $ fetchAPID manager actorId host luActor
|
|
if keyUriListed luKey actor
|
|
then return actor
|
|
else throwE "Actor publicKey has no URI matching pkey @id"
|
|
where
|
|
keyUriListed uk a =
|
|
let match (Left uri) = uri == uk
|
|
match (Right _) = False
|
|
in any match $ actorPublicKeys a
|
|
|
|
matchKeyObj :: (Foldable f, Monad m) => LocalURI -> f (Either LocalURI PublicKey) -> ExceptT String m PublicKey
|
|
matchKeyObj luKey es =
|
|
case find' (match luKey) es of
|
|
Nothing -> throwE "keyId resolved to actor which doesn't have a key object with that ID"
|
|
Just pk -> return pk
|
|
where
|
|
find' :: Foldable f => (a -> Maybe b) -> f a -> Maybe b
|
|
find' p = join . fmap getFirst . foldMap (Just . First . p)
|
|
match _ (Left _) = Nothing
|
|
match luk (Right pk) =
|
|
if publicKeyId pk == luk
|
|
then Just pk
|
|
else Nothing
|
|
|
|
verifyAlgo :: Maybe S.Algorithm -> PublicVerifKey -> Either String ()
|
|
verifyAlgo Nothing _ = Right ()
|
|
verifyAlgo (Just a) k =
|
|
case a of
|
|
S.AlgorithmEd25519 ->
|
|
case k of
|
|
PublicVerifKeyEd25519 _ -> Right ()
|
|
PublicVerifKeyRSA _ ->
|
|
Left "Algo mismatch, algo is Ed25519 but actual key is RSA"
|
|
S.AlgorithmRsaSha256 ->
|
|
case k of
|
|
PublicVerifKeyEd25519 _ ->
|
|
Left
|
|
"Algo mismatch, algo is RSA-SHA256 but actual key is \
|
|
\Ed25519"
|
|
PublicVerifKeyRSA _ -> Right ()
|
|
S.AlgorithmOther b -> Left $ concat
|
|
[ "Unrecognized algo "
|
|
, BC.unpack b
|
|
, ", actual key is "
|
|
, case k of
|
|
PublicVerifKeyEd25519 _ -> "Ed25519"
|
|
PublicVerifKeyRSA _ -> "RSA"
|
|
]
|
|
|
|
-- | Fetch a key we don't have cached locally.
|
|
fetchUnknownKey
|
|
:: MonadIO m
|
|
=> Manager
|
|
-- ^ Manager for making HTTP requests
|
|
-> Maybe S.Algorithm
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
|
-> Text
|
|
-- ^ Instance host
|
|
-> Maybe LocalURI
|
|
-- ^ Actor URI possibly provided in the HTTP request's actor header
|
|
-> LocalURI
|
|
-- ^ Key URI provided in HTTP signature header
|
|
-> ExceptT String m Fetched
|
|
fetchUnknownKey manager malgo host mluActor luKey = do
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
|
fetched <-
|
|
case obj of
|
|
Left pkey -> do
|
|
(oi, luActor) <-
|
|
case publicKeyOwner pkey of
|
|
OwnerInstance ->
|
|
case mluActor of
|
|
Nothing -> throwE "Key is shared but actor header not specified!"
|
|
Just u -> return (True, u)
|
|
OwnerActor owner -> do
|
|
for_ mluActor $ \ lu ->
|
|
if owner == lu
|
|
then return ()
|
|
else throwE "Key's owner doesn't match actor header"
|
|
return (False, owner)
|
|
actor <- ExceptT $ keyListedByActor manager host luKey luActor
|
|
return Fetched
|
|
{ fetchedPublicKey = publicKeyMaterial pkey
|
|
, fetchedKeyExpires = publicKeyExpires pkey
|
|
, fetchedActorId = luActor
|
|
, fetchedActorName = actorName actor <|> actorUsername actor
|
|
, fetchedActorInbox = actorInbox actor
|
|
, fetchedKeyShared = oi
|
|
}
|
|
Right actor -> do
|
|
if actorId actor == luKey { luriFragment = "" }
|
|
then return ()
|
|
else throwE "Actor ID doesn't match the keyid URI we fetched"
|
|
for_ mluActor $ \ lu ->
|
|
if actorId actor == lu
|
|
then return ()
|
|
else throwE "Key's owner doesn't match actor header"
|
|
pk <- matchKeyObj luKey $ actorPublicKeys actor
|
|
owner <- case publicKeyOwner pk of
|
|
OwnerInstance -> throwE "Actor's publicKey is shared, but embedded in actor document! We allow shared keys only if they're in a separate document"
|
|
OwnerActor owner ->
|
|
if owner == actorId actor
|
|
then return owner
|
|
else throwE "Actor's publicKey's owner doesn't match the actor's ID"
|
|
return Fetched
|
|
{ fetchedPublicKey = publicKeyMaterial pk
|
|
, fetchedKeyExpires = publicKeyExpires pk
|
|
, fetchedActorId = owner
|
|
, fetchedActorName = actorName actor <|> actorUsername actor
|
|
, fetchedActorInbox = actorInbox actor
|
|
, fetchedKeyShared = False
|
|
}
|
|
ExceptT . pure $ verifyAlgo malgo $ fetchedPublicKey fetched
|
|
return fetched
|
|
|
|
keyDetail pk = (publicKeyMaterial pk, publicKeyExpires pk)
|
|
|
|
-- | Fetch a personal key we already have cached locally, but we'd like to
|
|
-- refresh the local copy by fetching the key again from the server.
|
|
fetchKnownPersonalKey
|
|
:: MonadIO m
|
|
=> Manager
|
|
-- ^ Manager for making HTTP requests
|
|
-> Maybe S.Algorithm
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
|
-> Text
|
|
-- ^ Instance host
|
|
-> LocalURI
|
|
-- ^ Key owner actor ID URI
|
|
-> LocalURI
|
|
-- ^ Key URI
|
|
-> ExceptT String m (PublicVerifKey, Maybe UTCTime)
|
|
fetchKnownPersonalKey manager malgo host luOwner luKey = do
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
|
(material, mexpires) <-
|
|
case obj of
|
|
Left pkey -> do
|
|
case publicKeyOwner pkey of
|
|
OwnerInstance -> throwE "Personal key became shared"
|
|
OwnerActor owner ->
|
|
when (luOwner /= owner) $ throwE "Key owner changed"
|
|
return $ keyDetail pkey
|
|
Right actor -> do
|
|
when (actorId actor /= luKey { luriFragment = "" }) $
|
|
throwE "Actor ID doesn't match the keyid URI we fetched"
|
|
when (actorId actor /= luOwner) $
|
|
throwE "Key owner changed"
|
|
pk <- matchKeyObj luKey $ actorPublicKeys actor
|
|
case publicKeyOwner pk of
|
|
OwnerInstance -> throwE "Personal key became shared"
|
|
OwnerActor owner ->
|
|
when (owner /= luOwner) $
|
|
throwE "Actor's publicKey's owner doesn't match the actor's ID"
|
|
return $ keyDetail pk
|
|
ExceptT . pure $ verifyAlgo malgo material
|
|
return (material, mexpires)
|
|
|
|
-- | Fetch a shared key we already have cached locally, but we'd like to
|
|
-- refresh the local copy by fetching the key again from the server.
|
|
fetchKnownSharedKey
|
|
:: MonadIO m
|
|
=> Manager
|
|
-- ^ Manager for making HTTP requests
|
|
-> Maybe S.Algorithm
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
|
-> Text
|
|
-- ^ Instance host
|
|
-> LocalURI
|
|
-- ^ Actor ID from HTTP actor header
|
|
-> LocalURI
|
|
-- ^ Key URI
|
|
-> ExceptT String m (PublicVerifKey, Maybe UTCTime)
|
|
fetchKnownSharedKey manager malgo host luActor luKey = do
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
|
pkey <-
|
|
case obj :: Either PublicKey Actor of
|
|
Left pk -> return pk
|
|
Right _actor -> throwE "Expected stand-alone key, got embedded key"
|
|
case publicKeyOwner pkey of
|
|
OwnerInstance -> return ()
|
|
OwnerActor _owner -> throwE "Shared key became personal"
|
|
let (material, mexpires) = keyDetail pkey
|
|
ExceptT . pure $ verifyAlgo malgo material
|
|
return (material, mexpires)
|