Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Sketch WSS implementation #215

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

.hpc
dist
dist-newstyle
tests/coverage

tests/haskell/TestSuite
Expand Down
53 changes: 53 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{ nixpkgs ? import <nixpkgs> {}, compiler ? "ghc962", doBenchmark ? false }:

let

inherit (nixpkgs) pkgs;

f = { mkDerivation, async, attoparsec, base, base64-bytestring
, binary, bytestring, case-insensitive, connection, containers
, criterion, cryptonite, data-default-class, entropy, HUnit, lib
, network, QuickCheck, random, SHA, streaming-commons
, test-framework, test-framework-hunit, test-framework-quickcheck2
, text, tls, tls-session-manager
}:
mkDerivation {
pname = "websockets";
version = "0.13.0.0";
src = ./.;
isLibrary = true;
isExecutable = true;
libraryHaskellDepends = [
async attoparsec base base64-bytestring binary bytestring
case-insensitive connection containers cryptonite
data-default-class entropy network random SHA streaming-commons
text tls tls-session-manager
];
testHaskellDepends = [
async attoparsec base base64-bytestring binary bytestring
case-insensitive containers entropy HUnit network QuickCheck random
SHA streaming-commons test-framework test-framework-hunit
test-framework-quickcheck2 text
];
benchmarkHaskellDepends = [
async attoparsec base base64-bytestring binary bytestring
case-insensitive containers criterion entropy network random SHA
text
];
doCheck = false;
homepage = "http://jaspervdj.be/websockets";
description = "A sensible and clean way to write WebSocket-capable servers in Haskell";
license = lib.licenses.bsd3;
};

haskellPackages = if compiler == "default"
then pkgs.haskellPackages
else pkgs.haskell.packages.${compiler};

variant = if doBenchmark then pkgs.haskell.lib.doBenchmark else pkgs.lib.id;

drv = variant (haskellPackages.callPackage f {});

in

if pkgs.lib.inNixShell then drv.env else drv
102 changes: 102 additions & 0 deletions src/Network/WebSockets/Connection/Options.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,28 @@

, SizeLimit (..)
, atMostSizeLimit

, TLSSettings (..)
, defaultTlsSettings

, CertSettings (..)
, defaultCertSettings
) where


--------------------------------------------------------------------------------
import Data.Int (Int64)
import Data.Monoid (Monoid (..))

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.6 on ubuntu-latest

The import of ‘Data.Monoid’ is redundant

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on ubuntu-latest

The import of ‘Data.Monoid’ is redundant

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

The import of ‘Data.Monoid’ is redundant

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.4 on ubuntu-latest

The import of ‘Data.Monoid’ is redundant

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

The import of ‘Data.Monoid’ is redundant

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on windows-latest

The import of ‘Data.Monoid’ is redundant

Check warning on line 24 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on macos-latest

The import of ‘Data.Monoid’ is redundant
import Prelude

import qualified Crypto.PubKey.DH as DH

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.6 on ubuntu-latest

Could not load module ‘Crypto.PubKey.DH’

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on ubuntu-latest

Could not load module ‘Crypto.PubKey.DH’.

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

Could not load module ‘Crypto.PubKey.DH’

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.4 on ubuntu-latest

Could not load module ‘Crypto.PubKey.DH’

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

Could not load module ‘Crypto.PubKey.DH’

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on windows-latest

Could not load module ‘Crypto.PubKey.DH’.

Check failure on line 27 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on macos-latest

Could not load module ‘Crypto.PubKey.DH’.
import qualified Data.ByteString as B
import Data.Default.Class (def)

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.6 on ubuntu-latest

Could not load module ‘Data.Default.Class’

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on ubuntu-latest

Could not load module ‘Data.Default.Class’.

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

Could not load module ‘Data.Default.Class’

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.4 on ubuntu-latest

Could not load module ‘Data.Default.Class’

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

Could not load module ‘Data.Default.Class’

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on windows-latest

Could not load module ‘Data.Default.Class’.

Check failure on line 29 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on macos-latest

Could not load module ‘Data.Default.Class’.
import qualified Data.IORef as IO
import qualified Network.TLS as TLS

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.6 on ubuntu-latest

Could not load module ‘Network.TLS’

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on ubuntu-latest

Could not load module ‘Network.TLS’.

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

Could not load module ‘Network.TLS’

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.4 on ubuntu-latest

Could not load module ‘Network.TLS’

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

Could not load module ‘Network.TLS’

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on windows-latest

Could not load module ‘Network.TLS’.

Check failure on line 31 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on macos-latest

