packages feed

pipes-network-tls-0.1.1.0: examples/tls-tunnel.hs

{-# LANGUAGE BangPatterns #-}

-- Yeah, yeah... I know. This code could be a bit more organized.

module Main (main) where

import           Control.Concurrent.Async   as A
import           Control.Applicative
import           Control.Proxy              ((>->))
import qualified Control.Proxy              as P
import qualified Control.Proxy.TCP.TLS      as Pt
import           Data.Certificate.X509      (X509)
import           Data.Maybe                 (maybeToList)
import           Data.Monoid                ((<>))
import qualified Network.Socket             as NS
import qualified Network.TLS                as T
import           Network.TLS.Extra          as TE
import           System.Certificate.X509    (getSystemCertificateStore)
import           System.Console.GetOpt
import           System.Environment         (getProgName, getArgs)
import qualified Data.CertificateStore      as C

runTlsTunnel
  :: Pt.ServerSettings       -- ^Local server settings
  -> Pt.HostPreference       -- ^Local host to bind
  -> NS.ServiceName          -- ^Local port to bind
  -> Pt.ClientSettings       -- ^Client to remote server settings.
  -> NS.HostName             -- ^Remote host name to connect to
  -> NS.ServiceName          -- ^Remote tcp port to connect to
  -> IO ()
runTlsTunnel sS sHp sPort cS cHost cPort = do
    Pt.serve sS sHp sPort $ \(sCtx, sAddr) -> do
        let sMsg = show sAddr
        putStrLn $ sMsg <> " joined."
        putStrLn $ sMsg <> " is being tunneled to " <> show (cHost, cPort)
        Pt.connect cS cHost cPort $ \(cCtx, cAddr) -> do
            let cMsg = "Secure connection to " <> show cAddr
            putStrLn $ cMsg <> " established."
            a1 <- A.async . P.runProxy $ Pt.contextReadS sCtx >-> Pt.contextWriteD cCtx
            P.runProxy $ Pt.contextReadS cCtx >-> Pt.contextWriteD sCtx
            A.wait a1
            putStrLn $ cMsg <> " closed."
        putStrLn $ sMsg <> " quit."


main :: IO ()
main = Pt.withSocketsDo $ do
    args <- getArgs
    case getOpt RequireOrder options args of
      (actions, [locHost,locPort,remHost,remPort], _) -> do
        opts <- foldl (>>=) (return defaultOptions) actions
        let !sCred = Pt.Credential (optLocalCert opts) (optLocalKey opts) []
            smcStore = C.makeCertificateStore . pure <$> optLocalCACert opts
            sS = Pt.makeServerSettings sCred smcStore
        ccStore <- case optRemoteCACert opts of
                     Nothing -> getSystemCertificateStore
                     Just ca -> return $ C.makeCertificateStore [ca]
        let !cCreds = maybeToList $ Pt.Credential <$> optRemoteCert opts
                                                  <*> optRemoteKey opts
                                                  <*> pure []
            cS = Pt.makeClientSettings cCreds (Nothing) ccStore
        runTlsTunnel sS (Pt.Host locHost) locPort cS remHost remPort
      (_,_,msgs) -> do
        pn <- getProgName
        let header = "Usage: " <> pn
              <> " [OPTIONS] LOCAL-HOST LOCAL-PORT REMOTE-HOST REMOTE-PORT"
        error $ concat msgs ++ usageInfo header options


--------------------------------------------------------------------------------
-- The boring stuff below is related to command line parsing


data Options = Options
  { optLocalCert    :: X509
  , optLocalKey     :: T.PrivateKey
  , optLocalCACert  :: Maybe X509
  , optRemoteCert   :: Maybe X509
  , optRemoteKey    :: Maybe T.PrivateKey
  , optRemoteCACert :: Maybe X509
  } deriving (Show)

defaultOptions :: Options
defaultOptions = Options
  { optLocalCert    = error "Missing optLocalCert"
  , optLocalKey     = error "Missing optLocalKey"
  , optLocalCACert  = Nothing
  , optRemoteCert   = Nothing
  , optRemoteKey    = Nothing
  , optRemoteCACert = Nothing
  }

options :: [OptDescr (Options -> IO Options)]
options =
  [ Option [] ["lcert"]   (ReqArg readLocalCert    "FILE")
    "Local server certificate"
  , Option [] ["lkey"]    (ReqArg readLocalKey     "FILE")
    "Local server private key"
  , Option [] ["lcacert"] (OptArg readLocalCACert  "FILE")
    "If given, request a client certificate for incomming connections\
    \ and verify it against this CA."
  , Option [] ["rcert"]   (OptArg readRemoteCert   "FILE")
    "Certificate to provide to remote server if requested"
  , Option [] ["rkey"]    (OptArg readRemoteKey    "FILE")
    "Key to use together with 'rcert', if requested"
  , Option [] ["rcacert"] (OptArg readRemoteCACert "FILE")
    "If given, verify the remote server certificate using this CA,\
    \ otherwise use the operating system default CAs."
  ]

readLocalCert :: FilePath -> Options -> IO Options
readLocalCert arg opt = do
    cert <- TE.fileReadCertificate arg
    return $ opt { optLocalCert = cert }

readLocalKey :: FilePath -> Options -> IO Options
readLocalKey arg opt = do
    key <- TE.fileReadPrivateKey arg
    return $ opt { optLocalKey = key }

readLocalCACert :: Maybe FilePath -> Options -> IO Options
readLocalCACert Nothing    opt = return opt
readLocalCACert (Just arg) opt = do
    cert <- TE.fileReadCertificate arg
    return $ opt { optLocalCACert = Just cert }

readRemoteCert :: Maybe FilePath -> Options -> IO Options
readRemoteCert Nothing    opt = return opt
readRemoteCert (Just arg) opt = do
    cert <- TE.fileReadCertificate arg
    return $ opt { optRemoteCert = Just cert }

readRemoteKey :: Maybe FilePath -> Options -> IO Options
readRemoteKey Nothing    opt = return opt
readRemoteKey (Just arg) opt = do
    key <- TE.fileReadPrivateKey arg
    return $ opt { optRemoteKey = Just key }

readRemoteCACert :: Maybe FilePath -> Options -> IO Options
readRemoteCACert Nothing    opt = return opt
readRemoteCACert (Just arg) opt = do
    cert <- TE.fileReadCertificate arg
    return $ opt { optRemoteCACert = Just cert }