packages feed

socks5-0.8.0.0: Network/SOCKS5/Internal.hs

module Network.SOCKS5.Internal
  ( Method (..),
    Hello (..),
    MethodSelection (..),
    Command (..),
    Address (..),
    Request (..),
    Rep (..),
    Reply (..),
    UDPRequest (..),
    sendUDPRequestTo,
    UserPassRequest (..),
    Status (..),
    UserPassResponse (..),
    SOCKSException (..),
    Connection (..),
    fromSockAddr_,
    toSockAddr_,
    recvAndDecode,
    encodeAndSend,
  )
where

import Control.Exception
import Control.Monad
import Control.Monad.State (MonadIO (liftIO))
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString qualified as B
import Data.ByteString.Lazy qualified as LB
import Data.IP
import Data.Text.Lazy qualified as LT
import Data.Text.Lazy.Encoding qualified as LTE
import Network.Socket
import Network.Socket.ByteString qualified as SB
import Network.Socket.ByteString.Lazy qualified as LSB
import Network.TLS
import Prelude hiding (getContents)

--  The client connects to the server, and sends a version
--  identifier/method selection message:
--    +-----+----------+----------+
--    | VER | NMETHODS | METHODS  |
--    +-----+----------+----------+
--    |  1  |    1     | 1 to 255 |
--    +-----+----------+----------+

--  The values currently defined for METHOD are:
--    X'00': NO AUTHENTICATION REQUIRED
--    X'01': GSSAPI
--    X'02': USERNAME/PASSWORD
--    X'03': to X'7F' IANA ASSIGNED
--    X'80': to X'FE' RESERVED FOR PRIVATE METHODS
--    X'FF': NO ACCEPTABLE METHODS

data Method
  = NoAuth
  | GSSAPI
  | UserPass
  | IANAAssigned Word8
  | PrivateMethod Word8
  | NoAcceptableMethods
  | UnknownMethod Word8
  deriving (Eq, Show)

instance Binary Method where
  put :: Method -> Put
  put NoAuth = putWord8 0x00
  put GSSAPI = putWord8 0x01
  put UserPass = putWord8 0x02
  put (IANAAssigned m) = putWord8 m
  put (PrivateMethod m) = putWord8 m
  put NoAcceptableMethods = putWord8 0xFF
  put (UnknownMethod m) = putWord8 m

  get :: Get Method
  get = do
    m <- getWord8
    case m of
      0x00 -> return NoAuth
      0x01 -> return GSSAPI
      0x02 -> return UserPass
      _ | m >= 0x03 && m <= 0x7F -> return $ IANAAssigned m
      _ | m >= 0x80 && m <= 0xFE -> return $ PrivateMethod m
      0xFF -> return NoAcceptableMethods
      _ -> return $ UnknownMethod m

newtype Hello = Hello
  { methods :: [Method]
  }
  deriving (Eq, Show)

instance Binary Hello where
  put :: Hello -> Put
  put (Hello methods) = do
    putWord8 5
    putWord8 (fromIntegral $ length methods)
    mapM_ put methods

  get :: Get Hello
  get = do
    void getWord8
    nMethods <- getWord8
    methods <- replicateM (fromIntegral nMethods) get
    return $ Hello methods

--  The server selects from one of the methods given in METHODS, and
--  sends a METHOD selection message:
--    +-----+--------+
--    | VER | METHOD |
--    +-----+--------+
--    |  1  |   1    |
--    +-----+--------+

newtype MethodSelection = MethodSelection
  { method :: Method
  }
  deriving (Eq, Show)

instance Binary MethodSelection where
  put :: MethodSelection -> Put
  put (MethodSelection method) = do
    putWord8 5
    put method

  get :: Get MethodSelection
  get = do
    void getWord8
    MethodSelection <$> get

--  The SOCKS request is formed as follows:
--    +-----+-----+-------+------+----------+----------+
--    | VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
--    +-----+-----+-------+------+----------+----------+
--    |  1  |  1  | X'00' |  1   | Variable |    2     |
--    +-----+-----+-------+------+----------+----------+
--  Where:
--    VER: protocol version: X'05'
--    CMD:
--      CONNECT:       X'01'
--      BIND:          X'02'
--      UDP ASSOCIATE: X'03'
--    RSV: RESERVED
--    ATYP: address type of following address:
--      IP V4 address: X'01'
--      DOMAINNAME:    X'03'
--      IP V6 address: X'04'
--    DST.ADDR: desired destination address
--    DST.PORT: desired destination port in network octet order

data Command
  = Connect
  | Bind
  | UDPAssociate
  deriving (Eq, Show)

instance Binary Command where
  put :: Command -> Put
  put Connect = putWord8 0x01
  put Bind = putWord8 0x02
  put UDPAssociate = putWord8 0x03

  get :: Get Command
  get = do
    cmd <- getWord8
    case cmd of
      0x01 -> return Connect
      0x02 -> return Bind
      0x03 -> return UDPAssociate
      _ -> fail $ "Unknown command: " ++ show cmd

-- | Address type used for destination addresses in SOCKS5 protocol.
data Address
  = AddressIPv4 IPv4
  | AddressIPv6 IPv6
  | AddressDomain LT.Text
  deriving (Eq)

instance Show Address where
  show :: Address -> String
  show (AddressIPv4 ip) = show ip
  show (AddressIPv6 ip) = show ip
  show (AddressDomain name) = LT.unpack name

instance Binary Address where
  put :: Address -> Put
  put (AddressIPv4 ip) = do
    putWord8 0x01
    putWord32be $ fromIPv4w ip
  put (AddressIPv6 ip) = do
    putWord8 0x04
    let (a1, a2, a3, a4) = fromIPv6w ip
     in mapM_ putWord32be [a1, a2, a3, a4]
  put (AddressDomain name) = do
    putWord8 0x03
    let encodedName = LTE.encodeUtf8 name
        byteLength = LB.length encodedName
    if byteLength > fromIntegral (maxBound :: Word8)
      then
        error $
          "DomainName (UTF-8 encoded) has length of "
            ++ show byteLength
            ++ " bytes. Max allowed is "
            ++ show (maxBound :: Word8)
      else putWord8 $ fromIntegral byteLength
    putLazyByteString encodedName

  get :: Get Address
  get = do
    addrType <- getWord8
    case addrType of
      0x01 -> do
        AddressIPv4 . toIPv4w <$> getWord32be
      0x04 -> do
        a1 <- getWord32be
        a2 <- getWord32be
        a3 <- getWord32be
        a4 <- getWord32be
        return $ AddressIPv6 $ toIPv6w (a1, a2, a3, a4)
      0x03 -> do
        len <- getWord8
        encodedName <- getLazyByteString (fromIntegral len)
        case LTE.decodeUtf8' encodedName of
          Left err -> fail $ "Failed to decode UTF-8 domain name: " ++ show err
          Right name -> return $ AddressDomain name
      _ -> fail $ "Unknown address type: " ++ show addrType

data Request = Request
  { command :: Command,
    destinationAddress :: Address,
    destinationPort :: PortNumber
  }
  deriving (Eq, Show)

instance Binary Request where
  put :: Request -> Put
  put (Request command destinationAddress destinationPort) = do
    putWord8 0x05
    put command
    putWord8 0x00 -- RSV
    put destinationAddress
    putWord16be $ fromIntegral destinationPort

  get :: Get Request
  get = do
    void getWord8
    command <- get
    void getWord8 -- RSV, ignored
    address <- get
    port <- getWord16be
    let destinationPort = fromIntegral port
    return $ Request command address destinationPort

--  The server evaluates the request, and
--  returns a reply formed as follows:
--    +-----+-----+-------+------+----------+----------+
--    | VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
--    +-----+-----+-------+------+----------+----------+
--    |  1  |  1  | X'00' |  1   | Variable |    2     |
--    +-----+-----+-------+------+----------+----------+
--  Where:
--  VER: protocol version: X'05'
--  REP: Reply field:
--    X'00': succeeded
--    X'01': general SOCKS server failure
--    X'02': connection not allowed by ruleset
--    X'03': Network unreachable
--    X'04': Host unreachable
--    X'05': Connection refused
--    X'06': TTL expired
--    X'07': Command not supported
--    X'08': Address type not supported
--    X'09': to X'FF' unassigned
--  RSV: RESERVED
--  ATYP: address type of following address
--    IP V4 address: X'01'
--    DOMAINNAME: X'03'
--    IP V6 address: X'04'
--  BND.ADDR: server bound address
--  BND.PORT: server bound port in network octet order

data Rep
  = Succeeded
  | GeneralSOCKSFailure
  | ConnectionNotAllowedByRuleset
  | NetworkUnreachable
  | HostUnreachable
  | ConnectionRefused
  | TTLExpired
  | CommandNotSupported
  | AddressTypeNotSupported
  | Unassigned Word8
  deriving (Eq, Show)

instance Binary Rep where
  put :: Rep -> Put
  put Succeeded = putWord8 0x00
  put GeneralSOCKSFailure = putWord8 0x01
  put ConnectionNotAllowedByRuleset = putWord8 0x02
  put NetworkUnreachable = putWord8 0x03
  put HostUnreachable = putWord8 0x04
  put ConnectionRefused = putWord8 0x05
  put TTLExpired = putWord8 0x06
  put CommandNotSupported = putWord8 0x07
  put AddressTypeNotSupported = putWord8 0x08
  put (Unassigned m) = putWord8 m

  get :: Get Rep
  get = do
    rep <- getWord8
    case rep of
      0x00 -> return Succeeded
      0x01 -> return GeneralSOCKSFailure
      0x02 -> return ConnectionNotAllowedByRuleset
      0x03 -> return NetworkUnreachable
      0x04 -> return HostUnreachable
      0x05 -> return ConnectionRefused
      0x06 -> return TTLExpired
      0x07 -> return CommandNotSupported
      0x08 -> return AddressTypeNotSupported
      _ -> return $ Unassigned rep

data Reply = Reply
  { reply :: Rep,
    boundAddress :: Address,
    boundPort :: PortNumber
  }
  deriving (Eq, Show)

instance Binary Reply where
  put :: Reply -> Put
  put (Reply reply bindAddress boundPort) = do
    putWord8 0x05
    put reply
    putWord8 0x00 -- RSV
    put bindAddress
    putWord16be $ fromIntegral boundPort

  get :: Get Reply
  get = do
    void getWord8
    reply <- get
    void getWord8 -- RSV, ignored
    address <- get
    port <- getWord16be
    let boundPort = fromIntegral port
    return $ Reply reply address boundPort

--  Each UDP datagram carries a UDP request header with it:
--    +-----+------+------+----------+----------+----------+
--    | RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
--    +-----+------+------+----------+----------+----------+
--    |  2  |  1   |  1   | Variable |    2     | Variable |
--    +-----+------+------+----------+----------+----------+
--  The fields in the UDP request header are:
--    RSV: Reserved X'0000'
--    FRAG: Current fragment number
--    ATYP: address type of following addresses:
--      IP V4 address: X'01'
--      DOMAINNAME:    X'03'
--      IP V6 address: X'04'
--    DST.ADDR: desired destination address
--    DST.PORT: desired destination port
--    DATA: user data

data UDPRequest = UDPRequest
  { frag :: Word8,
    address :: Address,
    port :: PortNumber,
    payload :: LB.ByteString
  }
  deriving (Eq, Show)

instance Binary UDPRequest where
  put :: UDPRequest -> Put
  put (UDPRequest frag addr port p) = do
    putWord16be 0x0000 -- RSV
    putWord8 frag
    put addr
    putWord16be $ fromIntegral port
    putLazyByteString p

  get :: Get UDPRequest
  get = do
    void getWord16be -- RSV
    frag <- getWord8
    addr <- get
    port <- getWord16be
    let portNum = fromIntegral port
    UDPRequest frag addr portNum <$> getRemainingLazyByteString

sendUDPRequestTo :: Socket -> Word8 -> Address -> PortNumber -> LB.ByteString -> SockAddr -> IO ()
sendUDPRequestTo sock frag addr port payload =
  SB.sendAllTo sock $ LB.toStrict $ encode $ UDPRequest frag addr port payload

--  Once the SOCKS V5 server has started, and the client has selected the
--  Username/Password Authentication protocol, the Username/Password
--  subnegotiation begins. This begins with the client producing a
--  Username/Password request:
--    +-----+------+----------+------+----------+
--    | VER | ULEN |  UNAME   | PLEN |  PASSWD  |
--    +-----+------+----------+------+----------+
--    |  1  |  1   | 1 to 255 |  1   | 1 to 255 |
--    +-----+------+----------+------+----------+

data UserPassRequest = UserPassRequest
  { username :: LT.Text,
    password :: LT.Text
  }
  deriving (Eq, Show)

instance Binary UserPassRequest where
  put :: UserPassRequest -> Put
  put (UserPassRequest uname passwd) = do
    let encodedUname = LTE.encodeUtf8 uname
        encodedPasswd = LTE.encodeUtf8 passwd
        uLen = fromIntegral $ LB.length encodedUname
        pLen = fromIntegral $ LB.length encodedPasswd
    putWord8 0x01 -- VER
    putWord8 uLen
    putLazyByteString encodedUname
    putWord8 pLen
    putLazyByteString encodedPasswd

  get :: Get UserPassRequest
  get = do
    void getWord8 -- VER, ignored
    uLen <- getWord8
    uname <- getLazyByteString (fromIntegral uLen)
    pLen <- getWord8
    passwd <- getLazyByteString (fromIntegral pLen)
    case (LTE.decodeUtf8' uname, LTE.decodeUtf8' passwd) of
      (Left err, _) -> fail $ "Failed to decode username: " ++ show err
      (_, Left err) -> fail $ "Failed to decode password: " ++ show err
      (Right u, Right p) -> return $ UserPassRequest u p

--  The server verifies the supplied UNAME and PASSWD, and sends the
--  following response:
--    +-----+--------+
--    | VER | STATUS |
--    +-----+--------+
--    |  1  |   1    |
--    +-----+--------+

data Status
  = Success
  | Failure
  deriving (Eq, Show)

instance Binary Status where
  put :: Status -> Put
  put Success = putWord8 0x00
  put Failure = putWord8 0x01

  get :: Get Status
  get = do
    status <- getWord8
    case status of
      0x00 -> return Success
      _ -> return Failure

newtype UserPassResponse = UserPassResponse
  { status :: Status
  }
  deriving (Eq, Show)

instance Binary UserPassResponse where
  put :: UserPassResponse -> Put
  put (UserPassResponse status) = do
    putWord8 0x01 -- VER
    put status

  get :: Get UserPassResponse
  get = do
    void getWord8 -- VER, ignored
    UserPassResponse <$> get

data SOCKSException
  = HandshakeFail Reply
  | AuthUnsupported Method
  | AuthFailed Status
  | AuthMissingCredentials
  | ConnectionToTargetFailed SomeException
  | NoAcceptableAuthMethods
  | PortStringInvalid ServiceName
  deriving (Show)

instance Exception SOCKSException

class Connection c where
  connRecv :: c -> IO B.ByteString
  connSend :: c -> LB.ByteString -> IO ()

instance Connection Socket where
  connRecv :: Socket -> IO B.ByteString
  connRecv sock = SB.recv sock 4096

  connSend :: Socket -> LB.ByteString -> IO ()
  connSend = LSB.sendAll

instance Connection Context where
  connRecv :: Context -> IO B.ByteString
  connRecv = recvData

  connSend :: Context -> LB.ByteString -> IO ()
  connSend = sendData

fromSockAddr_ :: SockAddr -> (Address, PortNumber)
fromSockAddr_ (SockAddrInet port host) = (AddressIPv4 (fromHostAddress host), fromIntegral port)
fromSockAddr_ (SockAddrInet6 port _ host _) = (AddressIPv6 (toIPv6w host), fromIntegral port)
fromSockAddr_ path = error $ "Unexpected Unix socket address: " ++ show path

toSockAddr_ :: Address -> PortNumber -> SockAddr
toSockAddr_ (AddressIPv4 host) port = SockAddrInet (fromIntegral port) $ toHostAddress host
toSockAddr_ (AddressIPv6 host) port = SockAddrInet6 (fromIntegral port) 0 (toHostAddress6 host) 0
toSockAddr_ (AddressDomain host) _ = error $ "Address conversion for DomainName not implemented: " ++ show host

recvAndDecode :: (Binary a, Connection c, MonadIO m) => c -> B.ByteString -> m (a, B.ByteString)
recvAndDecode conn buffer = liftIO $ go $ pushChunk (runGetIncremental get) buffer
  where
    go :: Decoder a -> IO (a, B.ByteString)
    go (Done left _ val) = return (val, left)
    go (Fail _ _ err) = throwIO $ userError $ "SOCKS5 parse error: " ++ err
    go (Partial k) = do
      chunk <- connRecv conn
      if B.null chunk
        then go (k Nothing)
        else go (k (Just chunk))

encodeAndSend :: (Binary a, Connection c, MonadIO m) => c -> a -> m ()
encodeAndSend conn val = do
  liftIO $ connSend conn $ encode val