split FediChord messages into multiple parts for size restrictions

includes encoding of messages as ASN.1 DER
This commit is contained in:
Trolli Schmittlauch 2020-05-06 02:13:01 +02:00
parent 1d8d9a33fd
commit 31b73b7667
4 changed files with 177 additions and 100 deletions

View file

@ -46,7 +46,7 @@ category: Network
extra-source-files: CHANGELOG.md extra-source-files: CHANGELOG.md
common deps common deps
build-depends: base ^>=4.12.0.0, containers ^>=0.6.0.1, bytestring, utf8-string ^>=1.0.1.1, network ^>=2.8.0.1, time ^>=1.8.0.2, cmdargs ^>= 0.10, cryptonite ^>= 0.25, memory, async, asn1-encoding, asn1-types, asn1-parse, publicsuffix, network-byte-order build-depends: base ^>=4.12.0.0, containers ^>=0.6.0.1, bytestring, utf8-string ^>=1.0.1.1, network ^>=2.8.0.1, time ^>=1.8.0.2, cmdargs ^>= 0.10, cryptonite ^>= 0.25, memory, async, asn1-encoding, asn1-types, asn1-parse, publicsuffix, network-byte-order, safe
ghc-options: -Wall ghc-options: -Wall

View file

@ -12,8 +12,10 @@ import Data.Time.Clock.POSIX
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Safe
import Hash2Pub.FediChord import Hash2Pub.FediChord
import Hash2Pub.Utils
import Hash2Pub.DHTProtocol (QueryResponse (..)) import Hash2Pub.DHTProtocol (QueryResponse (..))
data Action = data Action =
@ -74,12 +76,147 @@ data ActionPayload =
} }
deriving (Show, Eq) deriving (Show, Eq)
-- ToDo: pagination so packets do not exceed maximum size -- | Try splitting a payload into multiple parts to be able to reduce size of
-- probably should be taken care of by the callers of this, as the ASN.1 -- individual messages.
-- encoding functions are layer-4 agnostic -- Only some kinds of payloads can be split, and only to a limited number of parts.
-- This function only deals with potentially large payload types and passes the
-- rest as-is.
splitPayload :: Int -> ActionPayload -> [ActionPayload]
splitPayload numParts pl@LeaveRequestPayload{} = [
LeaveRequestPayload {
leaveSuccessors = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ leaveSuccessors pl
, leavePredecessors = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ leavePredecessors pl
} | thisPart <- [0..numParts] ]
splitPayload numParts pl@StabiliseResponsePayload{} = [
StabiliseResponsePayload {
stabiliseSuccessors = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ stabiliseSuccessors pl
, stabilisePredecessors = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ stabilisePredecessors pl
} | thisPart <- [0..numParts] ]
splitPayload numParts pl@PingResponsePayload{} = [
PingResponsePayload {
pingNodeStates = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ pingNodeStates pl
} | thisPart <- [0..numParts] ]
splitPayload numParts pl@JoinResponsePayload{} = [
JoinResponsePayload {
joinSuccessors = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ joinSuccessors pl
, joinPredecessors = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ joinPredecessors pl
, joinCache = fromMaybe [] $ headMay . drop thisPart $ listInto numParts $ joinCache pl
} | thisPart <- [0..numParts] ]
splitPayload _ pl@(QueryIDResponsePayload FOUND{}) = [pl]
splitPayload numParts pl@(QueryIDResponsePayload (FORWARD resSet)) = [
QueryIDResponsePayload {
queryResult = maybe (FORWARD Set.empty) FORWARD $ headMay . drop thisPart $ setInto numParts resSet
} | thisPart <- [0..numParts] ]
-- pass all other payloads as-is
splitPayload _ somePayload = [somePayload]
listInto :: Int -> [a] -> [[a]]
listInto numParts xs = chunksOf (chunkLength numParts $ length xs) xs
setInto :: Int -> Set.Set a -> [Set.Set a]
setInto numParts aSet = chunkSet (chunkLength numParts $ Set.size aSet) aSet
chunkLength :: Int -> Int -> Int
chunkLength numParts totalSize = ceiling $ (realToFrac totalSize :: Double) / realToFrac numParts
-- | Serialise a 'FediChordMessage' to one or more parts represented as a 'BS.ByteString' in ASN.1 DER,
-- such that their length does not exceed a maximum number of bytes if possible.
-- This is important for making sure the message fits into a certain packet size.
-- The number of parts per message is limited to 150 for DOS protection reasons.
-- The returned byte strings might exceed the desired maximum length, as only the payload (and not all of them)
-- can be split into multiple parts.
serialiseMessage :: FediChordMessage -> Int -> [BS.ByteString]
serialiseMessage msg maxBytesLength =
splitPayloadUntilSmallEnough 1
where
splitPayloadUntilSmallEnough numParts
| maxMsgLength (encodedMsgs numParts) <= maxBytesLength = encodedMsgs numParts
-- ToDo: log this
-- limit to maximum number of parts to reduce DOS-potential of repeated
-- splitting
| numParts == maximumParts = encodedMsgs numParts
| otherwise = splitPayloadUntilSmallEnough $ numParts + 1
messageParts :: Int -> [FediChordMessage]
messageParts i = foldr (modifyMessage i) [] $ payloadParts i
-- insert payload parts into message and adjust parts metadata
modifyMessage :: Int -> (Integer, ActionPayload) -> [FediChordMessage] -> [FediChordMessage]
modifyMessage i (partNum, pl) pls = (msg {
part = partNum
, payload = pl
, parts = fromIntegral i
}):pls
-- part starts at 0
payloadParts :: Int -> [(Integer, ActionPayload)]
payloadParts i = zip [0..] (splitPayload i actionPayload)
actionPayload = payload msg
encodedMsgs i = map (encodeASN1' DER . encodeMessage) $ messageParts i
maxMsgLength msgs = maximum $ map BS.length msgs
maximumParts = 150
-- ===== encoding functions ===== -- ===== encoding functions =====
encodePayload :: ActionPayload -> [ASN1]
encodePayload LeaveResponsePayload = [Null]
encodePayload payload@LeaveRequestPayload{} =
Start Sequence
: Start Sequence
: (map (IntVal . getNodeID) $ leaveSuccessors payload)
++ [End Sequence
, Start Sequence]
++ (map (IntVal . getNodeID) $ leavePredecessors payload)
++ [End Sequence
, End Sequence]
-- currently StabiliseResponsePayload and LeaveRequestPayload are equal
encodePayload payload@StabiliseResponsePayload{} =
Start Sequence
: Start Sequence
: (map (IntVal . getNodeID) $ stabiliseSuccessors payload)
++ [End Sequence
, Start Sequence]
++ (map (IntVal . getNodeID) $ stabilisePredecessors payload)
++ [End Sequence
, End Sequence]
encodePayload payload@StabiliseRequestPayload = [Null]
encodePayload payload@QueryIDResponsePayload{} =
let
resp = queryResult payload
in
Start Sequence
: encodeQueryResult resp
: case resp of
FOUND _ -> []
FORWARD entrySet ->
Start Sequence
: (concatMap encodeCacheEntry . Set.elems $ entrySet)
++ [End Sequence]
++ [End Sequence]
encodePayload payload@QueryIDResponsePayload{} = [
Start Sequence
, IntVal . getNodeID $ queryTargetID payload
, IntVal $ queryLBestNodes payload
, End Sequence
]
-- | encodes the @JoinResponsePayload@ ASN.1 type
encodePayload payload@JoinResponsePayload{} =
Start Sequence
: Start Sequence
: (map (IntVal . getNodeID) $ joinSuccessors payload)
++ [End Sequence
, Start Sequence]
++ (map (IntVal . getNodeID) $ joinPredecessors payload)
++ [End Sequence
, Start Sequence]
++ (concatMap encodeCacheEntry $ joinCache payload)
++ [End Sequence
, End Sequence]
encodePayload payload@JoinRequestPayload{} = [Null]
encodePayload PingRequestPayload{} = [Null]
encodePayload payload@PingResponsePayload{} =
Start Sequence
: (concatMap encodeNodeState $ pingNodeStates payload)
++ [End Sequence]
encodeNodeState :: NodeState -> [ASN1] encodeNodeState :: NodeState -> [ASN1]
encodeNodeState ns = [ encodeNodeState ns = [
Start Sequence Start Sequence
@ -102,100 +239,15 @@ encodeCacheEntry (NodeEntry _ ns timestamp) =
, End Sequence] , End Sequence]
encodeCacheEntry _ = [] encodeCacheEntry _ = []
encodeLeaveResponsePayload :: ActionPayload -> [ASN1]
encodeLeaveResponsePayload LeaveResponsePayload = [Null]
encodeLeaveRequestPayload :: ActionPayload -> [ASN1]
encodeLeaveRequestPayload payload@LeaveRequestPayload{} =
Start Sequence
: Start Sequence
: (map (IntVal . getNodeID) $ leaveSuccessors payload)
++ [End Sequence
, Start Sequence]
++ (map (IntVal . getNodeID) $ leavePredecessors payload)
++ [End Sequence
, End Sequence]
-- currently StabiliseResponsePayload and LeaveRequestPayload are equal
encodeStabiliseResponsePayload :: ActionPayload -> [ASN1]
encodeStabiliseResponsePayload payload@StabiliseResponsePayload{} =
Start Sequence
: Start Sequence
: (map (IntVal . getNodeID) $ stabiliseSuccessors payload)
++ [End Sequence
, Start Sequence]
++ (map (IntVal . getNodeID) $ stabilisePredecessors payload)
++ [End Sequence
, End Sequence]
encodeStabiliseRequestPayload :: ActionPayload -> [ASN1]
encodeStabiliseRequestPayload payload@StabiliseRequestPayload = [Null]
encodeQueryResult :: QueryResponse -> ASN1 encodeQueryResult :: QueryResponse -> ASN1
encodeQueryResult FOUND{} = Enumerated 0 encodeQueryResult FOUND{} = Enumerated 0
encodeQueryResult FORWARD{} = Enumerated 1 encodeQueryResult FORWARD{} = Enumerated 1
encodeQueryIDResponsePayload :: ActionPayload -> [ASN1]
encodeQueryIDResponsePayload payload@QueryIDResponsePayload{} =
let
resp = queryResult payload
in
Start Sequence
: encodeQueryResult resp
: case resp of
FOUND _ -> []
FORWARD entrySet ->
Start Sequence
: (concatMap encodeCacheEntry . Set.elems $ entrySet)
++ [End Sequence]
++ [End Sequence]
encodeQueryIDRequestPayload :: ActionPayload -> [ASN1]
encodeQueryIDRequestPayload payload@QueryIDResponsePayload{} = [
Start Sequence
, IntVal . getNodeID $ queryTargetID payload
, IntVal $ queryLBestNodes payload
, End Sequence
]
-- | encodes the @JoinResponsePayload@ ASN.1 type
encodeJoinResponsePayload :: ActionPayload -> [ASN1]
encodeJoinResponsePayload payload@JoinResponsePayload{} =
Start Sequence
: Start Sequence
: (map (IntVal . getNodeID) $ joinSuccessors payload)
++ [End Sequence
, Start Sequence]
++ (map (IntVal . getNodeID) $ joinPredecessors payload)
++ [End Sequence
, Start Sequence]
++ (concatMap encodeCacheEntry $ joinCache payload)
++ [End Sequence
, End Sequence]
encodeJoinRequestPayload :: ActionPayload -> [ASN1]
encodeJoinRequestPayload payload@JoinRequestPayload{} = [Null]
encodePingRequestPayload :: ActionPayload -> [ASN1]
encodePingRequestPayload PingRequestPayload{} = [Null]
encodePingResponsePayload :: ActionPayload -> [ASN1]
encodePingResponsePayload payload@PingResponsePayload{} =
Start Sequence
: (concatMap encodeNodeState $ pingNodeStates payload)
++ [End Sequence]
-- | Encode a 'FediChordMessage' as ASN.1. -- | Encode a 'FediChordMessage' as ASN.1.
-- The 'ActionPayload' needs to be passed separately, already encoded as ASN.1, encodeMessage :: FediChordMessage -- ^ the 'FediChordMessage to be encoded
-- to allow controlling message size by splitting the payload into multiple parts.
encodeMessage :: FediChordMessage -- ^ the 'FediChordMessage to be encoded, the
-- contained 'payload' is ignored
-> [ASN1] -- ^ the ASN.1 encoding of
-- the message's 'payload'
-> [ASN1] -> [ASN1]
encodeMessage encodeMessage
(Request requestID sender parts part action _) (Request requestID sender parts part action requestPayload) =
payload =
Start Sequence Start Sequence
: (Enumerated . fromIntegral . fromEnum $ action) : (Enumerated . fromIntegral . fromEnum $ action)
: IntVal requestID : IntVal requestID
@ -203,17 +255,16 @@ encodeMessage
++ [ ++ [
IntVal parts IntVal parts
, IntVal part ] , IntVal part ]
++ payload ++ encodePayload requestPayload
encodeMessage encodeMessage
(Response responseTo senderID parts part action _) (Response responseTo senderID parts part action responsePayload) = [
payload = [
Start Sequence Start Sequence
, IntVal responseTo , IntVal responseTo
, IntVal . getNodeID $ senderID , IntVal . getNodeID $ senderID
, IntVal parts , IntVal parts
, IntVal part , IntVal part
, Enumerated . fromIntegral . fromEnum $ action] , Enumerated . fromIntegral . fromEnum $ action]
++ payload ++ encodePayload responsePayload
-- ===== parser combinators ===== -- ===== parser combinators =====

View file

@ -326,11 +326,7 @@ ipAddrAsBS (a, b, c, d) = mconcat $ map NetworkBytes.bytestring32 [a, b, c, d]
bsAsIpAddr :: BS.ByteString -> HostAddress6 bsAsIpAddr :: BS.ByteString -> HostAddress6
bsAsIpAddr bytes = (a,b,c,d) bsAsIpAddr bytes = (a,b,c,d)
where where
a:b:c:d:_ = map NetworkBytes.word32 . fourBytes $ bytes a:b:c:d:_ = map NetworkBytes.word32 . chunkBytes 4 $ bytes
fourBytes bs =
case BS.splitAt 4 bs of
(a, "") -> [a]
(a, b) -> a: fourBytes b
-- | generates a 256 bit long NodeID using SHAKE128, represented as ByteString -- | generates a 256 bit long NodeID using SHAKE128, represented as ByteString

View file

@ -1,6 +1,36 @@
{-# LANGUAGE OverloadedStrings #-}
module Hash2Pub.Utils where module Hash2Pub.Utils where
import qualified Data.ByteString as BS
import qualified Data.Set as Set
-- |wraps a list into a Maybe, by replacing empty lists with Nothing -- |wraps a list into a Maybe, by replacing empty lists with Nothing
maybeEmpty :: [a] -> Maybe [a] maybeEmpty :: [a] -> Maybe [a]
maybeEmpty [] = Nothing maybeEmpty [] = Nothing
maybeEmpty nonemptyList = Just nonemptyList maybeEmpty nonemptyList = Just nonemptyList
-- | Chop a list into sublists of i elements. The last sublist might contain
-- less than i elements.
chunksOf :: Int -> [a] -> [[a]]
chunksOf i xs =
case splitAt i xs of
(a, []) -> [a]
(a, b) -> a : chunksOf i b
-- | Chop a 'BS.ByteString' into list of substrings of i elements. The last
-- substring might contain less than i elements.
chunkBytes :: Int -> BS.ByteString -> [BS.ByteString]
chunkBytes i xs =
case BS.splitAt i xs of
(a, "") -> [a]
(a, b) -> a : chunkBytes i b
-- | Chop a 'Set.Set' into a list of disjuct subsets of i elements. The last
-- subset might contain less than i elements.
chunkSet :: Int -> Set.Set a -> [Set.Set a]
chunkSet i xs
| Set.null . snd $ splitSet = [fst splitSet]
| otherwise = fst splitSet : chunkSet i (snd splitSet)
where
splitSet = Set.splitAt i xs