packages feed

second-transfer-0.10.0.1: hs-src/SecondTransfer/TLS/CoreServer.hs

{-# LANGUAGE RankNTypes,
             FunctionalDependencies,
             PartialTypeSignatures,
             OverloadedStrings,
             ScopedTypeVariables,
             TemplateHaskell
             #-}
module SecondTransfer.TLS.CoreServer (
               -- * Simpler interfaces
               -- These functions are simple enough but don't work with controllable
               -- processes.
                 tlsServeWithALPN
               , tlsServeWithALPNNSSockAddr
               , tlsSessionHandler
               , tlsServeWithALPNUnderSOCKS5SockAddr

               -- * Interfaces for a pre-fork scenario
               -- The first part of the invocation opens and
               -- bind the socket, the second part does the accepting...
               , NormalTCPHold
               , tlsServeWithALPNNSSockAddr_Prepare
               , tlsServeWithALPNNSSockAddr_Do
               , Socks5Hold

               , tlsServeWithALPNUnderSOCKS5SockAddr_Prepare
               , tlsServeWithALPNUnderSOCKS5SockAddr_Do

               , coreListen

               -- * Utility
               , NamedAttendants
               , chooseProtocol

               -- * Conduit-based session management
               -- , coreItcli

       ) where

import           Control.Concurrent
--import           Control.Monad.IO.Class                                    (liftIO)
--import           Control.Monad                                             (when)
import           Control.Lens                                              ( (^.), makeLenses, over, set )

--import           Data.Conduit
-- import qualified Data.Conduit                                              as Con(yield)
--import qualified Data.Conduit.List                                         as CL
import           Data.Typeable                                             (Proxy(..))
import           Data.List                                                 (elemIndex)
import           Data.Maybe                                                (-- fromMaybe,
                                                                            isJust)
import qualified Data.ByteString                                           as B
import           Data.ByteString.Char8                                     (pack, unpack)
import           Data.Int                                                  (Int64)
-- import           Data.IORef

--import qualified Data.ByteString.Lazy                                      as LB
--import qualified Data.ByteString.Builder                                   as Bu

import qualified Network.Socket                                            as NS

import           SecondTransfer.IOCallbacks.Types
import           SecondTransfer.TLS.Types
import           SecondTransfer.IOCallbacks.SocketServer
import           SecondTransfer.IOCallbacks.WrapSocket                     (HasSocketPeer(..))

import           SecondTransfer.Socks5.Session                             (tlsSOCKS5Serve, initSocks5ServerState)
import           SecondTransfer.Socks5.Types                               (Socks5ConnectionCallbacks)
import           SecondTransfer.Exception                                  (forkIOExc)

import           SecondTransfer.Sessions.HashableSockAddr                  (hashableSockAddrFromNSSockAddr)

--import           Debug.Trace                                               (traceStack)


data SessionHandlerState = SessionHandlerState {
    _liveSessions_S    ::  !Int64
  , _nextConnId_S      ::  !Int64
  , _connCallbacks_S   ::  ConnectionCallbacks
    }

makeLenses ''SessionHandlerState


-- | A simple Alias
type NamedAttendants = [(String, Attendant)]


-- | Convenience function to open a port and listen there for connections and
--   select protocols and so on.
tlsServeWithALPN ::   forall ctx session . (TLSContext ctx session)
                 => (Proxy ctx )          -- ^ This is a simple proxy type from Typeable that is used to select the type
                                          --   of TLS backend to use during the invocation
                 -> ConnectionCallbacks   -- ^ Control and log connections
                 -> B.ByteString              -- ^ String with contents of certificate chain
                 -> B.ByteString              -- ^ String with contents of PKCS #8 key
                 -> String                -- ^ Name of the network interface
                 -> NamedAttendants       -- ^ List of attendants and their handlers
                 -> Int                   -- ^ Port to listen for connections
                 -> IO ()
tlsServeWithALPN proxy  conn_callbacks cert_filename key_filename interface_name attendants interface_port = do
    listen_socket <- createAndBindListeningSocket interface_name interface_port
    coreListen proxy conn_callbacks cert_filename key_filename listen_socket tlsServe attendants

-- | Use a previously given network address
tlsServeWithALPNNSSockAddr ::   forall ctx session . (TLSContext ctx session)
                 => (Proxy ctx )          -- ^ This is a simple proxy type from Typeable that is used to select the type
                                          --   of TLS backend to use during the invocation
                 -> ConnectionCallbacks   -- ^ Control and regulate SOCKS5 connections
                 -> B.ByteString              -- ^ String with contents of certificate chain
                 -> B.ByteString              -- ^ String with contents of PKCS #8 key
                 -> NS.SockAddr           -- ^ Address to bind to
                 -> NamedAttendants        -- ^ List of attendants and their handlers
                 -> IO ()
tlsServeWithALPNNSSockAddr proxy conn_callbacks  cert_filename key_filename sock_addr attendants = do
    listen_socket <- createAndBindListeningSocketNSSockAddr sock_addr
    coreListen proxy conn_callbacks cert_filename key_filename listen_socket tlsServe attendants

data NormalTCPHold   = NormalTCPHold ( IO () )

-- | The prefork way requires a first step where we create the sockets and then we listen on them...
--   This function is identical otherwise to the one without _Prepare. The real thing is done by the
--   one with _Do below...
tlsServeWithALPNNSSockAddr_Prepare ::   forall ctx session . (TLSContext ctx session)
                 => (Proxy ctx )          -- ^ This is a simple proxy type from Typeable that is used to select the type
                                          --   of TLS backend to use during the invocation
                 -> ConnectionCallbacks   -- ^ Control and regulate SOCKS5 connections
                 -> B.ByteString              -- ^ String with contents of certificate chain
                 -> B.ByteString              -- ^ String with contents of PKCS #8 key
                 -> NS.SockAddr           -- ^ Address to bind to
                 -> IO NamedAttendants    -- ^ Will-be list of attendants and their handlers
                 -> IO NormalTCPHold
tlsServeWithALPNNSSockAddr_Prepare proxy conn_callbacks  cert_filename key_filename sock_addr make_attendants = do
    listen_socket <- createAndBindListeningSocketNSSockAddr sock_addr
    return . NormalTCPHold $ do
        attendants <- make_attendants
        coreListen proxy conn_callbacks cert_filename key_filename listen_socket tlsServe attendants


-- | Actually listen, possibly at the other side of the fork.
tlsServeWithALPNNSSockAddr_Do :: NormalTCPHold  -> IO ()
tlsServeWithALPNNSSockAddr_Do (NormalTCPHold action) = action


tlsServeWithALPNUnderSOCKS5SockAddr ::   forall ctx session  . (TLSContext ctx session)
                 => Proxy ctx             -- ^ This is a simple proxy type from Typeable that is used to select the type
                                          --   of TLS backend to use during the invocation
                 -> ConnectionCallbacks   -- ^ Log and control of the inner TLS session
                 -> Socks5ConnectionCallbacks -- ^ Log and control the outer SOCKS5 session
                 -> B.ByteString              -- ^ String with contents of certificate chain
                 -> B.ByteString              -- ^ String with contents of PKCS #8 key
                 -> NS.SockAddr           -- ^ Address to bind to
                 -> NamedAttendants       -- ^ List of attendants and their handlers,
                 -> [B.ByteString]        -- ^ Names of "internal" hosts
                 -> Bool                  -- ^ Should I forward connection requests?
                 -> IO ()
tlsServeWithALPNUnderSOCKS5SockAddr
    proxy
    conn_callbacks
    socks5_callbacks
    cert_filename
    key_filename
    host_addr
    attendants
    internal_hosts
    forward_no_internal = do
    let
        approver :: B.ByteString -> Bool
        approver name = isJust $ elemIndex name internal_hosts
    socks5_state_mvar <- newMVar initSocks5ServerState
    listen_socket <- createAndBindListeningSocketNSSockAddr host_addr
    coreListen
       proxy
       conn_callbacks
       cert_filename
       key_filename
       listen_socket
       (tlsSOCKS5Serve socks5_state_mvar socks5_callbacks approver forward_no_internal)
       attendants


-- | Opaque hold type
data Socks5Hold = Socks5Hold (IO ())


tlsServeWithALPNUnderSOCKS5SockAddr_Prepare ::   forall ctx session  . (TLSContext ctx session)
                 => Proxy ctx             -- ^ This is a simple proxy type from Typeable that is used to select the type
                                          --   of TLS backend to use during the invocation
                 -> ConnectionCallbacks   -- ^ Log and control of the inner TLS session
                 -> Socks5ConnectionCallbacks -- ^ Log and control the outer SOCKS5 session
                 -> B.ByteString              -- ^ String with contents of certificate chain
                 -> B.ByteString              -- ^ String with contents of PKCS #8 key
                 -> NS.SockAddr           -- ^ Address to bind to
                 -> IO NamedAttendants    -- ^ List of attendants and their handlers, as it will be built
                 -> [B.ByteString]        -- ^ Names of "internal" hosts
                 -> Bool                  -- ^ Should I forward connection requests?
                 -> IO Socks5Hold
tlsServeWithALPNUnderSOCKS5SockAddr_Prepare
    proxy
    conn_callbacks
    socks5_callbacks
    cert_pemfile_data
    key_pemfile_data
    host_addr
    make_attendants
    internal_hosts
    forward_no_internal = do
    let
        approver :: B.ByteString -> Bool
        approver name = isJust $ elemIndex name internal_hosts
    socks5_state_mvar <- newMVar initSocks5ServerState
    listen_socket <- createAndBindListeningSocketNSSockAddr host_addr
    return . Socks5Hold $ do
        attendants <- make_attendants
        coreListen
            proxy
            conn_callbacks
            cert_pemfile_data
            key_pemfile_data
            listen_socket
            (tlsSOCKS5Serve socks5_state_mvar socks5_callbacks approver forward_no_internal)
            attendants


tlsServeWithALPNUnderSOCKS5SockAddr_Do :: Socks5Hold -> IO ()
tlsServeWithALPNUnderSOCKS5SockAddr_Do (Socks5Hold action) = action


tlsSessionHandler ::  (TLSContext ctx session, TLSServerIO encrypted_io, HasSocketPeer encrypted_io) =>
       MVar SessionHandlerState
       -> NamedAttendants
       ->  ctx
       ->  encrypted_io
       -> IO ()
tlsSessionHandler session_handler_state_mvar attendants ctx encrypted_io = do
    -- Have the handshake happen in another thread
    _ <- forkIOExc "tlsSessionHandler" $ do

      -- Get a new connection id
      (new_conn_id, live_now) <- modifyMVar session_handler_state_mvar $ \ s -> do
          let new_conn_id = s ^. nextConnId_S
              live_now_ = (s ^. liveSessions_S) + 1
              new_s = over nextConnId_S ( + 1 ) s
              new_new_s = live_now_ `seq` set liveSessions_S live_now_ new_s
          return $  new_new_s `seq` (new_new_s, (new_conn_id, live_now_) )

      connection_callbacks <- withMVar session_handler_state_mvar $ \ s -> do
          return $ s ^. connCallbacks_S
      let
          log_events_maybe = connection_callbacks ^. logEvents_CoCa
          log_event :: ConnectionEvent -> IO ()
          log_event ev = case log_events_maybe of
              Nothing -> return ()
              Just c -> c ev
          wconn_id = ConnectionId new_conn_id
      sock_addr <- getSocketPeerAddress encrypted_io
      let
          hashable_addr = hashableSockAddrFromNSSockAddr sock_addr
          connection_data = ConnectionData hashable_addr
      log_event (Established_CoEv sock_addr wconn_id live_now)
      session <- unencryptTLSServerIO ctx encrypted_io
      plaintext_io_callbacks_u <- handshake session :: IO IOCallbacks

      close_reported <- newMVar False

      let
          instr = do
              modifyMVar_ close_reported $ \ close_reported_x -> do
                  if (not close_reported_x) then  do
                      -- We can close just once
                      plaintext_io_callbacks_u ^. closeAction_IOC
                      log_event (Ended_CoEv wconn_id)
                      modifyMVar_ session_handler_state_mvar $ \ s -> do
                          let
                              live_now_ = (s ^. liveSessions_S) - 1
                              new_new_s = set liveSessions_S live_now_ s
                          new_new_s `seq` return new_new_s
                      return True
                    else
                      return close_reported_x

          plaintext_io_callbacks = set closeAction_IOC instr plaintext_io_callbacks_u

      maybe_sel_prot <- getSelectedProtocol session
      let maybe_attendant =
            case maybe_sel_prot of
                Just (_, prot_name) ->
                    lookup (unpack prot_name) attendants
                Nothing ->
                    lookup "" attendants
      case maybe_attendant of
          Just use_attendant ->
              use_attendant connection_data plaintext_io_callbacks
          Nothing -> do
              log_event (ALPNFailed_CoEv wconn_id)
              plaintext_io_callbacks ^. closeAction_IOC
    return ()


-- tlsSessionHandlerControllable ::  (TLSContext ctx session, TLSServerIO encrypted_io)
--     => [(String, ControllableAttendant controller)]  ->  ctx ->  encrypted_io -> IO (Maybe controller)
-- tlsSessionHandlerControllable attendants ctx encrypted_io = do
--     session <- unencryptTLSServerIO ctx encrypted_io
--     plaintext_io_callbacks <- handshake session :: IO IOCallbacks
--     maybe_sel_prot <- getSelectedProtocol session
--     case maybe_sel_prot of
--         Just (_, prot_name) -> do
--             let
--                 Just  use_attendant = lookup (unpack prot_name) attendants
--             x <- use_attendant plaintext_io_callbacks
--             return . Just $ x

--         Nothing -> do
--             let
--                 maybe_attendant = lookup "" attendants
--             case maybe_attendant of
--                 Just use_attendant -> do
--                     x <- use_attendant plaintext_io_callbacks
--                     return . Just $ x
--                 Nothing -> do
--                     -- Silently do nothing, and close the connection
--                     plaintext_io_callbacks ^. closeAction_IOC
--                     return Nothing



chooseProtocol :: [(String, a)] ->  [B.ByteString] -> IO (Maybe Int)
chooseProtocol attendants proposed_protocols =
    let
        i_want_protocols = map (pack . fst) attendants
        chosen =
            foldl
                ( \ selected want_protocol ->
                    case (selected, elemIndex want_protocol proposed_protocols) of
                        ( Just a, _) -> Just a
                        (_,   Just idx) -> Just idx
                        (_,   _ ) -> Nothing
                )
                Nothing
                i_want_protocols
    in return chosen


coreListen ::
       forall a ctx session b . (TLSContext ctx session, TLSServerIO b, HasSocketPeer b)
     => (Proxy ctx )                      -- ^ This is a simple proxy type from Typeable that is used to select the type
                                          --   of TLS backend to use during the invocation
     -> ConnectionCallbacks               -- ^ Functions to log and control behaviour of the server
     -> B.ByteString                      -- ^ PEM-encoded certificate chain, in this string
     -> B.ByteString                      -- ^ PEM-encoded, un-encrypted PKCS #8 key in this string
     -> a                                 -- ^ An entity that is used to fork new handlers
     -> ( a -> (b -> IO()) -> IO () )    -- ^ The fork-handling functionality
     -> [(String, Attendant)]             -- ^ List of attendants and their handlers
     -> IO ()
coreListen _ conn_callbacks certificate_pemfile_data key_pemfile_data listen_abstraction session_forker attendants =   do
     let
         state = SessionHandlerState {
             _liveSessions_S = 0
           , _nextConnId_S = 0
           , _connCallbacks_S = conn_callbacks
             }
     state_mvar <- newMVar state
     ctx <- newTLSContextFromMemory certificate_pemfile_data key_pemfile_data (chooseProtocol attendants) :: IO ctx
     session_forker listen_abstraction (tlsSessionHandler state_mvar attendants ctx)


-- | A conduit that takes TLS-encrypted callbacks, creates a TLS server session on top of it, passes the resulting
--   plain-text io-callbacks to a chosen Attendant in the argument list, and passes up the controller of the attendant
--   so that it can be undone if needs come. This should be considered a toy API, as multiple handshake can not progress
--   simultaeneusly through Conduits, so a server using this would be blocked for the entire length of a TLS handshake
--   with a remote client .... :-(


-- coreItcli ::
--          forall ctx session b . (TLSContext ctx session, TLSServerIO b)
--      => ctx                                                      -- ^ Passing in a tls context already-built value allows for sharing a single
--                                                                  --   context across multiple listening abstractions...
--      -> [(String, Attendant)]             -- ^ List of attendants and their handlers
--      -> Conduit b IO ()
-- coreItcli  ctx  controllable_attendants = do
--     let
--         monomorphicHandler :: [(String, Attendant )]  ->  ctx ->  b -> IO ()
--         monomorphicHandler =  tlsSessionHandler
--     CL.mapMaybeM  $  liftIO <$> monomorphicHandler controllable_attendants ctx