format
This commit is contained in:
parent
10d9a92b12
commit
34590c8a66
23 changed files with 329 additions and 307 deletions
45
app/DB.hs
45
app/DB.hs
|
@ -1,22 +1,20 @@
|
||||||
|
|
||||||
-- needed because of a compiler bug with OverloadedRecordDot:
|
-- needed because of a compiler bug with OverloadedRecordDot:
|
||||||
-- <https://play.haskell.org/saved/Xq0ZFrQi>
|
-- <https://play.haskell.org/saved/Xq0ZFrQi>
|
||||||
{-# language FieldSelectors #-}
|
{-# LANGUAGE FieldSelectors #-}
|
||||||
|
|
||||||
-- | Database interaction
|
-- | Database interaction
|
||||||
module DB
|
module DB (
|
||||||
( module DB
|
module DB,
|
||||||
, DB.Int64
|
DB.Int64,
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
|
import Data.Typeable
|
||||||
import Database.Sqlite.Easy qualified as DB
|
import Database.Sqlite.Easy qualified as DB
|
||||||
import Fedi
|
import Fedi
|
||||||
import GHC.Stack (HasCallStack)
|
import GHC.Stack (HasCallStack)
|
||||||
import Text.RawString.QQ
|
import Text.RawString.QQ
|
||||||
import Control.Monad.IO.Class (liftIO)
|
|
||||||
import Data.Typeable
|
|
||||||
|
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
@ -27,12 +25,14 @@ data DB
|
||||||
{ getNotes :: IO [Note]
|
{ getNotes :: IO [Note]
|
||||||
, getNote :: DB.Int64 -> IO (Maybe Note)
|
, getNote :: DB.Int64 -> IO (Maybe Note)
|
||||||
, insertNote :: NoteEntry -> IO (DB.Int64, Note)
|
, insertNote :: NoteEntry -> IO (DB.Int64, Note)
|
||||||
, -- | We use a callback so we can revert if the operation fails.
|
, insertFollower
|
||||||
insertFollower ::
|
:: forall a
|
||||||
forall a. Typeable a => FollowerEntry -> (DB.Int64 -> IO a) -> IO a
|
. (Typeable a) => FollowerEntry -> (DB.Int64 -> IO a) -> IO a
|
||||||
, -- | We use a callback so we can revert if the operation fails.
|
-- ^ We use a callback so we can revert if the operation fails.
|
||||||
deleteFollower ::
|
, deleteFollower
|
||||||
forall a. Typeable a => FollowerEntry -> (Maybe DB.Int64 -> IO a) -> IO a
|
:: forall a
|
||||||
|
. (Typeable a) => FollowerEntry -> (Maybe DB.Int64 -> IO a) -> IO a
|
||||||
|
-- ^ We use a callback so we can revert if the operation fails.
|
||||||
, getFollowers :: IO [Follower]
|
, getFollowers :: IO [Follower]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,14 +45,14 @@ data NoteEntry
|
||||||
, name :: Maybe String
|
, name :: Maybe String
|
||||||
, url :: Maybe Url
|
, url :: Maybe Url
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving (Show)
|
||||||
|
|
||||||
data FollowerEntry
|
data FollowerEntry
|
||||||
= FollowerEntry
|
= FollowerEntry
|
||||||
{ followId :: T.Text
|
{ followId :: T.Text
|
||||||
, actorId :: T.Text
|
, actorId :: T.Text
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving (Show)
|
||||||
|
|
||||||
data Follower
|
data Follower
|
||||||
= Follower
|
= Follower
|
||||||
|
@ -60,7 +60,7 @@ data Follower
|
||||||
, followId :: T.Text
|
, followId :: T.Text
|
||||||
, actorId :: T.Text
|
, actorId :: T.Text
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving (Show)
|
||||||
|
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
@ -275,6 +275,7 @@ getFollowersSQL url =
|
||||||
|]
|
|]
|
||||||
, [DB.SQLText $ T.pack url]
|
, [DB.SQLText $ T.pack url]
|
||||||
)
|
)
|
||||||
|
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
-- ** Decode row
|
-- ** Decode row
|
||||||
|
@ -292,8 +293,9 @@ decodeNoteRow = \case
|
||||||
] ->
|
] ->
|
||||||
let
|
let
|
||||||
emptyNote = emptyUserNote $ T.unpack actor
|
emptyNote = emptyUserNote $ T.unpack actor
|
||||||
in (noteid,
|
in
|
||||||
emptyNote
|
( noteid
|
||||||
|
, emptyNote
|
||||||
{ id = Just $ ObjectId $ T.unpack noteidurl
|
{ id = Just $ ObjectId $ T.unpack noteidurl
|
||||||
, published = Just $ read (T.unpack published)
|
, published = Just $ read (T.unpack published)
|
||||||
, attributedTo = Just $ LLink $ Link $ T.unpack actor
|
, attributedTo = Just $ LLink $ Link $ T.unpack actor
|
||||||
|
@ -312,7 +314,8 @@ decodeNoteRow = \case
|
||||||
{ id = Just $ ObjectId $ T.unpack noteidurl <> "/shares"
|
{ id = Just $ ObjectId $ T.unpack noteidurl <> "/shares"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
row -> error $ "Couldn't decode row as Note: " <> show row
|
row -> error $ "Couldn't decode row as Note: " <> show row
|
||||||
|
|
||||||
decodeIntRow :: [DB.SQLData] -> DB.Int64
|
decodeIntRow :: [DB.SQLData] -> DB.Int64
|
||||||
|
|
|
@ -58,7 +58,8 @@ notesHtml notes = do
|
||||||
-- | A single post as HTML.
|
-- | A single post as HTML.
|
||||||
noteHtml :: Fedi.Note -> Html
|
noteHtml :: Fedi.Note -> Html
|
||||||
noteHtml note = do
|
noteHtml note = do
|
||||||
let noteid = T.pack (maybe "" (\i -> i.unwrap) note.id)
|
let
|
||||||
|
noteid = T.pack (maybe "" (\i -> i.unwrap) note.id)
|
||||||
H.div_ [H.class_ "note"] $ do
|
H.div_ [H.class_ "note"] $ do
|
||||||
H.div_ [H.class_ "note-header"] $ do
|
H.div_ [H.class_ "note-header"] $ do
|
||||||
case note.name of
|
case note.name of
|
||||||
|
@ -133,7 +134,8 @@ newNoteHtml details = do
|
||||||
)
|
)
|
||||||
|
|
||||||
localDateJs :: String
|
localDateJs :: String
|
||||||
localDateJs = [r|
|
localDateJs =
|
||||||
|
[r|
|
||||||
let collection = document.querySelectorAll(".note-date-published");
|
let collection = document.querySelectorAll(".note-date-published");
|
||||||
|
|
||||||
for (let i = 0; i < collection.length; i++) {
|
for (let i = 0; i < collection.length; i++) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
|
import Control.Logger.Simple qualified as Log
|
||||||
import DB
|
import DB
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Data.Functor ((<&>))
|
import Data.Functor ((<&>))
|
||||||
|
@ -16,7 +17,6 @@ import Network.Wai.Middleware.Routed qualified as Wai
|
||||||
import Routes
|
import Routes
|
||||||
import System.Environment (getArgs, lookupEnv)
|
import System.Environment (getArgs, lookupEnv)
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Control.Logger.Simple qualified as Log
|
|
||||||
|
|
||||||
data Command
|
data Command
|
||||||
= Serve
|
= Serve
|
||||||
|
@ -122,7 +122,8 @@ runServer port authMiddleware app = do
|
||||||
( Logger.logStdoutDev
|
( Logger.logStdoutDev
|
||||||
. Limit.requestSizeLimitMiddleware Limit.defaultRequestSizeLimitSettings
|
. Limit.requestSizeLimitMiddleware Limit.defaultRequestSizeLimitSettings
|
||||||
. auth
|
. auth
|
||||||
) app
|
)
|
||||||
|
app
|
||||||
|
|
||||||
matchAdmin :: [T.Text] -> Bool
|
matchAdmin :: [T.Text] -> Bool
|
||||||
matchAdmin = any (== "admin")
|
matchAdmin = any (== "admin")
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
module Routes where
|
module Routes where
|
||||||
|
|
||||||
|
import Control.Concurrent.Async qualified as Async
|
||||||
|
import Control.Logger.Simple qualified as Log
|
||||||
import DB
|
import DB
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Data.Functor ((<&>))
|
import Data.Functor ((<&>))
|
||||||
import Data.Maybe (maybeToList)
|
import Data.Maybe (maybeToList)
|
||||||
|
import Data.Text qualified as T
|
||||||
import Fedi qualified as Fedi
|
import Fedi qualified as Fedi
|
||||||
import Html
|
import Html
|
||||||
import Lucid qualified as H
|
import Lucid qualified as H
|
||||||
|
import Routes.Inbox.Follow
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Data.Text qualified as T
|
|
||||||
import Control.Concurrent.Async qualified as Async
|
|
||||||
import Control.Logger.Simple qualified as Log
|
|
||||||
import Routes.Inbox.Follow
|
|
||||||
|
|
||||||
routes :: DB -> FilePath -> [Twain.Middleware]
|
routes :: DB -> FilePath -> [Twain.Middleware]
|
||||||
routes db detailsFile =
|
routes db detailsFile =
|
||||||
|
@ -47,12 +47,10 @@ routes db detailsFile =
|
||||||
notes <- map noteToCreate <$> liftIO db.getNotes
|
notes <- map noteToCreate <$> liftIO db.getNotes
|
||||||
|
|
||||||
Fedi.handleCreateNote details notes
|
Fedi.handleCreateNote details notes
|
||||||
|
|
||||||
, -- Match inbox
|
, -- Match inbox
|
||||||
Twain.post (Fedi.matchInbox $ unsafePerformIO $ fetchUserDetails detailsFile) do
|
Twain.post (Fedi.matchInbox $ unsafePerformIO $ fetchUserDetails detailsFile) do
|
||||||
Log.logTrace "Inbox"
|
Log.logTrace "Inbox"
|
||||||
Fedi.handleInbox (handleInbox db detailsFile)
|
Fedi.handleInbox (handleInbox db detailsFile)
|
||||||
|
|
||||||
, -- Match Create object
|
, -- Match Create object
|
||||||
Twain.get (Fedi.matchCreateNote $ unsafePerformIO $ fetchUserDetails detailsFile) do
|
Twain.get (Fedi.matchCreateNote $ unsafePerformIO $ fetchUserDetails detailsFile) do
|
||||||
details <- liftIO $ fetchUserDetails detailsFile
|
details <- liftIO $ fetchUserDetails detailsFile
|
||||||
|
@ -74,11 +72,11 @@ routes db detailsFile =
|
||||||
Nothing -> Twain.next
|
Nothing -> Twain.next
|
||||||
Just thenote ->
|
Just thenote ->
|
||||||
Twain.send $ Twain.html $ H.renderBS $ actorPage details [thenote]
|
Twain.send $ Twain.html $ H.renderBS $ actorPage details [thenote]
|
||||||
|
|
||||||
, -- Followers
|
, -- Followers
|
||||||
Twain.get (Fedi.matchFollowers $ unsafePerformIO $ fetchUserDetails detailsFile) do
|
Twain.get (Fedi.matchFollowers $ unsafePerformIO $ fetchUserDetails detailsFile) do
|
||||||
details <- liftIO $ fetchUserDetails detailsFile
|
details <- liftIO $ fetchUserDetails detailsFile
|
||||||
followers <- liftIO db.getFollowers
|
followers <-
|
||||||
|
liftIO db.getFollowers
|
||||||
<&> map (\follower -> T.unpack follower.actorId)
|
<&> map (\follower -> T.unpack follower.actorId)
|
||||||
Fedi.handleFollowers details followers
|
Fedi.handleFollowers details followers
|
||||||
, -- Following
|
, -- Following
|
||||||
|
@ -89,8 +87,8 @@ routes db detailsFile =
|
||||||
Twain.get Fedi.matchWebfinger do
|
Twain.get Fedi.matchWebfinger do
|
||||||
details <- liftIO $ fetchUserDetails detailsFile
|
details <- liftIO $ fetchUserDetails detailsFile
|
||||||
Fedi.handleWebfinger details
|
Fedi.handleWebfinger details
|
||||||
--------------------------------------------------------------------------------------------
|
, --------------------------------------------------------------------------------------------
|
||||||
, -- Admin page
|
-- Admin page
|
||||||
Twain.get (fromString $ "/" <> (unsafePerformIO $ fetchUserDetails detailsFile).username <> "/admin") do
|
Twain.get (fromString $ "/" <> (unsafePerformIO $ fetchUserDetails detailsFile).username <> "/admin") do
|
||||||
details <- liftIO $ fetchUserDetails detailsFile
|
details <- liftIO $ fetchUserDetails detailsFile
|
||||||
notes <- liftIO db.getNotes
|
notes <- liftIO db.getNotes
|
||||||
|
@ -137,12 +135,15 @@ handleInbox db detailsFile activity = do
|
||||||
handleInboxFollow details db activity follow
|
handleInboxFollow details db activity follow
|
||||||
Fedi.ActivityUndo
|
Fedi.ActivityUndo
|
||||||
( Fedi.Object
|
( Fedi.Object
|
||||||
{ otype = Fedi.TypeActivity
|
{ otype =
|
||||||
{ atype = Fedi.TypeUndo
|
Fedi.TypeActivity
|
||||||
|
{ atype =
|
||||||
|
Fedi.TypeUndo
|
||||||
{ object = Fedi.ActivityFollow follow
|
{ object = Fedi.ActivityFollow follow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) ->
|
}
|
||||||
|
) ->
|
||||||
handleInboxUnfollow details db activity follow
|
handleInboxUnfollow details db activity follow
|
||||||
_ -> do
|
_ -> do
|
||||||
Log.logError $ "Unsupported activity: " <> Fedi.pShow activity
|
Log.logError $ "Unsupported activity: " <> Fedi.pShow activity
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
module Routes.Inbox.Accept where
|
module Routes.Inbox.Accept where
|
||||||
|
|
||||||
import DB
|
|
||||||
import Fedi qualified as Fedi
|
|
||||||
import Control.Concurrent (threadDelay)
|
import Control.Concurrent (threadDelay)
|
||||||
import Control.Concurrent.Async qualified as Async
|
import Control.Concurrent.Async qualified as Async
|
||||||
import Control.Logger.Simple qualified as Log
|
import Control.Logger.Simple qualified as Log
|
||||||
|
import DB
|
||||||
|
import Fedi qualified as Fedi
|
||||||
|
|
||||||
acceptRequest
|
acceptRequest
|
||||||
:: Fedi.UserDetails
|
:: Fedi.UserDetails
|
||||||
|
@ -19,10 +19,12 @@ acceptRequest details actor activity operation = do
|
||||||
let
|
let
|
||||||
callback =
|
callback =
|
||||||
( \(opid :: DB.Int64) -> do
|
( \(opid :: DB.Int64) -> do
|
||||||
result <- Fedi.sendPost
|
result <-
|
||||||
|
Fedi.sendPost
|
||||||
details
|
details
|
||||||
(actor.unwrap <> "/inbox")
|
(actor.unwrap <> "/inbox")
|
||||||
( Fedi.makeAccept Fedi.MkAccept
|
( Fedi.makeAccept
|
||||||
|
Fedi.MkAccept
|
||||||
{ Fedi.acceptId =
|
{ Fedi.acceptId =
|
||||||
Fedi.actorUrl details <> "/accepts/requests/" <> show opid
|
Fedi.actorUrl details <> "/accepts/requests/" <> show opid
|
||||||
, Fedi.acceptingActorUrl = Fedi.Link $ Fedi.actorUrl details
|
, Fedi.acceptingActorUrl = Fedi.Link $ Fedi.actorUrl details
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
module Routes.Inbox.Follow where
|
module Routes.Inbox.Follow where
|
||||||
|
|
||||||
|
import Control.Logger.Simple qualified as Log
|
||||||
import DB
|
import DB
|
||||||
import Fedi qualified as Fedi
|
import Fedi qualified as Fedi
|
||||||
import Web.Twain qualified as Twain
|
|
||||||
import Control.Logger.Simple qualified as Log
|
|
||||||
import Routes.Inbox.Accept
|
import Routes.Inbox.Accept
|
||||||
|
import Web.Twain qualified as Twain
|
||||||
|
|
||||||
handleInboxFollow
|
handleInboxFollow
|
||||||
:: Fedi.UserDetails
|
:: Fedi.UserDetails
|
||||||
|
@ -35,9 +35,7 @@ handleInboxFollow details db activity follow = do
|
||||||
liftIO $ acceptRequest details actor activity operation
|
liftIO $ acceptRequest details actor activity operation
|
||||||
|
|
||||||
pure $ Twain.text ""
|
pure $ Twain.text ""
|
||||||
|
|
||||||
else Twain.next
|
else Twain.next
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Twain.next
|
Twain.next
|
||||||
|
|
||||||
|
@ -64,9 +62,12 @@ handleInboxUnfollow details db activity follow = do
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
operation sendAccept = do
|
operation sendAccept = do
|
||||||
deleteFollower db followerEntry
|
deleteFollower
|
||||||
(\deletedId' -> do
|
db
|
||||||
let deletedId = Fedi.fromMaybe 0 deletedId'
|
followerEntry
|
||||||
|
( \deletedId' -> do
|
||||||
|
let
|
||||||
|
deletedId = Fedi.fromMaybe 0 deletedId'
|
||||||
sendAccept deletedId
|
sendAccept deletedId
|
||||||
<* Log.logInfo ("Deleted follower: " <> Fedi.pShow deletedId)
|
<* Log.logInfo ("Deleted follower: " <> Fedi.pShow deletedId)
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,9 +2,9 @@ module Fedi (module Export) where
|
||||||
|
|
||||||
import Fedi.Crypto as Export
|
import Fedi.Crypto as Export
|
||||||
import Fedi.Helpers as Export
|
import Fedi.Helpers as Export
|
||||||
|
import Fedi.Requests as Export
|
||||||
import Fedi.Routes as Export
|
import Fedi.Routes as Export
|
||||||
import Fedi.Types as Export
|
import Fedi.Types as Export
|
||||||
import Fedi.Types.Helpers as Export
|
import Fedi.Types.Helpers as Export
|
||||||
import Fedi.UserDetails as Export
|
import Fedi.UserDetails as Export
|
||||||
import Fedi.Webfinger as Export
|
import Fedi.Webfinger as Export
|
||||||
import Fedi.Requests as Export
|
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
{-# language RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module Fedi.Crypto where
|
module Fedi.Crypto where
|
||||||
|
|
||||||
import Crypto.Hash qualified as Crypto
|
|
||||||
import Data.ByteArray qualified as BA
|
|
||||||
import Crypto.PubKey.RSA.PKCS15 qualified as Crypto
|
|
||||||
import Crypto.Store.X509 qualified as Crypto
|
|
||||||
import Crypto.Store.PKCS8 qualified as Crypto
|
|
||||||
import Data.X509 qualified as Crypto
|
|
||||||
import Fedi.Helpers
|
|
||||||
import Data.ByteString.Base64 qualified as Base64
|
|
||||||
import Data.Base64.Types qualified as Base64
|
|
||||||
import Data.Text qualified as T
|
|
||||||
import Control.Logger.Simple qualified as Log
|
import Control.Logger.Simple qualified as Log
|
||||||
import Control.Monad.IO.Class
|
import Control.Monad.IO.Class
|
||||||
|
import Crypto.Hash qualified as Crypto
|
||||||
|
import Crypto.PubKey.RSA.PKCS15 qualified as Crypto
|
||||||
|
import Crypto.Store.PKCS8 qualified as Crypto
|
||||||
|
import Crypto.Store.X509 qualified as Crypto
|
||||||
|
import Data.Base64.Types qualified as Base64
|
||||||
|
import Data.ByteArray qualified as BA
|
||||||
|
import Data.ByteString.Base64 qualified as Base64
|
||||||
|
import Data.Text qualified as T
|
||||||
|
import Data.X509 qualified as Crypto
|
||||||
|
import Fedi.Helpers
|
||||||
|
|
||||||
verifyPub :: MonadIO m => MonadThrow m => ByteString -> ByteString -> ByteString -> m Bool
|
verifyPub :: (MonadIO m) => (MonadThrow m) => ByteString -> ByteString -> ByteString -> m Bool
|
||||||
verifyPub pubkeypem sig message = do
|
verifyPub pubkeypem sig message = do
|
||||||
Log.logDebug $ "Verifying signature: " <> pShow
|
Log.logDebug $
|
||||||
|
"Verifying signature: "
|
||||||
|
<> pShow
|
||||||
[ ("pubkeypem", pubkeypem)
|
[ ("pubkeypem", pubkeypem)
|
||||||
, ("sig", sig)
|
, ("sig", sig)
|
||||||
, ("message", message)
|
, ("message", message)
|
||||||
|
@ -43,13 +45,13 @@ sign privatePemFile message = do
|
||||||
& either (throw . show) pure
|
& either (throw . show) pure
|
||||||
|
|
||||||
-- return
|
-- return
|
||||||
pure Signed{..}
|
pure Signed {..}
|
||||||
|
|
||||||
newtype Signed
|
newtype Signed
|
||||||
= Signed
|
= Signed
|
||||||
{ signedMessage :: ByteString
|
{ signedMessage :: ByteString
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving (Show)
|
||||||
|
|
||||||
ppSigned :: Signed -> String
|
ppSigned :: Signed -> String
|
||||||
ppSigned signed =
|
ppSigned signed =
|
||||||
|
|
|
@ -1,40 +1,39 @@
|
||||||
module Fedi.Helpers
|
module Fedi.Helpers (
|
||||||
( module Export
|
module Export,
|
||||||
, module Fedi.Helpers
|
module Fedi.Helpers,
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
import Fedi.UserDetails
|
import Control.Monad as Export
|
||||||
|
import Control.Monad.Catch as Export (Exception, MonadThrow, throwM)
|
||||||
|
import Data.Aeson qualified as A
|
||||||
|
import Data.Aeson.Encode.Pretty qualified as AP
|
||||||
|
import Data.ByteString as Export (ByteString)
|
||||||
import Data.Foldable as Export
|
import Data.Foldable as Export
|
||||||
|
import Data.Function as Export
|
||||||
|
import Data.Functor as Export
|
||||||
import Data.Maybe as Export (fromMaybe, listToMaybe, maybeToList)
|
import Data.Maybe as Export (fromMaybe, listToMaybe, maybeToList)
|
||||||
import Data.String as Export (fromString)
|
import Data.String as Export (fromString)
|
||||||
import Data.Text as Export (Text)
|
import Data.Text as Export (Text)
|
||||||
import Data.ByteString as Export (ByteString)
|
|
||||||
import Data.Time as Export (UTCTime)
|
|
||||||
import Data.Traversable as Export
|
|
||||||
import GHC.Generics as Export (Generic)
|
|
||||||
import Control.Monad as Export
|
|
||||||
import Data.Functor as Export
|
|
||||||
import Data.Function as Export
|
|
||||||
import Control.Monad.Catch as Export (throwM, Exception, MonadThrow)
|
|
||||||
import Text.Pretty.Simple qualified as PS
|
|
||||||
import Data.Text.Lazy qualified as TL
|
import Data.Text.Lazy qualified as TL
|
||||||
import Data.Text.Lazy.Encoding qualified as TL
|
import Data.Text.Lazy.Encoding qualified as TL
|
||||||
import Data.Aeson qualified as A
|
import Data.Time as Export (UTCTime)
|
||||||
import Data.Aeson.Encode.Pretty qualified as AP
|
import Data.Traversable as Export
|
||||||
|
import Fedi.UserDetails
|
||||||
|
import GHC.Generics as Export (Generic)
|
||||||
|
import Text.Pretty.Simple qualified as PS
|
||||||
|
|
||||||
data Error
|
data Error
|
||||||
= Error String
|
= Error String
|
||||||
deriving (Show, Exception)
|
deriving (Show, Exception)
|
||||||
|
|
||||||
throw :: MonadThrow m => String -> m a
|
throw :: (MonadThrow m) => String -> m a
|
||||||
throw = throwM . Error
|
throw = throwM . Error
|
||||||
|
|
||||||
pShow :: Show a => a -> Text
|
pShow :: (Show a) => a -> Text
|
||||||
pShow = TL.toStrict . PS.pShow
|
pShow = TL.toStrict . PS.pShow
|
||||||
|
|
||||||
pJson :: A.ToJSON a => a -> Text
|
pJson :: (A.ToJSON a) => a -> Text
|
||||||
pJson =
|
pJson =
|
||||||
TL.toStrict
|
TL.toStrict
|
||||||
. TL.decodeUtf8
|
. TL.decodeUtf8
|
||||||
. AP.encodePretty' AP.defConfig { AP.confIndent = AP.Spaces 2 }
|
. AP.encodePretty' AP.defConfig {AP.confIndent = AP.Spaces 2}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
{-# language DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
|
||||||
module Fedi.Requests where
|
module Fedi.Requests where
|
||||||
|
|
||||||
import Data.List (intercalate)
|
|
||||||
import Data.Aeson qualified as A
|
|
||||||
import Fedi.Helpers
|
|
||||||
import Fedi.UserDetails
|
|
||||||
import Fedi.Signature.Sign
|
|
||||||
import Network.HTTP.Req qualified as Req
|
|
||||||
import Data.ByteString.Lazy qualified as BSL
|
|
||||||
import Text.URI qualified as URI
|
|
||||||
import Data.Text qualified as T
|
|
||||||
import Control.Logger.Simple qualified as Log
|
import Control.Logger.Simple qualified as Log
|
||||||
|
import Data.Aeson qualified as A
|
||||||
|
import Data.ByteString.Lazy qualified as BSL
|
||||||
|
import Data.List (intercalate)
|
||||||
|
import Data.Text qualified as T
|
||||||
|
import Fedi.Helpers
|
||||||
|
import Fedi.Signature.Sign
|
||||||
|
import Fedi.UserDetails
|
||||||
|
import Network.HTTP.Req qualified as Req
|
||||||
|
import Text.URI qualified as URI
|
||||||
|
|
||||||
sendPost
|
sendPost
|
||||||
:: A.ToJSON input
|
:: (A.ToJSON input)
|
||||||
=> UserDetails
|
=> UserDetails
|
||||||
-> String
|
-> String
|
||||||
-> input
|
-> input
|
||||||
|
@ -24,7 +24,8 @@ sendPost details url payload = do
|
||||||
Log.logDebug $ "Post To: " <> fromString url
|
Log.logDebug $ "Post To: " <> fromString url
|
||||||
Log.logDebug $ "Post Sending: " <> pJson payload
|
Log.logDebug $ "Post Sending: " <> pJson payload
|
||||||
|
|
||||||
let encoded = BSL.toStrict $ A.encode payload
|
let
|
||||||
|
encoded = BSL.toStrict $ A.encode payload
|
||||||
httpSignature <- makeHttpSignature details uri encoded
|
httpSignature <- makeHttpSignature details uri encoded
|
||||||
Log.logDebug $ "Post http signature: " <> pShow httpSignature
|
Log.logDebug $ "Post http signature: " <> pShow httpSignature
|
||||||
Log.logDebug $ "Post http signature headers: " <> pShow (makeSigHeaders httpSignature)
|
Log.logDebug $ "Post http signature headers: " <> pShow (makeSigHeaders httpSignature)
|
||||||
|
@ -39,7 +40,7 @@ sendPost details url payload = do
|
||||||
( scheme
|
( scheme
|
||||||
<> sigHeaders httpSignature
|
<> sigHeaders httpSignature
|
||||||
)
|
)
|
||||||
(\request -> do
|
( \request -> do
|
||||||
Log.logDebug $ "Sending POST request: " <> pShow request
|
Log.logDebug $ "Sending POST request: " <> pShow request
|
||||||
pure request
|
pure request
|
||||||
)
|
)
|
||||||
|
@ -60,7 +61,6 @@ makeHttpSignature details uri encoded = do
|
||||||
Nothing -> "/"
|
Nothing -> "/"
|
||||||
signSignature details host ("post " <> path) encoded
|
signSignature details host ("post " <> path) encoded
|
||||||
|
|
||||||
|
|
||||||
sigHeaders :: HttpSignature -> Req.Option scheme
|
sigHeaders :: HttpSignature -> Req.Option scheme
|
||||||
sigHeaders =
|
sigHeaders =
|
||||||
foldMap (uncurry Req.header) . makeSigHeaders
|
foldMap (uncurry Req.header) . makeSigHeaders
|
||||||
|
@ -90,7 +90,7 @@ sendGet url = do
|
||||||
( scheme
|
( scheme
|
||||||
<> Req.header "ContentType" "application/activity+json"
|
<> Req.header "ContentType" "application/activity+json"
|
||||||
)
|
)
|
||||||
(\request -> do
|
( \request -> do
|
||||||
Log.logDebug $ "Sending GET request: " <> pShow request
|
Log.logDebug $ "Sending GET request: " <> pShow request
|
||||||
pure request
|
pure request
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
module Fedi.Routes
|
module Fedi.Routes (
|
||||||
( module Fedi.Routes
|
module Fedi.Routes,
|
||||||
, module Export
|
module Export,
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
|
import Fedi.Routes.Follow as Export
|
||||||
|
import Fedi.Routes.Helpers as Export
|
||||||
|
import Fedi.Routes.Inbox as Export
|
||||||
|
import Fedi.Routes.Notes as Export
|
||||||
|
import Fedi.Routes.Outbox as Export
|
||||||
|
import Fedi.Routes.User as Export
|
||||||
import Fedi.UserDetails
|
import Fedi.UserDetails
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
|
|
||||||
import Fedi.Routes.Helpers as Export
|
|
||||||
import Fedi.Routes.User as Export
|
|
||||||
import Fedi.Routes.Inbox as Export
|
|
||||||
import Fedi.Routes.Outbox as Export
|
|
||||||
import Fedi.Routes.Notes as Export
|
|
||||||
import Fedi.Routes.Follow as Export
|
|
||||||
|
|
||||||
-- * Routes
|
-- * Routes
|
||||||
|
|
||||||
routes :: UserDetails -> [Twain.Middleware]
|
routes :: UserDetails -> [Twain.Middleware]
|
||||||
|
|
|
@ -2,10 +2,10 @@ module Fedi.Routes.Follow where
|
||||||
|
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Fedi.Helpers
|
import Fedi.Helpers
|
||||||
|
import Fedi.Routes.Helpers
|
||||||
import Fedi.Types
|
import Fedi.Types
|
||||||
import Fedi.Types.Helpers
|
import Fedi.Types.Helpers
|
||||||
import Fedi.UserDetails
|
import Fedi.UserDetails
|
||||||
import Fedi.Routes.Helpers
|
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Web.Twain.Types qualified as Twain
|
import Web.Twain.Types qualified as Twain
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
module Fedi.Routes.Helpers where
|
module Fedi.Routes.Helpers where
|
||||||
|
|
||||||
|
import Control.Monad.Catch (throwM)
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Data.ByteString qualified as BS
|
import Data.ByteString qualified as BS
|
||||||
import Data.ByteString.Lazy qualified as BSL
|
import Data.ByteString.Lazy qualified as BSL
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Control.Monad.Catch (throwM)
|
|
||||||
|
|
||||||
jsonLD :: BSL.ByteString -> Twain.Response
|
jsonLD :: BSL.ByteString -> Twain.Response
|
||||||
jsonLD =
|
jsonLD =
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
module Fedi.Routes.Inbox where
|
module Fedi.Routes.Inbox where
|
||||||
|
|
||||||
import Prelude hiding (error)
|
import Control.Logger.Simple qualified as Log
|
||||||
import Fedi.Types
|
import Control.Monad.Catch (SomeException, catch, displayException)
|
||||||
import Fedi.Helpers
|
import Fedi.Helpers
|
||||||
|
import Fedi.Signature.Check
|
||||||
|
import Fedi.Types
|
||||||
import Fedi.UserDetails
|
import Fedi.UserDetails
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Web.Twain.Types qualified as Twain
|
import Web.Twain.Types qualified as Twain
|
||||||
import Control.Monad.Catch (catch, displayException, SomeException)
|
import Prelude hiding (error)
|
||||||
import Fedi.Signature.Check
|
|
||||||
import Control.Logger.Simple qualified as Log
|
|
||||||
|
|
||||||
-- * Inbox
|
-- * Inbox
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
module Fedi.Routes.Notes where
|
module Fedi.Routes.Notes where
|
||||||
|
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
|
import Fedi.Routes.Helpers
|
||||||
import Fedi.Types
|
import Fedi.Types
|
||||||
import Fedi.UserDetails
|
import Fedi.UserDetails
|
||||||
import Fedi.Routes.Helpers
|
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Web.Twain.Types qualified as Twain
|
import Web.Twain.Types qualified as Twain
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ module Fedi.Routes.Outbox where
|
||||||
|
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Fedi.Helpers
|
import Fedi.Helpers
|
||||||
|
import Fedi.Routes.Helpers
|
||||||
import Fedi.Types
|
import Fedi.Types
|
||||||
import Fedi.Types.Helpers
|
import Fedi.Types.Helpers
|
||||||
import Fedi.UserDetails
|
import Fedi.UserDetails
|
||||||
import Fedi.Routes.Helpers
|
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Web.Twain.Types qualified as Twain
|
import Web.Twain.Types qualified as Twain
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ module Fedi.Routes.User where
|
||||||
|
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Fedi.Helpers
|
import Fedi.Helpers
|
||||||
|
import Fedi.Routes.Helpers
|
||||||
|
import Fedi.Types.Helpers
|
||||||
import Fedi.UserDetails
|
import Fedi.UserDetails
|
||||||
import Fedi.Webfinger
|
import Fedi.Webfinger
|
||||||
import Fedi.Types.Helpers
|
|
||||||
import Fedi.Routes.Helpers
|
|
||||||
import Web.Twain qualified as Twain
|
import Web.Twain qualified as Twain
|
||||||
import Web.Twain.Types qualified as Twain
|
import Web.Twain.Types qualified as Twain
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
{-# language RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# language ViewPatterns #-}
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
|
|
||||||
module Fedi.Signature.Check
|
module Fedi.Signature.Check (
|
||||||
( module Fedi.Signature.Types
|
module Fedi.Signature.Types,
|
||||||
, module Fedi.Signature.Check
|
module Fedi.Signature.Check,
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
import Prelude hiding (error)
|
import Control.Monad.IO.Class
|
||||||
import Fedi.Types
|
|
||||||
import Fedi.UserDetails
|
|
||||||
import Fedi.Requests
|
|
||||||
import Fedi.Routes.Helpers
|
|
||||||
import Fedi.Helpers
|
|
||||||
import Web.Twain qualified as Twain
|
|
||||||
import Data.Text qualified as T
|
|
||||||
import Network.Wai qualified as Wai
|
|
||||||
import Network.HTTP.Types.URI qualified as HTTP
|
|
||||||
import Text.ParserCombinators.ReadP qualified as P
|
|
||||||
import Data.Text.Encoding qualified as T
|
|
||||||
import Data.ByteString qualified as BS
|
import Data.ByteString qualified as BS
|
||||||
import Data.ByteString.Lazy qualified as BSL
|
import Data.ByteString.Lazy qualified as BSL
|
||||||
import Fedi.Crypto
|
|
||||||
import Fedi.Signature.Types
|
|
||||||
import Data.CaseInsensitive qualified as CI
|
import Data.CaseInsensitive qualified as CI
|
||||||
import Control.Monad.IO.Class
|
import Data.Text qualified as T
|
||||||
|
import Data.Text.Encoding qualified as T
|
||||||
|
import Fedi.Crypto
|
||||||
|
import Fedi.Helpers
|
||||||
|
import Fedi.Requests
|
||||||
|
import Fedi.Routes.Helpers
|
||||||
|
import Fedi.Signature.Types
|
||||||
|
import Fedi.Types
|
||||||
|
import Fedi.UserDetails
|
||||||
|
import Network.HTTP.Types.URI qualified as HTTP
|
||||||
|
import Network.Wai qualified as Wai
|
||||||
|
import Text.ParserCombinators.ReadP qualified as P
|
||||||
|
import Web.Twain qualified as Twain
|
||||||
|
import Prelude hiding (error)
|
||||||
|
|
||||||
-- * Check
|
-- * Check
|
||||||
|
|
||||||
|
@ -36,11 +35,14 @@ checkSignatureAndParseBody = do
|
||||||
-- liftIO $ print ("headers", Twain.requestHeaders request)
|
-- liftIO $ print ("headers", Twain.requestHeaders request)
|
||||||
body <- liftIO (Wai.strictRequestBody request)
|
body <- liftIO (Wai.strictRequestBody request)
|
||||||
sigheader <- parseSignature =<< Twain.header "Signature"
|
sigheader <- parseSignature =<< Twain.header "Signature"
|
||||||
digest <- Twain.header "Digest" >>=
|
digest <-
|
||||||
maybe (throw "missing header Digest") (pure . T.encodeUtf8)
|
Twain.header "Digest"
|
||||||
|
>>= maybe (throw "missing header Digest") (pure . T.encodeUtf8)
|
||||||
(person :: Person) <- liftIO $ sendGet sigheader.keyId
|
(person :: Person) <- liftIO $ sendGet sigheader.keyId
|
||||||
let personPkid = person.otype.publicKey.pkid
|
let
|
||||||
let personPublicKey = pemToBS person.otype.publicKey.publicKeyPem
|
personPkid = person.otype.publicKey.pkid
|
||||||
|
let
|
||||||
|
personPublicKey = pemToBS person.otype.publicKey.publicKeyPem
|
||||||
|
|
||||||
signatureString <-
|
signatureString <-
|
||||||
makeSignatureString request sigheader.headers
|
makeSignatureString request sigheader.headers
|
||||||
|
@ -53,12 +55,14 @@ checkSignatureAndParseBody = do
|
||||||
parseJson body
|
parseJson body
|
||||||
|
|
||||||
makeSignatureString
|
makeSignatureString
|
||||||
:: forall m. MonadThrow m => Wai.Request -> [T.Text] -> m ByteString
|
:: forall m. (MonadThrow m) => Wai.Request -> [T.Text] -> m ByteString
|
||||||
makeSignatureString request (map (T.encodeUtf8 . T.toLower) -> headers) = do
|
makeSignatureString request (map (T.encodeUtf8 . T.toLower) -> headers) = do
|
||||||
let
|
let
|
||||||
requestHeaders = Wai.requestHeaders request
|
requestHeaders = Wai.requestHeaders request
|
||||||
method = T.encodeUtf8 $ T.toLower $ T.decodeUtf8 $ Wai.requestMethod request
|
method = T.encodeUtf8 $ T.toLower $ T.decodeUtf8 $ Wai.requestMethod request
|
||||||
path = "/" <> T.encodeUtf8 (T.intercalate "/" $ Wai.pathInfo request)
|
path =
|
||||||
|
"/"
|
||||||
|
<> T.encodeUtf8 (T.intercalate "/" $ Wai.pathInfo request)
|
||||||
<> HTTP.renderQuery True (Wai.queryString request)
|
<> HTTP.renderQuery True (Wai.queryString request)
|
||||||
requestTarget = method <> " " <> path
|
requestTarget = method <> " " <> path
|
||||||
let
|
let
|
||||||
|
@ -67,15 +71,20 @@ makeSignatureString request (map (T.encodeUtf8 . T.toLower) -> headers) = do
|
||||||
| header == "(request-target)" =
|
| header == "(request-target)" =
|
||||||
pure $ header <> ": " <> requestTarget
|
pure $ header <> ": " <> requestTarget
|
||||||
| header == "host" = do
|
| header == "host" = do
|
||||||
let result = lookup (CI.mk header) requestHeaders
|
let
|
||||||
|
result = lookup (CI.mk header) requestHeaders
|
||||||
case result of
|
case result of
|
||||||
Nothing -> throw $ "Missing header '" <> show header <> "'."
|
Nothing -> throw $ "Missing header '" <> show header <> "'."
|
||||||
Just value -> pure $ header <> ": "
|
Just value ->
|
||||||
|
pure $
|
||||||
|
header
|
||||||
|
<> ": "
|
||||||
<> if ":443" `BS.isSuffixOf` value
|
<> if ":443" `BS.isSuffixOf` value
|
||||||
then BS.dropEnd (BS.length ":443") value
|
then BS.dropEnd (BS.length ":443") value
|
||||||
else value
|
else value
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
let result = lookup (CI.mk header) requestHeaders
|
let
|
||||||
|
result = lookup (CI.mk header) requestHeaders
|
||||||
case result of
|
case result of
|
||||||
Nothing -> throw $ "Missing header '" <> show header <> "'."
|
Nothing -> throw $ "Missing header '" <> show header <> "'."
|
||||||
Just value -> pure $ header <> ": " <> value
|
Just value -> pure $ header <> ": " <> value
|
||||||
|
@ -83,7 +92,8 @@ makeSignatureString request (map (T.encodeUtf8 . T.toLower) -> headers) = do
|
||||||
BS.intercalate "\n" <$> traverse mylookup headers
|
BS.intercalate "\n" <$> traverse mylookup headers
|
||||||
|
|
||||||
checkSignature
|
checkSignature
|
||||||
:: MonadIO m => MonadThrow m
|
:: (MonadIO m)
|
||||||
|
=> (MonadThrow m)
|
||||||
=> Url -> ByteString -> SignatureHeader -> ByteString -> ByteString -> ByteString -> m ()
|
=> Url -> ByteString -> SignatureHeader -> ByteString -> ByteString -> ByteString -> m ()
|
||||||
checkSignature personPkid personPublicKey sigheader signatureString digest body = do
|
checkSignature personPkid personPublicKey sigheader signatureString digest body = do
|
||||||
-- check
|
-- check
|
||||||
|
@ -99,9 +109,9 @@ checkSignature personPkid personPublicKey sigheader signatureString digest body
|
||||||
unless (mydigest == digest) $
|
unless (mydigest == digest) $
|
||||||
throw "digest verification failed."
|
throw "digest verification failed."
|
||||||
|
|
||||||
-- todo: check date
|
-- todo: check date
|
||||||
|
|
||||||
parseSignature :: MonadThrow m => Maybe T.Text -> m SignatureHeader
|
parseSignature :: (MonadThrow m) => Maybe T.Text -> m SignatureHeader
|
||||||
parseSignature minput = do
|
parseSignature minput = do
|
||||||
input <- maybe (throw "no signature.") (pure . T.unpack) minput
|
input <- maybe (throw "no signature.") (pure . T.unpack) minput
|
||||||
case P.readP_to_S parser input of
|
case P.readP_to_S parser input of
|
||||||
|
@ -113,13 +123,14 @@ parseSignature minput = do
|
||||||
parser = do
|
parser = do
|
||||||
components <- component `P.sepBy` P.char ','
|
components <- component `P.sepBy` P.char ','
|
||||||
keyId <- lookup' KeyId components
|
keyId <- lookup' KeyId components
|
||||||
headers <- T.split (==' ') . T.pack <$> lookup' Headers components
|
headers <- T.split (== ' ') . T.pack <$> lookup' Headers components
|
||||||
signature <-
|
signature <-
|
||||||
( fromString
|
(fromString)
|
||||||
) <$> lookup' Signature components
|
<$> lookup' Signature components
|
||||||
P.eof
|
P.eof
|
||||||
pure SignatureHeader{..}
|
pure SignatureHeader {..}
|
||||||
component = P.choice
|
component =
|
||||||
|
P.choice
|
||||||
[ do
|
[ do
|
||||||
_ <- P.string "keyId="
|
_ <- P.string "keyId="
|
||||||
url <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
|
url <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
{-# language RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module Fedi.Signature.Sign
|
module Fedi.Signature.Sign (
|
||||||
( module Fedi.Signature.Types
|
module Fedi.Signature.Types,
|
||||||
, module Fedi.Signature.Sign
|
module Fedi.Signature.Sign,
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
import Prelude hiding (error)
|
|
||||||
import Fedi.UserDetails
|
|
||||||
import Fedi.Helpers
|
|
||||||
import Data.ByteString qualified as BS
|
import Data.ByteString qualified as BS
|
||||||
import Fedi.Crypto
|
|
||||||
import Data.Time qualified as Time
|
import Data.Time qualified as Time
|
||||||
|
import Fedi.Crypto
|
||||||
|
import Fedi.Helpers
|
||||||
import Fedi.Signature.Types
|
import Fedi.Signature.Types
|
||||||
|
import Fedi.UserDetails
|
||||||
|
import Prelude hiding (error)
|
||||||
|
|
||||||
-- * Sign
|
-- * Sign
|
||||||
|
|
||||||
signSignature
|
signSignature
|
||||||
:: UserDetails -> String -> String -> ByteString -> IO HttpSignature
|
:: UserDetails -> String -> String -> ByteString -> IO HttpSignature
|
||||||
signSignature details host requestTarget body = do
|
signSignature details host requestTarget body = do
|
||||||
date <- Time.getCurrentTime
|
date <-
|
||||||
|
Time.getCurrentTime
|
||||||
<&> Time.formatTime Time.defaultTimeLocale "%a, %d %b %Y %H:%M:%S GMT"
|
<&> Time.formatTime Time.defaultTimeLocale "%a, %d %b %Y %H:%M:%S GMT"
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -35,14 +35,15 @@ signSignature details host requestTarget body = do
|
||||||
|
|
||||||
let
|
let
|
||||||
signature = encodeBase64 signed.signedMessage
|
signature = encodeBase64 signed.signedMessage
|
||||||
signatureHeader = SignatureHeader{..}
|
signatureHeader = SignatureHeader {..}
|
||||||
|
|
||||||
pure HttpSignature{..}
|
pure HttpSignature {..}
|
||||||
|
|
||||||
makeSignatureString
|
makeSignatureString
|
||||||
:: String -> String -> String -> ByteString -> ByteString
|
:: String -> String -> String -> ByteString -> ByteString
|
||||||
makeSignatureString host requestTarget date digest =
|
makeSignatureString host requestTarget date digest =
|
||||||
BS.intercalate "\n"
|
BS.intercalate
|
||||||
|
"\n"
|
||||||
[ "(request-target): " <> fromString requestTarget
|
[ "(request-target): " <> fromString requestTarget
|
||||||
, "host: " <> fromString host
|
, "host: " <> fromString host
|
||||||
, "date: " <> fromString date
|
, "date: " <> fromString date
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{-# language RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module Fedi.Signature.Types where
|
module Fedi.Signature.Types where
|
||||||
|
|
||||||
import Prelude hiding (error)
|
import Data.ByteString qualified as BS
|
||||||
import Fedi.UserDetails
|
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Data.Text.Encoding qualified as T
|
import Data.Text.Encoding qualified as T
|
||||||
import Data.ByteString qualified as BS
|
import Fedi.UserDetails
|
||||||
|
import Prelude hiding (error)
|
||||||
|
|
||||||
data HttpSignature
|
data HttpSignature
|
||||||
= HttpSignature
|
= HttpSignature
|
||||||
|
@ -15,11 +15,12 @@ data HttpSignature
|
||||||
, host :: String
|
, host :: String
|
||||||
, digest :: ByteString
|
, digest :: ByteString
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving (Show)
|
||||||
|
|
||||||
toSignature :: SignatureHeader -> ByteString
|
toSignature :: SignatureHeader -> ByteString
|
||||||
toSignature sig =
|
toSignature sig =
|
||||||
BS.intercalate ","
|
BS.intercalate
|
||||||
|
","
|
||||||
[ "keyId=\"" <> fromString sig.keyId <> "\""
|
[ "keyId=\"" <> fromString sig.keyId <> "\""
|
||||||
, "headers=\"" <> BS.intercalate " " (map T.encodeUtf8 sig.headers) <> "\""
|
, "headers=\"" <> BS.intercalate " " (map T.encodeUtf8 sig.headers) <> "\""
|
||||||
, "signature=\"" <> sig.signature <> "\""
|
, "signature=\"" <> sig.signature <> "\""
|
||||||
|
@ -28,15 +29,15 @@ toSignature sig =
|
||||||
|
|
||||||
data SignatureHeader
|
data SignatureHeader
|
||||||
= SignatureHeader
|
= SignatureHeader
|
||||||
{ -- | Where to get the public key for this actor
|
{ keyId :: Url
|
||||||
keyId :: Url
|
-- ^ Where to get the public key for this actor
|
||||||
, -- | Which headers have been sent
|
, headers :: [T.Text]
|
||||||
headers :: [T.Text]
|
-- ^ Which headers have been sent
|
||||||
, -- | Contains the signature
|
, signature :: ByteString
|
||||||
signature :: ByteString
|
-- ^ Contains the signature
|
||||||
, components :: [(Component, String)]
|
, components :: [(Component, String)]
|
||||||
}
|
}
|
||||||
deriving Show
|
deriving (Show)
|
||||||
|
|
||||||
data Component
|
data Component
|
||||||
= KeyId
|
= KeyId
|
||||||
|
|
|
@ -48,8 +48,7 @@ instance (ToObject a) => A.ToJSON (Object a) where
|
||||||
instance (ToObject a) => ToObject (Object a) where
|
instance (ToObject a) => ToObject (Object a) where
|
||||||
toObject object =
|
toObject object =
|
||||||
[ "@context"
|
[ "@context"
|
||||||
A..=
|
A..= [ ("https://www.w3.org/ns/activitystreams" :: String)
|
||||||
[ ("https://www.w3.org/ns/activitystreams" :: String)
|
|
||||||
, ("https://w3id.org/security/v1" :: String)
|
, ("https://w3id.org/security/v1" :: String)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -348,7 +347,6 @@ instance A.FromJSON TypeUndo where
|
||||||
object <- value A..: "object"
|
object <- value A..: "object"
|
||||||
pure TypeUndo {..}
|
pure TypeUndo {..}
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
type Like = Object (TypeActivity TypeLike)
|
type Like = Object (TypeActivity TypeLike)
|
||||||
|
|
||||||
|
@ -539,6 +537,7 @@ type Outbox = OrderedCollection AnyActivity
|
||||||
type OutboxPage = OrderedCollectionPage AnyActivity
|
type OutboxPage = OrderedCollectionPage AnyActivity
|
||||||
|
|
||||||
type Followers = OrderedCollection Url
|
type Followers = OrderedCollection Url
|
||||||
|
|
||||||
type FollowersPage = OrderedCollectionPage Url
|
type FollowersPage = OrderedCollectionPage Url
|
||||||
|
|
||||||
data CollectionType t
|
data CollectionType t
|
||||||
|
|
|
@ -166,7 +166,8 @@ makeAccept accept =
|
||||||
, otype =
|
, otype =
|
||||||
TypeActivity
|
TypeActivity
|
||||||
{ actor = accept.acceptingActorUrl
|
{ actor = accept.acceptingActorUrl
|
||||||
, atype = TypeAccept
|
, atype =
|
||||||
|
TypeAccept
|
||||||
{ object = accept.acceptedActivity
|
{ object = accept.acceptedActivity
|
||||||
}
|
}
|
||||||
, target = Nothing
|
, target = Nothing
|
||||||
|
|
|
@ -4,11 +4,11 @@ module Fedi.UserDetails (
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
|
import Data.ByteString as Export (ByteString)
|
||||||
import Data.Foldable as Export
|
import Data.Foldable as Export
|
||||||
import Data.Maybe as Export (fromMaybe, listToMaybe, maybeToList)
|
import Data.Maybe as Export (fromMaybe, listToMaybe, maybeToList)
|
||||||
import Data.String as Export (fromString)
|
import Data.String as Export (fromString)
|
||||||
import Data.Text as Export (Text)
|
import Data.Text as Export (Text)
|
||||||
import Data.ByteString as Export (ByteString)
|
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Data.Text.Encoding qualified as T
|
import Data.Text.Encoding qualified as T
|
||||||
import Data.Time as Export (UTCTime)
|
import Data.Time as Export (UTCTime)
|
||||||
|
|
Loading…
Add table
Reference in a new issue