Could not load module ‘Network.TLS’.
import qualified Network.TLS.Extra as TLSExtra

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.6 on ubuntu-latest

Could not load module ‘Network.TLS.Extra’

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on ubuntu-latest

Could not load module ‘Network.TLS.Extra’.

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

Could not load module ‘Network.TLS.Extra’

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.4 on ubuntu-latest

Could not load module ‘Network.TLS.Extra’

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

Could not load module ‘Network.TLS.Extra’

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on windows-latest

Could not load module ‘Network.TLS.Extra’.

Check failure on line 32 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on macos-latest

Could not load module ‘Network.TLS.Extra’.
import qualified Network.TLS.SessionManager as SM

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.6 on ubuntu-latest

Could not load module ‘Network.TLS.SessionManager’

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on ubuntu-latest

Could not load module ‘Network.TLS.SessionManager’.

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

Could not load module ‘Network.TLS.SessionManager’

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.4 on ubuntu-latest

Could not load module ‘Network.TLS.SessionManager’

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

Could not load module ‘Network.TLS.SessionManager’

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on windows-latest

Could not load module ‘Network.TLS.SessionManager’.

Check failure on line 33 in src/Network/WebSockets/Connection/Options.hs

View workflow job for this annotation

GitHub Actions / GHC 9.8 on macos-latest

Could not load module ‘Network.TLS.SessionManager’.


--------------------------------------------------------------------------------
-- | Set options for a 'Connection'. Please do not use this constructor
Expand Down Expand Up @@ -52,6 +66,7 @@
-- compressed messages, as well as the size of the uncompressed messages
-- as we are deflating them to ensure we don't use too much memory in any
-- case.
, connectionTlsSettings :: !(Maybe TLSSettings)
}


Expand All @@ -70,6 +85,7 @@
, connectionStrictUnicode = False
, connectionFramePayloadSizeLimit = mempty
, connectionMessageDataSizeLimit = mempty
, connectionTlsSettings = Nothing
}


Expand Down Expand Up @@ -130,3 +146,89 @@
atMostSizeLimit _ NoSizeLimit = True
atMostSizeLimit s (SizeLimit l) = s <= l
{-# INLINE atMostSizeLimit #-}

--------------------------------------------------------------------------------
-- | Determines where to load the certificate, chain
-- certificates, and key from.
data CertSettings
= CertFromFile !FilePath ![FilePath] !FilePath
| CertFromMemory !B.ByteString ![B.ByteString] !B.ByteString
| CertFromRef !(IO.IORef B.ByteString) ![IO.IORef B.ByteString] !(IO.IORef B.ByteString)

-- | The default 'CertSettings'.
defaultCertSettings :: CertSettings
defaultCertSettings = CertFromFile "certificate.pem" [] "key.pem"

--------------------------------------------------------------------------------
data TLSSettings = TLSSettings {
certSettings :: CertSettings
-- ^ Where are the certificate, chain certificates, and key
-- loaded from?
--
-- >>> certSettings defaultTlsSettings
-- tlsSettings "certificate.pem" "key.pem"
, tlsLogging :: TLS.Logging
-- ^ The level of logging to turn on.
--
-- Default: 'TLS.defaultLogging'.
, tlsAllowedVersions :: [TLS.Version]
-- ^ The TLS versions this server accepts.
--
-- >>> tlsAllowedVersions defaultTlsSettings
-- [TLS13,TLS12,TLS11,TLS10]
, tlsCiphers :: [TLS.Cipher]
-- ^ The TLS ciphers this server accepts.
--
-- >>> tlsCiphers defaultTlsSettings
-- [ECDHE-ECDSA-AES256GCM-SHA384,ECDHE-ECDSA-AES128GCM-SHA256,ECDHE-RSA-AES256GCM-SHA384,ECDHE-RSA-AES128GCM-SHA256,DHE-RSA-AES256GCM-SHA384,DHE-RSA-AES128GCM-SHA256,ECDHE-ECDSA-AES256CBC-SHA384,ECDHE-RSA-AES256CBC-SHA384,DHE-RSA-AES256-SHA256,ECDHE-ECDSA-AES256CBC-SHA,ECDHE-RSA-AES256CBC-SHA,DHE-RSA-AES256-SHA1,RSA-AES256GCM-SHA384,RSA-AES256-SHA256,RSA-AES256-SHA1,AES128GCM-SHA256,AES256GCM-SHA384]
, tlsWantClientCert :: Bool
-- ^ Whether or not to demand a certificate from the client. If this
-- is set to True, you must handle received certificates in a server hook
-- or all connections will fail.
--
-- >>> tlsWantClientCert defaultTlsSettings
-- False
, tlsServerHooks :: TLS.ServerHooks
-- ^ The server-side hooks called by the tls package, including actions
-- to take when a client certificate is received. See the "Network.TLS"
-- module for details.
--
-- Default: def
, tlsServerDHEParams :: Maybe DH.Params
-- ^ Configuration for ServerDHEParams
-- more function lives in `cryptonite` package
--
-- Default: Nothing
, tlsSessionManagerConfig :: Maybe SM.Config
-- ^ Configuration for in-memory TLS session manager.
-- If Nothing, 'TLS.noSessionManager' is used.
-- Otherwise, an in-memory TLS session manager is created
-- according to 'Config'.
--
-- Default: Nothing
, tlsCredentials :: Maybe TLS.Credentials
-- ^ Specifying 'TLS.Credentials' directly. If this value is
-- specified, other fields such as 'certFile' are ignored.
, tlsSessionManager :: Maybe TLS.SessionManager
-- ^ Specifying 'TLS.SessionManager' directly. If this value is
-- specified, 'tlsSessionManagerConfig' is ignored.
}

defaultTlsSettings :: TLSSettings
defaultTlsSettings =
TLSSettings
{ certSettings = defaultCertSettings
, tlsLogging = def
, tlsAllowedVersions = [TLS.TLS13,TLS.TLS12]
, tlsCiphers = ciphers
, tlsWantClientCert = False
, tlsServerHooks = def
, tlsServerDHEParams = Nothing
, tlsSessionManagerConfig = Nothing
, tlsCredentials = Nothing
, tlsSessionManager = Nothing
}
where
-- taken from stunnel example in tls-extra
ciphers :: [TLS.Cipher]
ciphers = TLSExtra.ciphersuite_strong
4 changes: 3 additions & 1 deletion src/Network/WebSockets/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ runApp socket opts app =
makePendingConnection
:: Socket -> ConnectionOptions -> IO PendingConnection
makePendingConnection socket opts = do
stream <- Stream.makeSocketStream socket
stream <- case connectionTlsSettings opts of
Nothing -> Stream.makeSocketStream socket
Just tls -> Stream.makeTlsSocketStream tls socket
makePendingConnectionFromStream stream opts


Expand Down
104 changes: 97 additions & 7 deletions src/Network/WebSockets/Stream.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
--------------------------------------------------------------------------------
-- | Lightweight abstraction over an input/output stream.
{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
module Network.WebSockets.Stream
( Stream
, makeStream
Expand All @@ -10,16 +11,23 @@
, parseBin
, write
, close
-- * TLS
, makeTlsSocketStream
, streamTlsContext
) where

import Control.Applicative ((<|>))

Check warning on line 19 in src/Network/WebSockets/Stream.hs

View workflow job for this annotation

GitHub Actions / GHC 9.2 on ubuntu-latest

The import of ‘Control.Applicative’ is redundant

Check warning on line 19 in src/Network/WebSockets/Stream.hs

View workflow job for this annotation

GitHub Actions / GHC 9.0 on ubuntu-latest

The import of ‘Control.Applicative’ is redundant
import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar,
putMVar, takeMVar, withMVar)
import Control.Exception (SomeException, SomeAsyncException, throwIO, catch, try, fromException)
import Control.Exception (SomeException, SomeAsyncException, throwIO, catch, handle, try, fromException)
import Control.Monad (forM_)
import qualified Data.Attoparsec.ByteString as Atto
import qualified Data.Binary.Get as BIN
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.Default.Class (def)
import Data.Functor ((<&>))
import qualified Data.IORef as IO
import Data.IORef (IORef, atomicModifyIORef',
newIORef, readIORef,
writeIORef)
Expand All @@ -33,7 +41,11 @@
#endif
import System.IO.Error (isResourceVanishedError)

import qualified Network.TLS as TLS
import qualified Network.TLS.SessionManager as SM
import Network.WebSockets.Types
import Network.WebSockets.Connection.Options
import System.IO.Error (isEOFError)


--------------------------------------------------------------------------------
Expand All @@ -46,12 +58,12 @@
--------------------------------------------------------------------------------
-- | Lightweight abstraction over an input/output stream.
data Stream = Stream
{ streamIn :: IO (Maybe B.ByteString)
, streamOut :: (Maybe BL.ByteString -> IO ())
, streamState :: !(IORef StreamState)
{ streamIn :: IO (Maybe B.ByteString)
, streamOut :: (Maybe BL.ByteString -> IO ())
, streamState :: !(IORef StreamState)
, streamTlsContext :: Maybe TLS.Context
}


--------------------------------------------------------------------------------
-- | Create a stream from a "receive" and "send" action. The following
-- properties apply:
Expand All @@ -73,7 +85,7 @@
ref <- newIORef (Open B.empty)
receiveLock <- newMVar ()
sendLock <- newMVar ()
return $ Stream (receive' ref receiveLock) (send' ref sendLock) ref
return $ Stream (receive' ref receiveLock) (send' ref sendLock) ref Nothing
where
closeRef :: IORef StreamState -> IO ()
closeRef ref = atomicModifyIORef' ref $ \state -> case state of
Expand Down Expand Up @@ -111,7 +123,6 @@
Nothing -> what *> pure ()
throwIO e


--------------------------------------------------------------------------------
makeSocketStream :: S.Socket -> IO Stream
makeSocketStream socket = makeStream receive send
Expand All @@ -133,6 +144,85 @@
forM_ (BL.toChunks bs) (SB.sendAll socket)
#endif

loadCredentials :: TLSSettings -> IO TLS.Credentials
loadCredentials TLSSettings{ tlsCredentials = Just creds } = return creds
loadCredentials TLSSettings{..} = case certSettings of
CertFromFile cert chainFiles key -> do
cred <- either error id <$> TLS.credentialLoadX509Chain cert chainFiles key
return $ TLS.Credentials [cred]
CertFromRef certRef chainCertsRef keyRef -> do
cert <- IO.readIORef certRef
chainCerts <- mapM IO.readIORef chainCertsRef
key <- IO.readIORef keyRef
cred <- either error return $ TLS.credentialLoadX509ChainFromMemory cert chainCerts key
return $ TLS.Credentials [cred]
CertFromMemory certMemory chainCertsMemory keyMemory -> do
cred <- either error return $ TLS.credentialLoadX509ChainFromMemory certMemory chainCertsMemory keyMemory
return $ TLS.Credentials [cred]

makeTlsSocketStream :: TLSSettings -> S.Socket -> IO Stream
makeTlsSocketStream stts socket = do
creds <- loadCredentials stts
mgr <- getSessionManager stts
ctx <- TLS.contextNew socket (params mgr creds)
TLS.contextHookSetLogging ctx (tlsLogging stts)
TLS.handshake ctx
makeStream (receive ctx) (send ctx) <&>
\s -> s { streamTlsContext = Just ctx }
where
receive ctx = handle onEOF go
where
onEOF e
| Just TLS.Error_EOF <- fromException e = pure Nothing
| Just ioe <- fromException e, isEOFError ioe = pure Nothing
| otherwise = throwIO e
go = do
x <- TLS.recvData ctx
if B.null x then
go
else
pure $ Just x

send _ Nothing = return ()
send ctx (Just bs) =
TLS.sendData ctx bs

params mgr creds = def { -- TLS.ServerParams
TLS.serverWantClientCert = tlsWantClientCert stts
, TLS.serverCACertificates = []
, TLS.serverDHEParams = tlsServerDHEParams stts
, TLS.serverHooks = hooks
, TLS.serverShared = shared mgr creds
, TLS.serverSupported = supported
, TLS.serverEarlyDataSize = 2018
}
-- Adding alpn to user's tlsServerHooks.
hooks = (tlsServerHooks stts)
{ TLS.onALPNClientSuggest = TLS.onALPNClientSuggest (tlsServerHooks stts)
-- <|> (if settingsHTTP2Enabled set then Just alpn else Nothing)
}

shared mgr creds = def {
TLS.sharedCredentials = creds
, TLS.sharedSessionManager = mgr
}
supported = def { -- TLS.Supported
TLS.supportedVersions = tlsAllowedVersions stts
, TLS.supportedCiphers = tlsCiphers stts
, TLS.supportedCompressions = [TLS.nullCompression]
, TLS.supportedSecureRenegotiation = True
, TLS.supportedClientInitiatedRenegotiation = False
, TLS.supportedSession = True
, TLS.supportedFallbackScsv = True
, TLS.supportedGroups = [TLS.X25519,TLS.P256,TLS.P384]
}

getSessionManager :: TLSSettings -> IO TLS.SessionManager
getSessionManager TLSSettings{ tlsSessionManager = Just mgr } = return mgr
getSessionManager stts' = case tlsSessionManagerConfig stts' of
Nothing -> return TLS.noSessionManager
Just config -> SM.newSessionManager config


--------------------------------------------------------------------------------
makeEchoStream :: IO Stream
Expand Down
Loading
Loading