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:
parent
e325175a9c
commit
8ac559d064
12 changed files with 176 additions and 128 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
101
src/Network/FedURI.hs
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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 $
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue