packages feed

tls-debug-0.2.5: src/Stunnel.hs

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ScopedTypeVariables #-}
import Network.BSD
import Network.Socket
import System.IO
import System.IO.Error (isEOFError)
import System.Console.CmdArgs
import System.Certificate.X509

import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L

import Control.Concurrent (forkIO)
import Control.Concurrent.MVar
import Control.Exception (finally, throw, SomeException)
import qualified Control.Exception as E
import Control.Monad (when, forever)

import Data.Char (isDigit)

import qualified Crypto.Random.AESCtr as RNG
import Network.TLS
import Network.TLS.Extra

ciphers :: [Cipher]
ciphers =
    [ cipher_AES128_SHA1
    , cipher_AES256_SHA1
    , cipher_RC4_128_MD5
    , cipher_RC4_128_SHA1
    ]

loopUntil :: Monad m => m Bool -> m ()
loopUntil f = f >>= \v -> if v then return () else loopUntil f

readOne h = do
    r <- E.try $ hWaitForInput h (-1)
    case r of
        Left err    -> if isEOFError err then return B.empty else throw err
        Right True  -> B.hGetNonBlocking h 4096
        Right False -> return B.empty

tlsclient :: Handle -> TLSCtx -> IO ()
tlsclient srchandle dsthandle = do
    hSetBuffering srchandle NoBuffering

    handshake dsthandle

    _ <- forkIO $ forever $ do
        dat <- recvData dsthandle
        putStrLn ("received " ++ show dat)
        B.hPut srchandle dat
    loopUntil $ do
        b <- readOne srchandle
        putStrLn ("sending " ++ show b)
        if B.null b
            then do
                bye dsthandle
                return True
            else do
                sendData dsthandle (L.fromChunks [b])
                return False
    return ()

tlsserver srchandle dsthandle = do
    hSetBuffering dsthandle NoBuffering

    handshake srchandle

    loopUntil $ do
        d <- recvData srchandle
        putStrLn ("received: " ++ show d)
        sendData srchandle (L.pack $ map (toEnum . fromEnum) "this is some data")
        return False
    putStrLn "end"

data MemSessionManager = MemSessionManager (MVar [(SessionID, SessionData)])

instance SessionManager MemSessionManager where
    sessionEstablish (MemSessionManager mvar) sid sdata = modifyMVar_ mvar (\l -> return $ (sid,sdata) : l)
    sessionResume (MemSessionManager mvar) sid          = withMVar mvar (return . lookup sid)
    sessionInvalidate (MemSessionManager mvar) _        = return ()

clientProcess certs handle dsthandle dbg sessionStorage _ = do
    rng <- RNG.makeSystem
    let logging = if not dbg
            then defaultLogging
            else defaultLogging { loggingPacketSent = putStrLn . ("debug: send: " ++)
                                , loggingPacketRecv = putStrLn . ("debug: recv: " ++)
                                }

    let serverstate = maybe id (setSessionManager . MemSessionManager) sessionStorage $ defaultParamsServer
                        { pAllowedVersions = [SSL3,TLS10,TLS11,TLS12]
                        , pCiphers         = ciphers
                        , pCertificates    = certs
                        , pLogging         = logging
                        }

    ctx <- contextNewOnHandle handle serverstate rng
    tlsserver ctx dsthandle

data Stunnel =
      ClientConfig
        { destinationType :: String
        , destination     :: String
        , sourceType      :: String
        , source          :: String
        , debug           :: Bool
        , validCert       :: Bool }
    | ServerConfig
        { destinationType :: String
        , destination     :: String
        , sourceType      :: String
        , source          :: String
        , debug           :: Bool
        , disableSession  :: Bool
        , certificate     :: FilePath
        , key             :: FilePath }
    deriving (Show, Data, Typeable)

clientOpts = ClientConfig
    { destinationType = "tcp"             &= help "type of source (tcp, unix, fd)" &= typ "DESTTYPE"
    , destination     = "localhost:6061"  &= help "destination address influenced by destination type" &= typ "ADDRESS"
    , sourceType      = "tcp"             &= help "type of source (tcp, unix, fd)" &= typ "SOURCETYPE"
    , source          = "localhost:6060"  &= help "source address influenced by source type" &= typ "ADDRESS"
    , debug           = False             &= help "debug the TLS protocol printing debugging to stdout" &= typ "Bool"
    , validCert       = False             &= help "check if the certificate receive is valid" &= typ "Bool"
    }
    &= help "connect to a remote destination that use SSL/TLS"
    &= name "client"

serverOpts = ServerConfig
    { destinationType = "tcp"             &= help "type of source (tcp, unix, fd)" &= typ "DESTTYPE"
    , destination     = "localhost:6060"  &= help "destination address influenced by destination type" &= typ "ADDRESS"
    , sourceType      = "tcp"             &= help "type of source (tcp, unix, fd)" &= typ "SOURCETYPE"
    , source          = "localhost:6061"  &= help "source address influenced by source type" &= typ "ADDRESS"
    , disableSession  = False             &= help "disable support for session" &= typ "Bool"
    , debug           = False             &= help "debug the TLS protocol printing debugging to stdout" &= typ "Bool"
    , certificate     = "certificate.pem" &= help "X509 public certificate to use" &= typ "FILE"
    , key             = "certificate.key" &= help "private key linked to the certificate" &= typ "FILE"
    }
    &= help "listen for connection that use SSL/TLS and relay it to a different connection"
    &= name "server"

