gbnet-hs-0.1.0.0: src/GBNet/Peer/Protocol.hs
-- |
-- Module : GBNet.Peer.Protocol
-- Description : Pure encoding/decoding utilities for Peer protocol
--
-- Salt encoding, deny reasons, payload header decoding, channel sequence
-- decoding, and FNV-1a hashing. No dependencies on other Peer sub-modules.
module GBNet.Peer.Protocol
( -- * Salt encoding
encodeSalt,
decodeSalt,
-- * Deny reasons
DenyReason (..),
encodeDenyReason,
decodeDenyReason,
denyToDisconnectReason,
-- * Payload header
decodePayloadHeader,
payloadFragmentFlag,
payloadChannelMask,
minPayloadSize,
-- * Channel sequence
decodeChannelSeq,
-- * FNV-1a hashing
sockAddrToKey,
fnvMix,
fnvOffsetBasis,
fnvPrime,
)
where
import Data.Bits (shiftL, xor, (.&.), (.|.))
import qualified Data.ByteString as BS
import Data.List (foldl')
import Data.Word (Word64, Word8)
import GBNet.Connection (DisconnectReason (..))
import GBNet.Serialize (deserialize, serialize)
import GBNet.Types (ChannelId (..), SequenceNum (..))
import Network.Socket (PortNumber, SockAddr (..))
-- | Encode a Word64 salt to bytes.
encodeSalt :: Word64 -> BS.ByteString
encodeSalt = serialize
{-# INLINE encodeSalt #-}
-- | Decode a Word64 salt from bytes.
decodeSalt :: BS.ByteString -> Maybe Word64
decodeSalt bs = case deserialize bs of
Left _ -> Nothing
Right v -> Just v
{-# INLINE decodeSalt #-}
-- | Deny reason codes sent during handshake rejection.
data DenyReason
= DenyServerFull
| DenyInvalidChallenge
| DenyUnknown !Word8
deriving (Eq, Show)
-- | Encode a deny reason to bytes.
encodeDenyReason :: DenyReason -> BS.ByteString
encodeDenyReason DenyServerFull = BS.singleton 1
encodeDenyReason DenyInvalidChallenge = BS.singleton 2
encodeDenyReason (DenyUnknown code) = BS.singleton code
-- | Decode a deny reason from bytes.
decodeDenyReason :: BS.ByteString -> DenyReason
decodeDenyReason bs = case BS.uncons bs of
Just (1, _) -> DenyServerFull
Just (2, _) -> DenyInvalidChallenge
Just (code, _) -> DenyUnknown code
Nothing -> DenyUnknown 0
-- | Convert a deny reason to a disconnect reason for event reporting.
denyToDisconnectReason :: DenyReason -> DisconnectReason
denyToDisconnectReason DenyServerFull = ReasonServerFull
denyToDisconnectReason DenyInvalidChallenge = ReasonProtocolMismatch
denyToDisconnectReason (DenyUnknown code) = ReasonUnknown code
-- | Payload header: channel (3 bits) + is_fragment (1 bit) + reserved (4 bits)
-- Encoded in first byte: [is_fragment:1][reserved:4][channel:3]
-- Channel is low 3 bits, is_fragment is high bit (0x80)
-- | Fragment flag bit in payload header.
payloadFragmentFlag :: Word8
payloadFragmentFlag = 0x80
-- | Channel mask in payload header.
payloadChannelMask :: Word8
payloadChannelMask = 0x07
-- | Decode payload header byte.
decodePayloadHeader :: Word8 -> (ChannelId, Bool)
decodePayloadHeader b =
let channel = ChannelId (b .&. payloadChannelMask)
isFragment = (b .&. payloadFragmentFlag) /= 0
in (channel, isFragment)
{-# INLINE decodePayloadHeader #-}
-- | Minimum size of a non-fragment payload: headerByte + 2 bytes channel sequence.
minPayloadSize :: Int
minPayloadSize = 3
-- | Decode channel sequence from payload bytes.
-- Returns (channelSeq, remaining data) or Nothing if too short.
decodeChannelSeq :: BS.ByteString -> Maybe (SequenceNum, BS.ByteString)
decodeChannelSeq bs
| BS.length bs < 2 = Nothing
| otherwise =
let chSeq =
SequenceNum $
(fromIntegral (BS.index bs 0) `shiftL` 8)
.|. fromIntegral (BS.index bs 1)
in Just (chSeq, BS.drop 2 bs)
{-# INLINE decodeChannelSeq #-}
-- | FNV-1a hash seed.
fnvOffsetBasis :: Word64
fnvOffsetBasis = 14695981039346656037
-- | FNV-1a hash prime.
fnvPrime :: Word64
fnvPrime = 1099511628211
-- | Convert SockAddr to a key for rate limiting using FNV-1a hash.
-- Hashes the raw address/port words directly, avoiding 'show'.
sockAddrToKey :: SockAddr -> Word64
sockAddrToKey addr = case addr of
SockAddrInet port host ->
fnvMix (fnvMix fnvOffsetBasis (fromIntegral host)) (fromIntegral (portToWord port))
SockAddrInet6 port _ (h1, h2, h3, h4) _ ->
foldl'
fnvMix
fnvOffsetBasis
[ fromIntegral (portToWord port),
fromIntegral h1,
fromIntegral h2,
fromIntegral h3,
fromIntegral h4
]
_ -> fnvMix fnvOffsetBasis 0
where
portToWord :: PortNumber -> Word64
portToWord = fromIntegral
{-# INLINE portToWord #-}
-- | FNV-1a mix step: XOR then multiply.
fnvMix :: Word64 -> Word64 -> Word64
fnvMix h val = (h `xor` val) * fnvPrime
{-# INLINE fnvMix #-}