mode = cmdArgsMode $ modes [clientOpts,serverOpts]
    &= help "create SSL/TLS tunnel in client or server mode" &= program "stunnel" &= summary "Stunnel v0.1 (Haskell TLS)"

data StunnelAddr   =
      AddrSocket Family SockAddr
    | AddrFD Handle Handle

data StunnelHandle =
      StunnelSocket Socket
    | StunnelFd     Handle Handle

getAddressDescription :: String -> String -> IO StunnelAddr
getAddressDescription "tcp"  desc = do
    let (s, p) = break ((==) ':') desc
    when (p == "") (error "missing port: expecting [source]:port")
    pn <- if and $ map isDigit $ drop 1 p
        then return $ fromIntegral $ (read (drop 1 p) :: Int)
        else do
            service <- getServiceByName (drop 1 p) "tcp"
            return $ servicePort service
    he <- getHostByName s
    return $ AddrSocket AF_INET (SockAddrInet pn (head $ hostAddresses he))

getAddressDescription "unix" desc = do
    return $ AddrSocket AF_UNIX (SockAddrUnix desc)

getAddressDescription "fd" _  =
    return $ AddrFD stdin stdout

getAddressDescription _ _  = error "unrecognized source type (expecting tcp/unix/fd)"

connectAddressDescription (AddrSocket family sockaddr) = do
    sock <- socket family Stream defaultProtocol
    E.catch (connect sock sockaddr)
          (\(e :: SomeException) -> sClose sock >> error ("cannot open socket " ++ show sockaddr ++ " " ++ show e))
    return $ StunnelSocket sock

connectAddressDescription (AddrFD h1 h2) = do
    return $ StunnelFd h1 h2

listenAddressDescription (AddrSocket family sockaddr) = do
    sock <- socket family Stream defaultProtocol
    E.catch (bindSocket sock sockaddr >> listen sock 10 >> setSocketOption sock ReuseAddr 1)
          (\(e :: SomeException) -> sClose sock >> error ("cannot open socket " ++ show sockaddr ++ " " ++ show e))
    return $ StunnelSocket sock

listenAddressDescription (AddrFD _ _) = do
    error "cannot listen on fd"

doClient :: Stunnel -> IO ()
doClient pargs = do
    srcaddr <- getAddressDescription (sourceType pargs) (source pargs)
    dstaddr <- getAddressDescription (destinationType pargs) (destination pargs)

    let logging = if not $ debug pargs then defaultLogging else defaultLogging
                            { loggingPacketSent = putStrLn . ("debug: send: " ++)
                            , loggingPacketRecv = putStrLn . ("debug: recv: " ++)
                            }

    store <- getSystemCertificateStore
    let crecv = if validCert pargs then certificateVerifyChain store else (\_ -> return CertificateUsageAccept)
    let clientstate = defaultParamsClient { pConnectVersion = TLS10
                                          , pAllowedVersions = [TLS10,TLS11,TLS12]
                                          , pCiphers = ciphers
                                          , pCertificates = []
                                          , pLogging = logging
                                          , onCertificatesRecv = crecv
                                          }

    case srcaddr of
        AddrSocket _ _ -> do
            (StunnelSocket srcsocket) <- listenAddressDescription srcaddr
            forever $ do
                (s, _) <- accept srcsocket
                rng    <- RNG.makeSystem
                srch   <- socketToHandle s ReadWriteMode

                (StunnelSocket dst)  <- connectAddressDescription dstaddr

                dsth <- socketToHandle dst ReadWriteMode
                dstctx <- contextNewOnHandle dsth clientstate rng
                _    <- forkIO $ finally
                    (tlsclient srch dstctx)
                    (hClose srch >> hClose dsth)
                return ()
        AddrFD _ _ -> error "bad error fd. not implemented"

doServer :: Stunnel -> IO ()
doServer pargs = do
    cert    <- fileReadCertificate $ certificate pargs
    pk      <- fileReadPrivateKey $ key pargs
    srcaddr <- getAddressDescription (sourceType pargs) (source pargs)
    dstaddr <- getAddressDescription (destinationType pargs) (destination pargs)

    sessionStorage <- if disableSession pargs then return Nothing else (Just `fmap` newMVar [])

    case srcaddr of
        AddrSocket _ _ -> do
            (StunnelSocket srcsocket) <- listenAddressDescription srcaddr
            forever $ do
                (s, addr) <- accept srcsocket
                srch <- socketToHandle s ReadWriteMode
                r <- connectAddressDescription dstaddr
                dsth <- case r of
                    StunnelFd _ _     -> return stdout
                    StunnelSocket dst -> socketToHandle dst ReadWriteMode

                _ <- forkIO $ finally
                    (clientProcess [(cert, Just pk)] srch dsth (debug pargs) sessionStorage addr >> return ())
                    (hClose srch >> (when (dsth /= stdout) $ hClose dsth))
                return ()
        AddrFD _ _ -> error "bad error fd. not implemented"

main :: IO ()
main = do
    x <- cmdArgsRun mode
    case x of
        ClientConfig {} -> doClient x
        ServerConfig {} -> doServer x