From 2a120dfe57ee84894be3ad98fc014f9b68084922 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 28 Sep 2024 23:15:17 +0100 Subject: [PATCH] smp server: Allow serving HTTPS and transport on the same port (v2) (#1327) * smp-server: Allow serving HTTPS and transport on the same port * update rfc * servers: refactor TLS credentials * provide server credentials in SNI hook * determine TLS server params dynamically, when starting the server * remove alpn from TransportServerConfig to decide it dynamically where server is started * diff * combine HTTP and SMP on the shared port * Update to SockAddr * Fix params and web.https parser * Switch fork urls * WIP: add smpServerTestStatic test * Update warp-tls repo * shared connection tests * cleanup * Add protocol tests * rename cert file, enable both ports and web by default * terminate with message on missing credentials * test cert file * client option to use port 443 as default SMP port * use SNI in non-SMP clients * supported * remove TODO * advice * fix test build * Add RSA-4096 check for web creds, fix test * Remove directory listing from static app * message * messages * update log tests --------- Co-authored-by: IC Rainbow --- apps/smp-server/Main.hs | 2 +- apps/smp-server/web/Static.hs | 49 +++++- cabal.project | 14 ++ package.yaml | 21 ++- rfcs/2024-09-15-shared-port.md | 163 ++++++++++++++++++ simplexmq.cabal | 16 +- src/Simplex/FileTransfer/Client.hs | 2 +- src/Simplex/Messaging/Agent/Env/SQLite.hs | 7 +- src/Simplex/Messaging/Client.hs | 36 ++-- src/Simplex/Messaging/Client/Agent.hs | 2 +- src/Simplex/Messaging/Notifications/Client.hs | 7 +- src/Simplex/Messaging/Notifications/Server.hs | 6 +- .../Messaging/Notifications/Server/Env.hs | 4 +- src/Simplex/Messaging/Server.hs | 45 +++-- src/Simplex/Messaging/Server/CLI.hs | 18 +- src/Simplex/Messaging/Server/Env/STM.hs | 40 ++++- src/Simplex/Messaging/Server/Main.hs | 73 ++++---- src/Simplex/Messaging/Transport.hs | 29 +++- src/Simplex/Messaging/Transport/Client.hs | 18 +- .../Messaging/Transport/HTTP2/Client.hs | 3 +- src/Simplex/Messaging/Transport/Server.hs | 27 +-- tests/AgentTests/FunctionalAPITests.hs | 2 +- tests/AgentTests/NotificationTests.hs | 2 +- tests/CLITests.hs | 100 ++++++++++- tests/CoreTests/SOCKSSettings.hs | 2 +- tests/NtfClient.hs | 4 +- tests/SMPClient.hs | 3 +- tests/fixtures/web.crt | 34 ++++ tests/fixtures/web.key | 52 ++++++ 29 files changed, 655 insertions(+), 126 deletions(-) create mode 100644 rfcs/2024-09-15-shared-port.md create mode 100644 tests/fixtures/web.crt create mode 100644 tests/fixtures/web.key diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index d0de4b4f1..9a9e094ea 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -19,4 +19,4 @@ main = do setLogLevel LogDebug cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath - withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath + withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles Static.attachStaticFiles cfgPath logPath diff --git a/apps/smp-server/web/Static.hs b/apps/smp-server/web/Static.hs index 4d25f2067..266ad663e 100644 --- a/apps/smp-server/web/Static.hs +++ b/apps/smp-server/web/Static.hs @@ -8,13 +8,18 @@ import Control.Logger.Simple import Control.Monad import Data.ByteString (ByteString) import qualified Data.ByteString as B +import Data.IORef (readIORef) import Data.Maybe (fromMaybe) import Data.String (fromString) import Data.Text.Encoding (encodeUtf8) -import Network.Wai.Application.Static as S -import Network.Wai.Handler.Warp as W -import qualified Network.Wai.Handler.WarpTLS as W +import Network.Socket (getPeerName) +import Network.Wai (Application) +import qualified Network.Wai.Application.Static as S +import qualified Network.Wai.Handler.Warp as W +import qualified Network.Wai.Handler.Warp.Internal as WI +import qualified Network.Wai.Handler.WarpTLS as WT import Simplex.Messaging.Encoding.String (strEncode) +import Simplex.Messaging.Server (AttachHTTP) import Simplex.Messaging.Server.Information import Simplex.Messaging.Server.Main (EmbeddedWebParams (..), WebHttpsParams (..)) import Simplex.Messaging.Transport.Client (TransportHost (..)) @@ -23,6 +28,7 @@ import Static.Embedded as E import System.Directory (createDirectoryIfMissing) import System.FilePath import UnliftIO.Concurrent (forkFinally) +import UnliftIO.Exception (bracket, finally) serveStaticFiles :: EmbeddedWebParams -> IO () serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do @@ -31,9 +37,42 @@ serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} W.runSettings (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath) forM_ webHttpsParams $ \WebHttpsParams {port, cert, key} -> flip forkFinally (\e -> logError $ "HTTPS server crashed: " <> tshow e) $ do logInfo $ "Serving static site on port " <> tshow port <> " (TLS)" - W.runTLS (W.tlsSettings cert key) (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath) + WT.runTLS (WT.tlsSettings cert key) (mkSettings port) app where - mkSettings port = setPort port defaultSettings + app = staticFiles webStaticPath + mkSettings port = W.setPort port W.defaultSettings + +-- | Prepare context and prepare HTTP handler for TLS connections that already passed TLS.handshake and ALPN check. +attachStaticFiles :: FilePath -> (AttachHTTP -> IO ()) -> IO () +attachStaticFiles path action = + -- Initialize global internal state for http server. + WI.withII settings $ \ii -> do + action $ \socket cxt -> do + -- Initialize internal per-connection resources. + addr <- getPeerName socket + withConnection addr cxt $ \(conn, transport) -> + withTimeout ii conn $ \th -> + -- Run Warp connection handler to process HTTP requests for static files. + WI.serveConnection conn ii th addr transport settings app + where + app = staticFiles path + settings = W.defaultSettings + -- from warp-tls + withConnection socket cxt = bracket (WT.attachConn socket cxt) (terminate . fst) + -- from warp + withTimeout ii conn = + bracket + (WI.registerKillThread (WI.timeoutManager ii) (WI.connClose conn)) + WI.cancel + -- shared clean up + terminate conn = WI.connClose conn `finally` (readIORef (WI.connWriteBuffer conn) >>= WI.bufFree) + +staticFiles :: FilePath -> Application +staticFiles root = S.staticApp settings + where + settings = (S.defaultFileServerSettings root) + { S.ssListing = Nothing + } generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO () generateSite si onionHost sitePath = do diff --git a/cabal.project b/cabal.project index 43afe30ea..1a4745ffc 100644 --- a/cabal.project +++ b/cabal.project @@ -28,3 +28,17 @@ source-repository-package type: git location: https://github.com/simplex-chat/sqlcipher-simple.git tag: a46bd361a19376c5211f1058908fc0ae6bf42446 + +-- waiting for published warp-tls-3.4.7 +source-repository-package + type: git + location: https://github.com/yesodweb/wai.git + tag: ec5e017d896a78e787a5acea62b37a4e677dec2e + subdir: warp-tls + +-- backported fork due http-5.0 +source-repository-package + type: git + location: https://github.com/simplex-chat/wai.git + tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0 + subdir: warp diff --git a/package.yaml b/package.yaml index 8699dce63..ce1c8efd4 100644 --- a/package.yaml +++ b/package.yaml @@ -123,9 +123,10 @@ executables: dependencies: - file-embed - simplexmq + - wai - wai-app-static - - warp - - warp-tls + - warp ==3.3.30 # the last one before http2-5.0 + - warp-tls ==3.4.7 # extra internals exposed ghc-options: - -threaded - -rtsopts @@ -159,19 +160,29 @@ executables: tests: simplexmq-test: - source-dirs: tests + source-dirs: + - tests + - apps/smp-server/web main: Test.hs dependencies: - - simplexmq - deepseq == 1.4.* + - file-embed - generic-random == 1.5.* - hspec == 2.11.* - hspec-core == 2.11.* + - http-client + - http-client-tls - HUnit == 1.6.* + - main-tester == 0.2.* - QuickCheck == 2.14.* - silently == 1.2.* - - main-tester == 0.2.* + - simplexmq - timeit == 2.0.* + - unordered-containers + - wai + - wai-app-static + - warp + - warp-tls ghc-options: - -threaded - -rtsopts diff --git a/rfcs/2024-09-15-shared-port.md b/rfcs/2024-09-15-shared-port.md new file mode 100644 index 000000000..67f55b3ca --- /dev/null +++ b/rfcs/2024-09-15-shared-port.md @@ -0,0 +1,163 @@ +# Sharing protocol ports with HTTPS + +Some networks block all ports other than web ports, including port 5223 used for SMP protocol by default. Running SMP servers on a common web port 443 would allow them to work on more networks. The servers would need to provide an HTTPS page for browsers (and probes). + +## Problem + +Browsers and tools rely on system CA bundles instead of certificate pinning. +The crypto parameters used by HTTPS are different from what the protocols use. +Public certificate providers like LetsEncrypt can only sign specific types of keys and Ed25519 isn't one of them. + +This means a server should distinguish browser and protocol clients and adjust its behavior to match. + +## Solution + +`tls` package has a server hook that allows producing a different set of `TLS.Credentials` according to a client-provided "Server Name Indication" extension. + +Since LE certificates are only handed out to domain names, TLS client will be sending the SNI. +However client transports are constructed over connected sockets and the SNI wouldn't be present unless explicitly requested. +When a client sends SNI, then it's a browser and a web credentials should be used. +Otherwise it's a protocol client to be offered the self-signed ca, cert and key. + +When a transport colocated with a HTTPS, its ALPN list should be extended with `h2 http/1.1`. +The browsers will send it, and it should be checked before running transport client. +If HTTP ALPN is detected, then the client connection is served with HTTP `Application` instead (the same "server information" page). + +If some client connects to server IP, doesn't send SNI and doesn't send ALPN, it will look like a pre-handshake client. +In that case a server will send its handshake first. +This can be mitigated by delaying its handshake and letting the probe to issue its HTTP request. + +## Implementation plan + +An unmodified client should be able to use protocols on port 443 right away. + +The switchover happens inside `runTransportServerState` before `runClient`: + +```haskell +runServer (tcpPort, ATransport t) = do + -- ... + runTransportServerState_ ss started tcpPort serverParams tCfg $ \socket h -> do -- expose raw socket for warp-tls internals to attach + negotiated <- getSessionALPN + if allowHTTP t && isHTTP negotiated -- only attempt the switch for the TLS transport + then runHTTP socket (tlsContext h)-- ... collect data and produce values needed to run WAI Application + else runClient serverSignKey t h `runReaderT` env -- performs serverHandshake etc as usual +``` + +The web app and server live outside, so `runHttp` has to be provided by the `runSMPServer` caller. +Additonally, Warp is using its `InternalInfo` object that's scoped to `withII` bracket. + +```haskell + runServer ini = do + -- ... + + runWebServer ini ServerInformation {config, information} $ if sharedHttps then Nothing else webHttpsParams -- suppress serving https + if sharedHttps + then withRunHTTP staticFilesPath \attachStatic -> runSMPServer cfg (Just attachStatic) -- provide wrapped application runner + else runSMPServer cfg Nothing +``` + +### Upstream + +The implementation relies on a few modification to upstream code: +- `warp-tls`: The library provides `httpOverTls`, but it wants to do handshake itself. + Since we have to do the handshake to switch on ALPN, the setup function has to be split. + This is a resonable change that may be upstreamed and nothing blocks us from using the recent version. +- `warp`: Only the re-export of `serveConnection` is needed. + Unfortunately the most recent `warp` version can't be used right away due to dependency cascade around `http-5` and `auto-update-2`. + So a fork containing the backported re-export has to be used until the dependencies are refreshed. + + +### TLS.ServerParams + +When a server has port sharing enabled, a new set of TLS params is loaded and combined with transport params: + +```haskell +newEnv config = do + -- ... + tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) + sharedServerParams <- forM ((,) <$> sharedHttpsCredentials config <*> alpn transportConfig) $ \((chain, key), alpn) -> + let ca = Nothing -- It is possible to provide CA certificate, but it is typical for web server to use combined certificate chains + loadHTTPSServerParams tlsServerParams ca chain key alpn +``` + +`loadHTTPSServerParams` extends params with: +1. `onALPNClientSuggest` hook gets `["h2", "http/1.1"]` added to the ALPN list which is now required. +2. `onServerNameIndication` hook added, which upon detecting client SNI prepends the web credentials. +3. `sharedCredentials = T.Credentials []` should be done to prevent transport credentials confusing browsers. + But that aborts key exchange somewhere in tls internals, so disabled for now. + As a workaround, another set of dummy credentials can be provided in the hope that any sane browser would reject them. + Like, RC4 ciphers, "impossible" digest combination, etc. + +### supportedParameters + +TLS certificate chains provided by LetsEncrypt use ECDSA/P256 and that requires extending `supportedParameters` with things disabled in transports: + +```haskell + browserCiphers = + [ TE.cipher_TLS13_AES128CCM8_SHA256 + , TE.cipher_ECDHE_ECDSA_AES128CCM8_SHA256 + , TE.cipher_ECDHE_ECDSA_AES256CCM8_SHA256 + ] + browserGroups = + [ T.P256 + ] + browserSigs = + [ (T.HashSHA256, T.SignatureECDSA), + (T.HashSHA384, T.SignatureECDSA) + ] +``` + +This may not be enough for other certificate providers. + +## Configuration + +> XXX: This is for the current implementation and should be updated. + +Web certificate chain is picked up from the WEB section: + +```ini +[TRANSPORT] +port: 443 + +[WEB] +https: 443 +cert: /etc/opt/simplex/web.cert +key: /etc/opt/simplex/web.key + +# Alternatively, with a proper access configuration, the paths can point to the LE creds directly: +# cert: /etc/letsencrypt/live/smp.hostname.tld/fullchain.pem +# key: /etc/letsencrypt/live/smp.hostname.tld/privkey.pem +``` + +When `TRANSPORT.port` matches `WEB.https` the transport server becomes shared. + +Perhaps a more desirable option would be explicit configuration resulting in additional transported to run: + +```ini +[TRANSPORT] +port: 5223 ; pure protocol transport +# control_port: 5224 +shared_port: 443 ; variant 1: register in TRANSPORT + +[WEB] +https: 443 +cert: /etc/opt/simplex/web.cert +key: /etc/opt/simplex/web.key +# transport: on ; variant 2: +``` + +## Caveats + +Serving static files and the protocols togother may pose a problem for those who currently use dedicated web servers as they should switch to embedded http handlers. + +As before, using embedded HTTP server is increasing attack surface. + +Users who want to run everything on a single host will have to add and extra IP address and bind servers to specific IPs instead of 0.0.0.0. +An amalgamated server binary can be provided that would contain both SMP and XFTP servers, where transport will dispatch connections by handshake ALPN. + +## Alternative: Use transports routable with reverse-proxies + +An "industrial" reverse proxy may do the ALPN routing, serving HTTP by itself and delegating `smp` and `xftp` to protocol servers. +Same with the `websockets`. + +Since this in effect does TLS termination, the protocol servers will have to rely on credentials from protocol handshakes. diff --git a/simplexmq.cabal b/simplexmq.cabal index 1ab3e4fe8..5590dd6a4 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -419,9 +419,10 @@ executable smp-server , transformers ==0.6.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , wai , wai-app-static - , warp - , warp-tls + , warp ==3.3.30 + , warp-tls ==3.4.7 , websockets ==0.12.* , yaml ==0.11.* , zstd ==0.1.3.* @@ -627,9 +628,12 @@ test-suite simplexmq-test XFTPCLI XFTPClient XFTPServerTests + Static + Static.Embedded Paths_simplexmq hs-source-dirs: tests + apps/smp-server/web default-extensions: StrictData ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1 @@ -657,12 +661,15 @@ test-suite simplexmq-test , deepseq ==1.4.* , direct-sqlcipher ==2.3.* , directory ==1.3.* + , file-embed , filepath ==1.4.* , generic-random ==1.5.* , hashable ==1.4.* , hourglass ==0.2.* , hspec ==2.11.* , hspec-core ==2.11.* + , http-client + , http-client-tls , http-types ==0.12.* , http2 >=4.2.2 && <4.3 , ini ==0.4.1 @@ -692,6 +699,11 @@ test-suite simplexmq-test , transformers ==0.6.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , unordered-containers + , wai + , wai-app-static + , warp + , warp-tls , websockets ==0.12.* , yaml ==0.11.* , zstd ==0.1.3.* diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index f2240ec50..22b111a26 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -104,7 +104,7 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, let socksCreds = clientSocksCredentials xftpNetworkConfig proxySessTs transportSession ProtocolServer _ host port keyHash = srv useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host - let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN} + let tcConfig = (transportClientConfig xftpNetworkConfig useHost True) {alpn = clientALPN} http2Config = xftpHTTP2Config tcConfig config clientVar <- newTVarIO Nothing let usePort = if null port then "443" else port diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index e54ee64b4..99a241d2e 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -74,8 +74,7 @@ import Simplex.Messaging.Parsers (defaultJSON) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth, ProtocolServer, ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..)) -import Simplex.Messaging.Transport.Client (defaultSMPPort) +import Simplex.Messaging.Transport (SMPVersion) import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors') import System.Mem.Weak (Weak) import System.Random (StdGen, newStdGen) @@ -195,8 +194,8 @@ defaultAgentConfig = sndAuthAlg = C.AuthAlg C.SEd25519, -- TODO replace with X25519 when switching to v7 connIdBytes = 12, tbqSize = 128, - smpCfg = defaultSMPClientConfig {defaultTransport = (show defaultSMPPort, transport @TLS)}, - ntfCfg = defaultNTFClientConfig {defaultTransport = ("443", transport @TLS)}, + smpCfg = defaultSMPClientConfig, + ntfCfg = defaultNTFClientConfig, xftpCfg = defaultXFTPClientConfig, reconnectInterval = defaultReconnectInterval, messageRetryInterval = defaultMessageRetryInterval, diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index afcc5eab2..461bb9ee8 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -141,7 +141,7 @@ import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport -import Simplex.Messaging.Transport.Client (SocksAuth (..), SocksProxyWithAuth (..), TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient) +import Simplex.Messaging.Transport.Client (SocksAuth (..), SocksProxyWithAuth (..), TransportClientConfig (..), TransportHost (..), defaultSMPPort, defaultTcpConnectTimeout, runTransportClient) import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Transport.WebSockets (WS) import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM) @@ -281,6 +281,8 @@ data NetworkConfig = NetworkConfig smpProxyMode :: SMPProxyMode, -- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions smpProxyFallback :: SMPProxyFallback, + -- | use web port 443 for SMP protocol + smpWebPort :: Bool, -- | timeout for the initial client TCP/TLS connection (microseconds) tcpConnectTimeout :: Int, -- | timeout of protocol commands (microseconds) @@ -352,6 +354,7 @@ defaultNetworkConfig = sessionMode = TSMSession, smpProxyMode = SPMNever, smpProxyFallback = SPFAllow, + smpWebPort = False, tcpConnectTimeout = defaultTcpConnectTimeout, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 5_000, @@ -362,9 +365,9 @@ defaultNetworkConfig = logTLSErrors = False } -transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig -transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host = - TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} +transportClientConfig :: NetworkConfig -> TransportHost -> Bool -> TransportClientConfig +transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host useSNI = + TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing, useSNI} where socksProxy' = (\(SocksProxyWithAuth _ proxy) -> proxy) <$> socksProxy useSocksProxy SMAlways = socksProxy' @@ -400,24 +403,29 @@ data ProtocolClientConfig v = ProtocolClientConfig -- | client-server protocol version range serverVRange :: VersionRange v, -- | agree shared session secret (used in SMP proxy for additional encryption layer) - agreeSecret :: Bool + agreeSecret :: Bool, + -- | send SNI to server, False for SMP + useSNI :: Bool } -- | Default protocol client configuration. -defaultClientConfig :: Maybe [ALPN] -> VersionRange v -> ProtocolClientConfig v -defaultClientConfig clientALPN serverVRange = +defaultClientConfig :: Maybe [ALPN] -> Bool -> VersionRange v -> ProtocolClientConfig v +defaultClientConfig clientALPN useSNI serverVRange = ProtocolClientConfig { qSize = 64, defaultTransport = ("443", transport @TLS), networkConfig = defaultNetworkConfig, clientALPN, serverVRange, - agreeSecret = False + agreeSecret = False, + useSNI } {-# INLINE defaultClientConfig #-} defaultSMPClientConfig :: ProtocolClientConfig SMPVersion -defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) supportedClientSMPRelayVRange +defaultSMPClientConfig = + (defaultClientConfig (Just supportedSMPHandshakes) False supportedClientSMPRelayVRange) + {defaultTransport = (show defaultSMPPort, transport @TLS)} {-# INLINE defaultSMPClientConfig #-} data Request err msg = Request @@ -477,14 +485,14 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe ByteString) -- A single queue can be used for multiple 'SMPClient' instances, -- as 'SMPServerTransmission' includes server information. getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) -getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ proxySessTs disconnected = do +getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret, useSNI} msgQ proxySessTs disconnected = do case chooseTransportHost networkConfig (host srv) of Right useHost -> (getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost) `catch` \(e :: IOException) -> pure . Left $ PCEIOError e Left e -> pure $ Left e where - NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig + NetworkConfig {smpWebPort, tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig mkProtocolClient :: TransportHost -> UTCTime -> IO (PClient v err msg) mkProtocolClient transportHost ts = do connected <- newTVarIO False @@ -515,7 +523,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) runClient (port', ATransport t) useHost c = do cVar <- newEmptyTMVarIO - let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN} + let tcConfig = (transportClientConfig networkConfig useHost useSNI) {alpn = clientALPN} socksCreds = clientSocksCredentials networkConfig proxySessTs transportSession tId <- runTransportClient tcConfig socksCreds useHost port' (Just $ keyHash srv) (client t c cVar) @@ -528,7 +536,9 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize useTransport :: (ServiceName, ATransport) useTransport = case port srv of - "" -> defaultTransport cfg + "" -> case protocolTypeI @(ProtoType msg) of + SPSMP | smpWebPort -> ("443", transport @TLS) + _ -> defaultTransport cfg "80" -> ("80", transport @WS) p -> (p, transport @TLS) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index f5d1a53cf..378b470a3 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -76,7 +76,7 @@ data SMPClientAgentConfig = SMPClientAgentConfig defaultSMPClientAgentConfig :: SMPClientAgentConfig defaultSMPClientAgentConfig = SMPClientAgentConfig - { smpCfg = defaultSMPClientConfig {defaultTransport = ("5223", transport @TLS)}, + { smpCfg = defaultSMPClientConfig, reconnectInterval = RetryInterval { initialInterval = second, diff --git a/src/Simplex/Messaging/Notifications/Client.hs b/src/Simplex/Messaging/Notifications/Client.hs index 72f0c15a8..f2c07741f 100644 --- a/src/Simplex/Messaging/Notifications/Client.hs +++ b/src/Simplex/Messaging/Notifications/Client.hs @@ -2,6 +2,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Messaging.Notifications.Client where @@ -13,13 +14,17 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes) import Simplex.Messaging.Protocol (ErrorType, pattern NoEntity) +import Simplex.Messaging.Transport (TLS, Transport (..)) type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse type NtfClientError = ProtocolClientError ErrorType defaultNTFClientConfig :: ProtocolClientConfig NTFVersion -defaultNTFClientConfig = defaultClientConfig (Just supportedNTFHandshakes) supportedClientNTFVRange +defaultNTFClientConfig = + (defaultClientConfig (Just supportedNTFHandshakes) True supportedClientNTFVRange) + {defaultTransport = ("443", transport @TLS)} +{-# INLINE defaultNTFClientConfig #-} ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519) ntfRegisterToken c pKey newTkn = diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 8b5f2274b..ab7c69f04 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -51,7 +51,7 @@ import Simplex.Messaging.Server import Simplex.Messaging.Server.Stats import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..), defaultSupportedParams) -import Simplex.Messaging.Transport.Server (runTransportServer) +import Simplex.Messaging.Transport.Server (AddHTTP, runTransportServer) import Simplex.Messaging.Util import System.Exit (exitFailure) import System.IO (BufferMode (..), hPutStrLn, hSetBuffering) @@ -80,8 +80,8 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do resubscribe s raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports <> serverStatsThread_ cfg) `finally` stopServer where - runServer :: (ServiceName, ATransport) -> M () - runServer (tcpPort, ATransport t) = do + runServer :: (ServiceName, ATransport, AddHTTP) -> M () + runServer (tcpPort, ATransport t, _addHTTP) = do srvCreds <- asks tlsServerCreds serverSignKey <- either fail pure $ fromTLSCredentials srvCreds env <- ask diff --git a/src/Simplex/Messaging/Notifications/Server/Env.hs b/src/Simplex/Messaging/Notifications/Server/Env.hs index e99adc884..c1e0981dc 100644 --- a/src/Simplex/Messaging/Notifications/Server/Env.hs +++ b/src/Simplex/Messaging/Notifications/Server/Env.hs @@ -33,13 +33,13 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..)) -import Simplex.Messaging.Transport.Server (ServerCredentials, TransportServerConfig, loadFingerprint, loadServerCredential) +import Simplex.Messaging.Transport.Server (AddHTTP, ServerCredentials, TransportServerConfig, loadFingerprint, loadServerCredential) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) import UnliftIO.STM data NtfServerConfig = NtfServerConfig - { transports :: [(ServiceName, ATransport)], + { transports :: [(ServiceName, ATransport, AddHTTP)], subIdBytes :: Int, regCodeBytes :: Int, clientQSize :: Natural, diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 5a5ae12fd..bf96b6deb 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -34,6 +34,7 @@ module Simplex.Messaging.Server verifyCmdAuthorization, dummyVerifyCmd, randomId, + AttachHTTP, ) where @@ -68,10 +69,12 @@ import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) import Data.Time.Clock.System (SystemTime (..), getSystemTime) import Data.Time.Format.ISO8601 (iso8601Show) import Data.Type.Equality +import Data.Typeable (cast) import GHC.IORef (atomicSwapIORef) import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) +import qualified Network.TLS as TLS import Numeric.Natural (Natural) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, forwardSMPTransmission, smpProxyError, temporaryClientError) @@ -115,22 +118,23 @@ import GHC.Conc.Sync (threadLabel) -- | Runs an SMP server using passed configuration. -- -- See a full server here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs -runSMPServer :: ServerConfig -> IO () -runSMPServer cfg = do +runSMPServer :: ServerConfig -> Maybe AttachHTTP -> IO () +runSMPServer cfg attachHTTP_ = do started <- newEmptyTMVarIO - runSMPServerBlocking started cfg + runSMPServerBlocking started cfg attachHTTP_ -- | Runs an SMP server using passed configuration with signalling. -- -- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True) -- and when it is disconnected from the TCP socket once the server thread is killed (False). -runSMPServerBlocking :: TMVar Bool -> ServerConfig -> IO () -runSMPServerBlocking started cfg = newEnv cfg >>= runReaderT (smpServer started cfg) +runSMPServerBlocking :: TMVar Bool -> ServerConfig -> Maybe AttachHTTP -> IO () +runSMPServerBlocking started cfg attachHTTP_ = newEnv cfg >>= runReaderT (smpServer started cfg attachHTTP_) type M a = ReaderT Env IO a +type AttachHTTP = Socket -> TLS.Context -> IO () -smpServer :: TMVar Bool -> ServerConfig -> M () -smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do +smpServer :: TMVar Bool -> ServerConfig -> Maybe AttachHTTP -> M () +smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} attachHTTP_ = do s <- asks server pa <- asks proxyAgent expired <- restoreServerMessages @@ -144,13 +148,26 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do ) `finally` withLock' (savingLock s) "final" (saveServer False >> closeServer) where - runServer :: (ServiceName, ATransport) -> M () - runServer (tcpPort, ATransport t) = do - srvCreds <- asks tlsServerCreds + runServer :: (ServiceName, ATransport, AddHTTP) -> M () + runServer (tcpPort, ATransport t, addHTTP) = do + smpCreds <- asks tlsServerCreds + httpCreds_ <- asks httpServerCreds ss <- asks sockets - serverSignKey <- either fail pure $ fromTLSCredentials srvCreds + serverSignKey <- either fail pure $ fromTLSCredentials smpCreds env <- ask - liftIO $ runTransportServerState ss started tcpPort defaultSupportedParams srvCreds (Just supportedSMPHandshakes) tCfg $ \h -> runClient serverSignKey t h `runReaderT` env + liftIO $ case (httpCreds_, attachHTTP_) of + (Just httpCreds, Just attachHTTP) | addHTTP -> + runTransportServerState_ ss started tcpPort defaultSupportedParamsHTTPS chooseCreds (Just combinedALPNs) tCfg $ \s h -> + case cast h of + Just TLS {tlsContext} | maybe False (`elem` httpALPN) (getSessionALPN h) -> labelMyThread "https client" >> attachHTTP s tlsContext + _ -> runClient serverSignKey t h `runReaderT` env + where + chooseCreds = maybe smpCreds (\_host -> httpCreds) + combinedALPNs = supportedSMPHandshakes <> httpALPN + httpALPN :: [ALPN] + httpALPN = ["h2", "http/1.1"] + _ -> + runTransportServerState ss started tcpPort defaultSupportedParams smpCreds (Just supportedSMPHandshakes) tCfg $ \h -> runClient serverSignKey t h `runReaderT` env fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey saveServer :: Bool -> M () @@ -803,7 +820,7 @@ send th c@Client {sndQ, msgQ, sessionId} = do -- replace MSG response with OK, accumulating MSG in a separate list. MSG {} -> ((CorrId "", entId, cmd) : msgs, (corrId, entId, OK)) _ -> (msgs, t) - + sendMsg :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO () sendMsg th c@Client {msgQ, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " sendMsg" @@ -1211,7 +1228,7 @@ client thParams' clnt@Client {clientId, subscriptions, ntfSubscriptions, rcvQ, s when (Just t /= updatedAt) $ do withLog $ \s -> logUpdateQueueTime s rId t st <- asks queueStore - liftIO $ updateQueueTime st rId t + liftIO $ updateQueueTime st rId t subscribeNotifications :: M (Transmission BrokerMsg) subscribeNotifications = do diff --git a/src/Simplex/Messaging/Server/CLI.hs b/src/Simplex/Messaging/Server/CLI.hs index f02612274..30e5a0b58 100644 --- a/src/Simplex/Messaging/Server/CLI.hs +++ b/src/Simplex/Messaging/Server/CLI.hs @@ -6,6 +6,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} @@ -29,7 +30,7 @@ import Options.Applicative import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI) import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..)) -import Simplex.Messaging.Transport.Server (loadFileFingerprint) +import Simplex.Messaging.Transport.Server (AddHTTP, loadFileFingerprint) import Simplex.Messaging.Transport.WebSockets (WS) import Simplex.Messaging.Util (eitherToMaybe, whenM) import System.Directory (doesDirectoryExist, listDirectory, removeDirectoryRecursive, removePathForcibly) @@ -275,7 +276,7 @@ checkSavedFingerprint cfgPath x509cfg = do where c = combine cfgPath . ($ x509cfg) -iniTransports :: Ini -> [(String, ATransport)] +iniTransports :: Ini -> [(ServiceName, ATransport, AddHTTP)] iniTransports ini = let smpPorts = ports $ strictIni "TRANSPORT" "port" ini ws = strictIni "TRANSPORT" "websockets" ini @@ -283,17 +284,22 @@ iniTransports ini = | ws == "off" = [] | ws == "on" = ["80"] | otherwise = ports ws \\ smpPorts - in map (,transport @TLS) smpPorts <> map (,transport @WS) wsPorts + in ts (transport @TLS) smpPorts <> ts (transport @WS) wsPorts where + ts :: ATransport -> [ServiceName] -> [(ServiceName, ATransport, AddHTTP)] + ts t = map (\port -> (port, t, webPort == Just port)) + webPort = T.unpack <$> eitherToMaybe (lookupValue "WEB" "https" ini) ports = map T.unpack . T.splitOn "," -printServerConfig :: [(ServiceName, ATransport)] -> Maybe FilePath -> IO () +printServerConfig :: [(ServiceName, ATransport, AddHTTP)] -> Maybe FilePath -> IO () printServerConfig transports logFile = do putStrLn $ case logFile of Just f -> "Store log: " <> f _ -> "Store log disabled." - forM_ transports $ \(p, ATransport t) -> - putStrLn $ "Listening on port " <> p <> " (" <> transportName t <> ")..." + forM_ transports $ \(p, ATransport t, addHTTP) -> do + let descr = p <> " (" <> transportName t <> ")..." + putStrLn $ "Serving SMP protocol on port " <> descr + when addHTTP $ putStrLn $ "Serving static site on port " <> descr deleteDirIfExists :: FilePath -> IO () deleteDirIfExists path = whenM (doesDirectoryExist path) $ removeDirectoryRecursive path diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index 214cecd19..05ed7da53 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -10,11 +11,13 @@ module Simplex.Messaging.Server.Env.STM where import Control.Concurrent (ThreadId) import Control.Logger.Simple import Control.Monad +import qualified Crypto.PubKey.RSA as RSA import Crypto.Random import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IM +import Data.List (intercalate) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -22,6 +25,7 @@ import Data.Maybe (isJust, isNothing) import qualified Data.Text as T import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) +import qualified Data.X509 as X import Data.X509.Validation (Fingerprint (..)) import Network.Socket (ServiceName) import qualified Network.TLS as T @@ -41,13 +45,15 @@ import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP) -import Simplex.Messaging.Transport.Server (ServerCredentials, SocketState, TransportServerConfig, loadFingerprint, loadServerCredential, newSocketState) +import Simplex.Messaging.Transport.Server +import System.Directory (doesFileExist) +import System.Exit (exitFailure) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) import UnliftIO.STM data ServerConfig = ServerConfig - { transports :: [(ServiceName, ATransport)], + { transports :: [(ServiceName, ATransport, AddHTTP)], smpHandshakeTimeout :: Int, tbqSize :: Natural, msgQueueQuota :: Int, @@ -79,6 +85,7 @@ data ServerConfig = ServerConfig -- | interval between sending pending END events to unsubscribed clients, seconds pendingENDInterval :: Int, smpCredentials :: ServerCredentials, + httpCredentials :: Maybe ServerCredentials, -- | SMP client-server protocol version range smpServerVRange :: VersionRangeSMP, -- | TCP transport config @@ -123,6 +130,7 @@ data Env = Env random :: TVar ChaChaDRG, storeLog :: Maybe (StoreLog 'WriteMode), tlsServerCreds :: T.Credential, + httpServerCreds :: Maybe T.Credential, serverStats :: ServerStats, sockets :: SocketState, clientSeq :: TVar ClientId, @@ -217,7 +225,7 @@ newProhibitedSub = do return Sub {subThread = ProhibitSub, delivered} newEnv :: ServerConfig -> IO Env -newEnv config@ServerConfig {smpCredentials, storeLogFile, smpAgentCfg, information, messageExpiration} = do +newEnv config@ServerConfig {smpCredentials, httpCredentials, storeLogFile, smpAgentCfg, information, messageExpiration} = do server <- newServer queueStore <- newQueueStore msgStore <- newMsgStore @@ -226,7 +234,9 @@ newEnv config@ServerConfig {smpCredentials, storeLogFile, smpAgentCfg, informati forM storeLogFile $ \f -> do logInfo $ "restoring queues from file " <> T.pack f restoreQueues queueStore f - tlsServerCreds <- loadServerCredential smpCredentials + tlsServerCreds <- getCredentials "SMP" smpCredentials + httpServerCreds <- mapM (getCredentials "HTTPS") httpCredentials + mapM_ checkHTTPSCredentials httpServerCreds Fingerprint fp <- loadFingerprint smpCredentials let serverIdentity = KeyHash fp serverStats <- newServerStats =<< getCurrentTime @@ -234,8 +244,28 @@ newEnv config@ServerConfig {smpCredentials, storeLogFile, smpAgentCfg, informati clientSeq <- newTVarIO 0 clients <- newTVarIO mempty proxyAgent <- newSMPProxyAgent smpAgentCfg random - pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerCreds, serverStats, sockets, clientSeq, clients, proxyAgent} + pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerCreds, httpServerCreds, serverStats, sockets, clientSeq, clients, proxyAgent} where + getCredentials protocol creds = do + files <- missingCreds + unless (null files) $ do + putStrLn $ "Error: no " <> protocol <> " credentials: " <> intercalate ", " files + when (protocol == "HTTPS") $ putStrLn letsEncrypt + exitFailure + loadServerCredential creds + where + missingfile f = (\y -> [f | not y]) <$> doesFileExist f + missingCreds = do + let files = maybe id (:) (caCertificateFile creds) [certificateFile creds, privateKeyFile creds] + in concat <$> mapM missingfile files + checkHTTPSCredentials (X.CertificateChain cc, _k) = + -- LetsEncrypt provides ECDSA with insecure curve p256 (https://safecurves.cr.yp.to) + case map (X.signedObject . X.getSigned) cc of + X.Certificate {X.certPubKey = X.PubKeyRSA rsa} : _ca | RSA.public_size rsa >= 512 -> pure () + _ -> do + putStrLn $ "Error: unsupported HTTPS credentials, required 4096-bit RSA\n" <> letsEncrypt + exitFailure + letsEncrypt = "Use Let's Encrypt to generate: certbot certonly --standalone -d yourdomainname --key-type rsa --rsa-key-size 4096" restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do (qs, s) <- readWriteStoreLog f diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 6cee2abd1..bcd96c170 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -36,7 +36,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer) -import Simplex.Messaging.Server (runSMPServer) +import Simplex.Messaging.Server (AttachHTTP, runSMPServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration, defaultProxyClientConcurrency) import Simplex.Messaging.Server.Expiration @@ -52,10 +52,16 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) import Text.Read (readMaybe) smpServerCLI :: FilePath -> FilePath -> IO () -smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ()) +smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ()) (\_ -> error "attachStaticFiles not available") -smpServerCLI_ :: (ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) -> (EmbeddedWebParams -> IO ()) -> FilePath -> FilePath -> IO () -smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = +smpServerCLI_ :: + (ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) -> + (EmbeddedWebParams -> IO ()) -> + (FilePath -> (AttachHTTP -> IO ()) -> IO ()) -> + FilePath -> + FilePath -> + IO () +smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath = getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case Init opts -> doesFileExist iniFile >>= \case @@ -77,10 +83,10 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = where iniFile = combine cfgPath "smp-server.ini" serverVersion = "SMP server v" <> simplexMQVersion - defaultServerPort = "5223" + defaultServerPorts = "5223,443" executableName = "smp-server" storeLogFilePath = combine logPath "smp-server-store.log" - httpsCertFile = combine cfgPath "web.cert" + httpsCertFile = combine cfgPath "web.crt" httpsKeyFile = combine cfgPath "web.key" defaultStaticPath = combine logPath "www" initializeServer opts@InitOptions {ip, fqdn, sourceCode = src', webStaticPath = sp', disableWeb = noWeb', scripted} @@ -96,7 +102,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine sourceCode' <- withPrompt ("Enter server source code URI (" <> maybe simplexmqSource T.unpack src' <> "): ") getServerSourceCode staticPath' <- withPrompt ("Enter path to store generated static site with server information (" <> fromMaybe defaultStaticPath sp' <> "): ") getLine - enableWeb <- onOffPrompt "Enable built-in web server for static site" (not noWeb') initialize opts { enableStoreLog, @@ -105,7 +110,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = password, sourceCode = (T.pack <$> sourceCode') <|> src', webStaticPath = if null staticPath' then sp' else Just staticPath', - disableWeb = not enableWeb + disableWeb = noWeb' } where serverPassword = @@ -172,7 +177,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = \# Host is only used to print server address on start.\n\ \# You can specify multiple server ports.\n" <> ("host: " <> T.pack host <> "\n") - <> ("port: " <> T.pack defaultServerPort <> "\n") + <> ("port: " <> T.pack defaultServerPorts <> "\n") <> "log_tls_errors: off\n\n\ \# Use `websockets: 443` to run websockets server in addition to plain TLS.\n\ \websockets: off\n\ @@ -205,19 +210,21 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = <> "# Run an embedded server on this port\n\ \# Onion sites can use any port and register it in the hidden service config.\n\ \# Running on a port 80 may require setting process capabilities.\n" - <> ((if disableWeb then "# " else "") <> "http: 8000\n\n") + <> (webDisabled <> "http: 8000\n\n") <> "# You can run an embedded TLS web server too if you provide port and cert and key files.\n\ - \# Not required for running relay on onion address.\n\ - \# https: 443\n" - <> ("# cert: " <> T.pack httpsCertFile <> "\n") - <> ("# key: " <> T.pack httpsKeyFile <> "\n") + \# Not required for running relay on onion address.\n" + <> (webDisabled <> "https: 443\n") + <> (webDisabled <> "cert: " <> T.pack httpsCertFile <> "\n") + <> (webDisabled <> "key: " <> T.pack httpsKeyFile <> "\n") + where + webDisabled = if disableWeb then "# " else "" runServer ini = do hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering fp <- checkSavedFingerprint cfgPath defaultX509Config let host = either (const "") T.unpack $ lookupValue "TRANSPORT" "host" ini port = T.unpack $ strictIni "TRANSPORT" "port" ini - cfg@ServerConfig {information, transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig + cfg@ServerConfig {information, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig sourceCode' = (\ServerPublicInfo {sourceCode} -> sourceCode) <$> information srv = ProtoServerWithAuth (SMPServer [THDomainName host] (if port == "5223" then "" else port) (C.KeyHash fp)) newQueueBasicAuth printServiceInfo serverVersion srv @@ -247,15 +254,25 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = newQueuesAllowed = allowNewQueues cfg, basicAuthEnabled = isJust newQueueBasicAuth } - runWebServer ini ServerInformation {config, information} - runSMPServer cfg + case webStaticPath' of + Just path | sharedHTTP -> do + runWebServer path Nothing ServerInformation {config, information} + attachStaticFiles path $ \attachHTTP -> runSMPServer cfg $ Just attachHTTP + Just path -> do + runWebServer path webHttpsParams' ServerInformation {config, information} + runSMPServer cfg Nothing + Nothing -> do + logWarn "No server static path set" + runSMPServer cfg Nothing where enableStoreLog = settingIsOn "STORE_LOG" "enable" ini logStats = settingIsOn "STORE_LOG" "log_stats" ini c = combine cfgPath . ($ defaultX509Config) + transports = iniTransports ini + sharedHTTP = any (\(_, _, addHTTP) -> addHTTP) transports serverConfig = ServerConfig - { transports = iniTransports ini, + { transports, smpHandshakeTimeout = 120000000, tbqSize = 128, msgQueueQuota = 128, @@ -267,6 +284,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = privateKeyFile = c serverKeyFile, certificateFile = c serverCrtFile }, + httpCredentials = (\WebHttpsParams {key, cert} -> ServerCredentials {caCertificateFile = Nothing, privateKeyFile = key, certificateFile = cert}) <$> webHttpsParams', storeLogFile = enableStoreLog $> storeLogFilePath, storeMsgsFile = let messagesPath = combine logPath "smp-server-messages.log" @@ -325,26 +343,23 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = } textToOwnServers :: Text -> [ByteString] textToOwnServers = map encodeUtf8 . T.words - - runWebServer ini si = - case eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini of - Nothing -> logWarn "No server static path set" - Just webStaticPath -> do + runWebServer webStaticPath webHttpsParams si = do let onionHost = either (const Nothing) (find isOnion) $ strDecode @(L.NonEmpty TransportHost) . encodeUtf8 =<< lookupValue "TRANSPORT" "host" ini webHttpPort = eitherToMaybe $ read . T.unpack <$> lookupValue "WEB" "http" ini - webHttpsParams = - eitherToMaybe $ do - port <- read . T.unpack <$> lookupValue "WEB" "https" ini - cert <- T.unpack <$> lookupValue "WEB" "cert" ini - key <- T.unpack <$> lookupValue "WEB" "key" ini - pure WebHttpsParams {port, cert, key} generateSite si onionHost webStaticPath when (isJust webHttpPort || isJust webHttpsParams) $ serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} where isOnion = \case THOnionHost _ -> True; _ -> False + webHttpsParams' = + eitherToMaybe $ do + port <- read . T.unpack <$> lookupValue "WEB" "https" ini + cert <- T.unpack <$> lookupValue "WEB" "cert" ini + key <- T.unpack <$> lookupValue "WEB" "key" ini + pure WebHttpsParams {port, cert, key} + webStaticPath' = eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini data EmbeddedWebParams = EmbeddedWebParams { webStaticPath :: FilePath, diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 0a005f046..da00fe231 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -66,6 +66,7 @@ module Simplex.Messaging.Transport connectTLS, closeTLS, defaultSupportedParams, + defaultSupportedParamsHTTPS, withTlsUnique, -- * SMP transport @@ -100,6 +101,7 @@ import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Default (def) import Data.Functor (($>)) +import Data.Typeable (Typeable) import Data.Version (showVersion) import Data.Word (Word16) import qualified Data.X509 as X @@ -214,7 +216,7 @@ data TransportConfig = TransportConfig transportTimeout :: Maybe Int } -class Transport c where +class Typeable c => Transport c where transport :: ATransport transport = ATransport (TProxy @c) @@ -321,8 +323,29 @@ defaultSupportedParams = TE.cipher_ECDHE_ECDSA_CHACHA20POLY1305_SHA256 -- for TLS12 ], T.supportedHashSignatures = [(T.HashIntrinsic, T.SignatureEd448), (T.HashIntrinsic, T.SignatureEd25519)], - T.supportedSecureRenegotiation = False, - T.supportedGroups = [T.X448, T.X25519] + T.supportedGroups = [T.X448, T.X25519], + T.supportedSecureRenegotiation = False + } + +-- | A selection of extra parameters to accomodate browser chains +defaultSupportedParamsHTTPS :: T.Supported +defaultSupportedParamsHTTPS = + defaultSupportedParams + { T.supportedCiphers = TE.ciphersuite_strong, + T.supportedGroups = [T.X25519, T.X448, T.FFDHE4096, T.FFDHE6144, T.FFDHE8192, T.P521], + T.supportedHashSignatures = + [ (T.HashIntrinsic, T.SignatureEd448), + (T.HashIntrinsic, T.SignatureEd25519), + (T.HashSHA256, T.SignatureECDSA), + (T.HashSHA384, T.SignatureECDSA), + (T.HashSHA512, T.SignatureECDSA), + (T.HashIntrinsic, T.SignatureRSApssRSAeSHA512), + (T.HashIntrinsic, T.SignatureRSApssRSAeSHA384), + (T.HashIntrinsic, T.SignatureRSApssRSAeSHA256), + (T.HashSHA512, T.SignatureRSA), + (T.HashSHA384, T.SignatureRSA), + (T.HashSHA256, T.SignatureRSA) + ] } instance Transport TLS where diff --git a/src/Simplex/Messaging/Transport/Client.hs b/src/Simplex/Messaging/Transport/Client.hs index 3b80b7f73..074a85dad 100644 --- a/src/Simplex/Messaging/Transport/Client.hs +++ b/src/Simplex/Messaging/Transport/Client.hs @@ -125,7 +125,8 @@ data TransportClientConfig = TransportClientConfig tcpKeepAlive :: Maybe KeepAliveOpts, logTLSErrors :: Bool, clientCredentials :: Maybe (X.CertificateChain, T.PrivKey), - alpn :: Maybe [ALPN] + alpn :: Maybe [ALPN], + useSNI :: Bool } deriving (Eq, Show) @@ -134,7 +135,7 @@ defaultTcpConnectTimeout :: Int defaultTcpConnectTimeout = 25_000_000 defaultTransportClientConfig :: TransportClientConfig -defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing +defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing True clientTransportConfig :: TransportClientConfig -> TransportConfig clientTransportConfig TransportClientConfig {logTLSErrors} = @@ -145,10 +146,10 @@ runTransportClient :: Transport c => TransportClientConfig -> Maybe SocksCredent runTransportClient = runTLSTransportClient defaultSupportedParams Nothing runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a -runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} socksCreds host port keyHash client = do +runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn, useSNI} socksCreds host port keyHash client = do serverCert <- newEmptyTMVarIO let hostName = B.unpack $ strEncode host - clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert + clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn useSNI serverCert connectTCP = case socksProxy of Just proxy -> connectSocksClient proxy socksCreds (hostAddr host) _ -> connectTCPClient hostName @@ -238,7 +239,7 @@ instance StrEncoding SocksProxy where socksAddr port = \case THIPv4 addr -> pure $ SockAddrInet port $ tupleToHostAddress addr THIPv6 addr -> pure $ SockAddrInet6 port 0 addr 0 - _ -> fail "SOCKS5 host should be IPv4 or IPv6 address" + _ -> fail "SOCKS5 host should be IPv4 or IPv6 address" instance StrEncoding SocksProxyWithAuth where strEncode (SocksProxyWithAuth auth proxy) = strEncode auth <> strEncode proxy @@ -263,10 +264,11 @@ instance StrEncoding SocksAuth where password <- A.takeTill (== '@') <* A.char '@' pure SocksAuthUsername {username, password} -mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> TMVar X.CertificateChain -> T.ClientParams -mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ serverCerts = +mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> Bool -> TMVar X.CertificateChain -> T.ClientParams +mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ sni serverCerts = (T.defaultParamsClient host p) - { T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_}, + { T.clientUseServerNameIndication = sni, + T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_}, T.clientHooks = def { T.onServerCertificate = onServerCert, diff --git a/src/Simplex/Messaging/Transport/HTTP2/Client.hs b/src/Simplex/Messaging/Transport/HTTP2/Client.hs index 53f229f06..b5e05d20d 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/Client.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/Client.hs @@ -78,7 +78,8 @@ defaultHTTP2ClientConfig = tcpKeepAlive = Nothing, logTLSErrors = True, clientCredentials = Nothing, - alpn = Nothing + alpn = Nothing, + useSNI = True }, bufferSize = defaultHTTP2BufferSize, bodyHeadSize = 16384, diff --git a/src/Simplex/Messaging/Transport/Server.hs b/src/Simplex/Messaging/Transport/Server.hs index 5c3be519c..fe4bd5879 100644 --- a/src/Simplex/Messaging/Transport/Server.hs +++ b/src/Simplex/Messaging/Transport/Server.hs @@ -7,8 +7,10 @@ module Simplex.Messaging.Transport.Server ( TransportServerConfig (..), ServerCredentials (..), + AddHTTP, defaultTransportServerConfig, runTransportServerState, + runTransportServerState_, SocketState, newSocketState, runTransportServer, @@ -65,6 +67,8 @@ data ServerCredentials = ServerCredentials } deriving (Show) +type AddHTTP = Bool + defaultTransportServerConfig :: TransportServerConfig defaultTransportServerConfig = TransportServerConfig @@ -87,32 +91,35 @@ runTransportServer started port srvSupported srvCreds alpn_ cfg server = do runTransportServerState ss started port srvSupported srvCreds alpn_ cfg server runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> (c -> IO ()) -> IO () -runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started Nothing port) (transportName (TProxy :: TProxy c)) +runTransportServerState ss started port srvSupported srvCreds alpn_ cfg server = runTransportServerState_ ss started port srvSupported (const srvCreds) alpn_ cfg (const server) + +runTransportServerState_ :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> TransportServerConfig -> (Socket -> c -> IO ()) -> IO () +runTransportServerState_ ss started port = runTransportServerSocketState ss started (startTCPServer started Nothing port) (transportName (TProxy :: TProxy c)) -- | Run a transport server with provided connection setup and handler. runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.Credential -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO () runTransportServerSocket started getSocket threadLabel srvCreds srvParams cfg server = do ss <- newSocketState - runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams cfg server + runTransportServerSocketState_ ss started getSocket threadLabel (const srvCreds) srvParams cfg (const server) -runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.Supported -> (X.CertificateChain, X.PrivKey) -> Maybe [ALPN] -> TransportServerConfig -> (a -> IO ()) -> IO () +runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> TransportServerConfig -> (Socket -> a -> IO ()) -> IO () runTransportServerSocketState ss started getSocket threadLabel srvSupported srvCreds alpn_ = runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams where srvParams = supportedTLSServerParams_ srvSupported srvCreds alpn_ -- | Run a transport server with provided connection setup and handler. -runTransportServerSocketState_ :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> (X.CertificateChain, X.PrivKey) -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO () +runTransportServerSocketState_ :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> (Maybe HostName -> (X.CertificateChain, X.PrivKey)) -> T.ServerParams -> TransportServerConfig -> (Socket -> a -> IO ()) -> IO () runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams cfg server = do labelMyThread $ "transport server for " <> threadLabel runTCPServerSocket ss started getSocket $ \conn -> - E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection server + E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection (server conn) where tCfg = serverTransportConfig cfg setup conn = timeout (tlsSetupTimeout cfg) $ do labelMyThread $ threadLabel <> "/setup" tls <- connectTLS Nothing tCfg srvParams conn - getServerConnection tCfg (fst srvCreds) tls + getServerConnection tCfg (fst $ srvCreds Nothing) tls -- | Run TCP server without TLS runLocalTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO () @@ -191,15 +198,15 @@ loadServerCredential ServerCredentials {caCertificateFile, certificateFile, priv Left _ -> putStrLn "invalid credential" >> exitFailure supportedTLSServerParams :: T.Credential -> Maybe [ALPN] -> T.ServerParams -supportedTLSServerParams = supportedTLSServerParams_ defaultSupportedParams +supportedTLSServerParams = supportedTLSServerParams_ defaultSupportedParams . const -supportedTLSServerParams_ :: T.Supported -> T.Credential -> Maybe [ALPN] -> T.ServerParams -supportedTLSServerParams_ serverSupported credential alpn_ = +supportedTLSServerParams_ :: T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> T.ServerParams +supportedTLSServerParams_ serverSupported creds alpn_ = def { T.serverWantClientCert = False, T.serverHooks = def - { T.onServerNameIndication = \_ -> pure $ T.Credentials [credential], + { T.onServerNameIndication = \host_ -> pure $ T.Credentials [creds host_], T.onALPNClientSuggest = (\alpn -> pure . fromMaybe "" . find (`elem` alpn)) <$> alpn_ }, T.serverSupported = serverSupported diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index ca9a27075..199b1a36a 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -2869,7 +2869,7 @@ testCreateQueueAuth srvVersion clnt1 clnt2 sqSecured baseId = do getClient clientId (clntAuth, clntVersion) db = let servers = initAgentServers {smp = userServers' [ProtoServerWithAuth testSMPServer clntAuth]} alpn_ = if clntVersion >= authCmdsSMPVersion then Just supportedSMPHandshakes else Nothing - smpCfg = defaultClientConfig alpn_ $ V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion + smpCfg = defaultClientConfig alpn_ False $ V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion sndAuthAlg = if srvVersion >= authCmdsSMPVersion && clntVersion >= authCmdsSMPVersion then C.AuthAlg C.SX25519 else C.AuthAlg C.SEd25519 in getSMPAgentClient' clientId agentCfg {smpCfg, sndAuthAlg} servers db diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index a25564a0b..40071873c 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -178,7 +178,7 @@ runNtfTestCfg :: HasCallStack => ATransport -> AgentMsgId -> ServerConfig -> Ntf runNtfTestCfg t baseId smpCfg ntfCfg aCfg bCfg runTest = do withSmpServerConfigOn t smpCfg testPort $ \_ -> withAPNSMockServer $ \apns -> - withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t)]} $ \_ -> + withNtfServerCfg ntfCfg {transports = [(ntfTestPort, t, False)]} $ \_ -> withAgentClientsCfg2 aCfg bCfg $ runTest apns baseId threadDelay 100000 diff --git a/tests/CLITests.hs b/tests/CLITests.hs index 1310665ee..e8417fc8b 100644 --- a/tests/CLITests.hs +++ b/tests/CLITests.hs @@ -3,15 +3,31 @@ module CLITests where -import Data.Ini (lookupValue, readIniFile) +import AgentTests.FunctionalAPITests (runRight_) +import Control.Logger.Simple +import Control.Monad +import qualified Crypto.PubKey.RSA as RSA +import qualified Data.ByteString.Lazy as BL +import qualified Data.HashMap.Strict as HM +import Data.Ini (Ini (..), lookupValue, readIniFile, writeIniFile) import Data.List (isPrefixOf) +import qualified Data.Text as T import qualified Data.X509 as X import qualified Data.X509.File as XF +import Data.X509.Validation (Fingerprint (..)) +import qualified Network.HTTP.Client as H1 +import qualified Network.HTTP2.Client as H2 import Simplex.FileTransfer.Server.Main (xftpServerCLI) +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Server.Main -import Simplex.Messaging.Server.Main -import Simplex.Messaging.Transport (simplexMQVersion) +import Simplex.Messaging.Server.Main (smpServerCLI, smpServerCLI_) +import Simplex.Messaging.Transport (TLS (..), defaultSupportedParams, defaultSupportedParamsHTTPS, simplexMQVersion, supportedClientSMPRelayVRange) +import Simplex.Messaging.Transport.Client (TransportClientConfig (..), defaultTransportClientConfig, runTLSTransportClient, smpClientHandshake) +import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..)) +import qualified Simplex.Messaging.Transport.HTTP2.Client as HC +import Simplex.Messaging.Transport.Server (loadFileFingerprint) import Simplex.Messaging.Util (catchAll_) +import qualified Static import System.Directory (doesFileExist) import System.Environment (withArgs) import System.FilePath (()) @@ -19,6 +35,10 @@ import System.IO.Silently (capture_) import System.Timeout (timeout) import Test.Hspec import Test.Main (withStdin) +import UnliftIO (catchAny) +import UnliftIO.Async (async, cancel) +import UnliftIO.Concurrent (threadDelay) +import UnliftIO.Exception (bracket) cfgPath :: FilePath cfgPath = "tests/tmp/cli/etc/opt/simplex" @@ -26,6 +46,9 @@ cfgPath = "tests/tmp/cli/etc/opt/simplex" logPath :: FilePath logPath = "tests/tmp/cli/etc/var/simplex" +webPath :: FilePath +webPath = "tests/tmp/cli/var/www" + ntfCfgPath :: FilePath ntfCfgPath = "tests/tmp/cli/etc/opt/simplex-notifications" @@ -46,6 +69,7 @@ cliTests = do it "with store log, random password (default)" $ smpServerTest True True it "no store log, no password" $ smpServerTest False False it "with store log, no password" $ smpServerTest True False + it "static files" smpServerTestStatic describe "Ntf server CLI" $ do it "should initialize, start and delete the server (no store log)" $ ntfServerTest False it "should initialize, start and delete the server (with store log)" $ ntfServerTest True @@ -61,7 +85,7 @@ smpServerTest storeLog basicAuth = do Right ini <- readIniFile $ cfgPath <> "/smp-server.ini" lookupValue "STORE_LOG" "enable" ini `shouldBe` Right (if storeLog then "on" else "off") lookupValue "STORE_LOG" "log_stats" ini `shouldBe` Right "off" - lookupValue "TRANSPORT" "port" ini `shouldBe` Right "5223" + lookupValue "TRANSPORT" "port" ini `shouldBe` Right "5223,443" lookupValue "TRANSPORT" "websockets" ini `shouldBe` Right "off" lookupValue "AUTH" "new_queues" ini `shouldBe` Right "on" lookupValue "INACTIVE_CLIENTS" "disconnect" ini `shouldBe` Right "off" @@ -70,7 +94,7 @@ smpServerTest storeLog basicAuth = do r <- lines <$> capture_ (withArgs ["start"] $ (100000 `timeout` smpServerCLI cfgPath logPath) `catchAll_` pure (Just ())) r `shouldContain` ["SMP server v" <> simplexMQVersion] r `shouldContain` (if storeLog then ["Store log: " <> logPath <> "/smp-server-store.log"] else ["Store log disabled."]) - r `shouldContain` ["Listening on port 5223 (TLS)..."] + r `shouldContain` ["Serving SMP protocol on port 5223 (TLS)...", "Serving SMP protocol on port 443 (TLS)...", "Serving static site on port 443 (TLS)..."] r `shouldContain` ["not expiring inactive clients"] r `shouldContain` (if basicAuth then ["creating new queues requires password"] else ["creating new queues allowed"]) -- cert @@ -94,6 +118,70 @@ smpServerTest storeLog basicAuth = do >>= (`shouldSatisfy` ("WARNING: deleting the server will make all queues inaccessible" `isPrefixOf`)) doesFileExist (cfgPath <> "/ca.key") `shouldReturn` False +smpServerTestStatic :: HasCallStack => IO () +smpServerTestStatic = do + let iniFile = cfgPath <> "/smp-server.ini" + capture_ (withArgs ["init", "-y", "--no-password", "--web-path", webPath] $ smpServerCLI cfgPath logPath) + >>= (`shouldSatisfy` (("Server initialized, please provide additional server information in " <> iniFile) `isPrefixOf`)) + doesFileExist (cfgPath <> "/ca.key") `shouldReturn` True + Right ini <- readIniFile iniFile + lookupValue "WEB" "static_path" ini `shouldBe` Right (T.pack webPath) + let transport = [("host", "localhost"), ("port", "5223"), ("log_tls_errors", "off"), ("websockets", "off")] + web = [("http", "8000"), ("https", "5223"), ("cert", "tests/fixtures/web.crt"), ("key", "tests/fixtures/web.key"), ("static_path", T.pack webPath)] + ini' = ini {iniSections = HM.insert "TRANSPORT" transport $ HM.insert "WEB" web (iniSections ini)} + writeIniFile iniFile ini' + + Right ini_ <- readIniFile iniFile + lookupValue "WEB" "https" ini_ `shouldBe` Right "5223" + + let smpServerCLI' = smpServerCLI_ Static.generateSite Static.serveStaticFiles Static.attachStaticFiles + let server = capture_ (withArgs ["start"] $ smpServerCLI' cfgPath logPath `catchAny` print) + bracket (async server) cancel $ \_t -> do + threadDelay 1000000 + html <- BL.readFile $ webPath <> "/index.html" + + -- "external" CA signing HTTP credentials + Fingerprint fpHTTP <- loadFileFingerprint "tests/fixtures/ca.crt" + let caHTTP = C.KeyHash fpHTTP + manager <- H1.newManager H1.defaultManagerSettings + H1.responseBody <$> H1.httpLbs "http://127.0.0.1:8000" manager `shouldReturn` html + logDebug "Plain HTTP works" + + threadDelay 2000000 + + let cfgHttp = defaultTransportClientConfig {alpn = Just ["h2"], useSNI = True} + runTLSTransportClient defaultSupportedParamsHTTPS Nothing cfgHttp Nothing "localhost" "5223" (Just caHTTP) $ \tls -> do + tlsALPN tls `shouldBe` Just "h2" + case getCerts tls of + X.Certificate {X.certPubKey = X.PubKeyRSA rsa} : _ca -> RSA.public_size rsa `shouldBe` 512 + leaf : _ -> error $ "Unexpected leaf cert: " <> show leaf + [] -> error "Empty chain" + + let h2cfg = HC.defaultHTTP2ClientConfig {HC.bodyHeadSize = 1024 * 1024} + h2 <- either (error . show) pure =<< HC.attachHTTP2Client h2cfg "localhost" "5223" mempty 65536 tls + let req = H2.requestNoBody "GET" "/" [] + HC.HTTP2Response {HC.respBody = HTTP2Body {bodyHead = shsBody}} <- either (error . show) pure =<< HC.sendRequest h2 req (Just 1000000) + BL.fromStrict shsBody `shouldBe` html + logDebug "Combined HTTPS works" + + -- "local" CA signing SMP credentials + Fingerprint fpSMP <- loadFileFingerprint (cfgPath <> "/ca.crt") + let caSMP = C.KeyHash fpSMP + let cfgSmp = defaultTransportClientConfig {alpn = Just ["smp/1"], useSNI = False} + runTLSTransportClient defaultSupportedParams Nothing cfgSmp Nothing "localhost" "5223" (Just caSMP) $ \tls -> do + tlsALPN tls `shouldBe` Just "smp/1" + case getCerts tls of + X.Certificate {X.certPubKey = X.PubKeyEd25519 _k} : _ca -> print _ca -- pure () + leaf : _ -> error $ "Unexpected leaf cert: " <> show leaf + [] -> error "Empty chain" + runRight_ . void $ smpClientHandshake tls Nothing caSMP supportedClientSMPRelayVRange + logDebug "Combined SMP works" + where + getCerts :: TLS -> [X.Certificate] + getCerts tls = + let X.CertificateChain cc = tlsServerCerts tls + in map (X.signedObject . X.getSigned) cc + ntfServerTest :: Bool -> IO () ntfServerTest storeLog = do capture_ (withArgs (["init"] <> ["-l" | storeLog]) $ ntfServerCLI ntfCfgPath ntfLogPath) @@ -107,7 +195,7 @@ ntfServerTest storeLog = do r <- lines <$> capture_ (withArgs ["start"] $ (100000 `timeout` ntfServerCLI ntfCfgPath ntfLogPath) `catchAll_` pure (Just ())) r `shouldContain` ["SMP notifications server v" <> simplexMQVersion] r `shouldContain` (if storeLog then ["Store log: " <> ntfLogPath <> "/ntf-server-store.log"] else ["Store log disabled."]) - r `shouldContain` ["Listening on port 443 (TLS)..."] + r `shouldContain` ["Serving SMP protocol on port 443 (TLS)..."] capture_ (withStdin "Y" . withArgs ["delete"] $ ntfServerCLI ntfCfgPath ntfLogPath) >>= (`shouldSatisfy` ("WARNING: deleting the server will make all queues inaccessible" `isPrefixOf`)) doesFileExist (cfgPath <> "/ca.key") `shouldReturn` False diff --git a/tests/CoreTests/SOCKSSettings.hs b/tests/CoreTests/SOCKSSettings.hs index 385208fd4..78e4ae187 100644 --- a/tests/CoreTests/SOCKSSettings.hs +++ b/tests/CoreTests/SOCKSSettings.hs @@ -90,7 +90,7 @@ testSocksMode = do where transportSocks proxy socksMode = transportSocksCfg defaultNetworkConfig {socksProxy = proxy, socksMode} transportSocksCfg cfg host = - let TransportClientConfig {socksProxy} = transportClientConfig cfg host + let TransportClientConfig {socksProxy} = transportClientConfig cfg host True in socksProxy testSocksProxyEncoding :: Spec diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index accaa4d37..87ff797f0 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -122,10 +122,10 @@ ntfServerCfgVPrev = serverVRange' = serverVRange smpCfg' withNtfServerStoreLog :: ATransport -> (ThreadId -> IO a) -> IO a -withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t)]} +withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t, False)]} withNtfServerThreadOn :: ATransport -> ServiceName -> (ThreadId -> IO a) -> IO a -withNtfServerThreadOn t port' = withNtfServerCfg ntfServerCfg {transports = [(port', t)]} +withNtfServerThreadOn t port' = withNtfServerCfg ntfServerCfg {transports = [(port', t, False)]} withNtfServerCfg :: HasCallStack => NtfServerConfig -> (ThreadId -> IO a) -> IO a withNtfServerCfg cfg@NtfServerConfig {transports} = diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 2cf92640f..970be7174 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -121,6 +121,7 @@ cfg = privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt" }, + httpCredentials = Nothing, smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig, controlPort = Nothing, @@ -166,7 +167,7 @@ withSmpServerStoreLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just tes withSmpServerConfigOn :: HasCallStack => ATransport -> ServerConfig -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerConfigOn t cfg' port' = serverBracket - (\started -> runSMPServerBlocking started cfg' {transports = [(port', t)]}) + (\started -> runSMPServerBlocking started cfg' {transports = [(port', t, False)]} Nothing) (threadDelay 10000) withSmpServerThreadOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a diff --git a/tests/fixtures/web.crt b/tests/fixtures/web.crt new file mode 100644 index 000000000..4de287b1e --- /dev/null +++ b/tests/fixtures/web.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIDnTCCAx2gAwIBAgIUFhZZsKj9uBgGnUrr+Cf3XFf7t6IwBQYDK2VxMCoxFjAU +BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjQwOTI2 +MTIyNTEyWhgPNDc2MjA4MjMxMjI1MTJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALB59b8oyxP5YtXI1kemBzJU +Pt0xLN/Tmzdul283DhbNCJV+eUn4fNz+PjiRS/F2vZLb3WXInPi3bc57hw2Yu94o +7MXH5DTWkaubNq0bV0Koi17zZBSCOOq+MbPN7bUT1sOwOHadLh3IWTfkz9EufowD +ivpymNKWbeAHMXlXsBJnHfHuM05MWlP87PTHd3D7YQmmgbISgEGG4GchWBqnnCxx +gOOa09f/n+gWJFbN3hkbVZKMEpT5gu9WWsgv9BDhJzcBSw13MMz0sByxYKzhwQBJ +ikFz+16AttZ0ccoDaWwajZzK8+yfFv9T3b8kWmioHi2dw2vBgSove78liUqYCsOU +Bt5MNk3P037KgSJPdp6azsF3bMKmPssEhT9vHMPgSkiBfmBlJ7dTTRd9dh/cLKIO +AMzu4O+pEodIOJDXTARBE6VX1qoEZQuft5+ljVy4i9ySpmHnkxLocF40rKV1G0c5 +LnVNTtr5GokC9sfIXZPZw0EEpk3eAseNWccwuyRfHQfL6yjcDig2IdLvLVcm9JyA +2P5QpP15EoA3Ow9uX8HmBbSFe1F35rqcNwY0lhDXEboSA/X4xDLnu4aVhNPiUnRq +NXqVlgz5ybRAUHd8fDBwK8fT5VhvuEnCja7+8hVc33gK56vu+28ZMkN2Y4z0GNQd +iamPUZJlUcCJzNI2cz27AgMBAAGjbzBtMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgPI +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBSWiPT6Nl13/CTjaHCkHp17 +GWoyvzAfBgNVHSMEGDAWgBQcUJvR7mm26yxMQfCsWgbnwMmJVDAFBgMrZXEDcwDC +DTbvSA61ydoRA8mTHFW1EYL+xfQjo0aH56N1Aqn47DzLGQZjP/fxoW929+Jwoiz0 +UgUtUAeFjgA9wfvDv7mMm/K4wqyiZzFuWVZdQV6AUwBJK0hN5qlXpvJzMKLrj3Ap +dRELAgLJvC2e/xVc3dXSFwA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCATagAwIBAgIUe2PryrWo0xXX9vcA3WfbCzcdmgAwBQYDK2VxMCoxFjAU +BgNVBAMMDVNNUCBzZXJ2ZXIgQ0ExEDAOBgNVBAoMB1NpbXBsZVgwIBcNMjIwMTEx +MTExNjM5WhgPNDc1OTEyMDgxMTE2MzlaMCoxFjAUBgNVBAMMDVNNUCBzZXJ2ZXIg +Q0ExEDAOBgNVBAoMB1NpbXBsZVgwQzAFBgMrZXEDOgCAcvFwVicR+RLZpiEWPFNR +XYTbf+mFcX1NHIyPQDugFwOCgqJAW1fsjYgFhtQJSMH/lc1N7clfm4CjUzBRMB0G +A1UdDgQWBBQcUJvR7mm26yxMQfCsWgbnwMmJVDAfBgNVHSMEGDAWgBQcUJvR7mm2 +6yxMQfCsWgbnwMmJVDAPBgNVHRMBAf8EBTADAQH/MAUGAytlcQNzAAAP/hMPNxyW +fyJi+iJViodU+C/aklnvHtjh5P3AbiVCSUfY6+PEdvkC8Ov0pBAYpYi5ukSNNVXl +ABVRlipB+vOcLQStNyaZ7kXzQ2IO/0btmIidh+G6SP8I4aytYIYYcV5pEUZpG1L1 +57g8P29SDv81AA== +-----END CERTIFICATE----- diff --git a/tests/fixtures/web.key b/tests/fixtures/web.key new file mode 100644 index 000000000..249d8b8b6 --- /dev/null +++ b/tests/fixtures/web.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCwefW/KMsT+WLV +yNZHpgcyVD7dMSzf05s3bpdvNw4WzQiVfnlJ+Hzc/j44kUvxdr2S291lyJz4t23O +e4cNmLveKOzFx+Q01pGrmzatG1dCqIte82QUgjjqvjGzze21E9bDsDh2nS4dyFk3 +5M/RLn6MA4r6cpjSlm3gBzF5V7ASZx3x7jNOTFpT/Oz0x3dw+2EJpoGyEoBBhuBn +IVgap5wscYDjmtPX/5/oFiRWzd4ZG1WSjBKU+YLvVlrIL/QQ4Sc3AUsNdzDM9LAc +sWCs4cEASYpBc/tegLbWdHHKA2lsGo2cyvPsnxb/U92/JFpoqB4tncNrwYEqL3u/ +JYlKmArDlAbeTDZNz9N+yoEiT3aems7Bd2zCpj7LBIU/bxzD4EpIgX5gZSe3U00X +fXYf3CyiDgDM7uDvqRKHSDiQ10wEQROlV9aqBGULn7efpY1cuIvckqZh55MS6HBe +NKyldRtHOS51TU7a+RqJAvbHyF2T2cNBBKZN3gLHjVnHMLskXx0Hy+so3A4oNiHS +7y1XJvScgNj+UKT9eRKANzsPbl/B5gW0hXtRd+a6nDcGNJYQ1xG6EgP1+MQy57uG +lYTT4lJ0ajV6lZYM+cm0QFB3fHwwcCvH0+VYb7hJwo2u/vIVXN94Cuer7vtvGTJD +dmOM9BjUHYmpj1GSZVHAiczSNnM9uwIDAQABAoICAAIOOHg0nO85RMTNItpjgeYY +P0HGvIAk63rX4dqss9lhbQBie9B4HPzIjjEfMv13qj5VBtimllYNTXrEuSUzeCi6 +E7vyTpo+qv/YEHtUadb/2tzfe1BxjdyX0vfz+CtXbAeefH1O6mGrI/Uuo3Xmpc9p +jJSmpg/DBl53Amm6xWLk6rq7dcNiWrfZS0T9xYFQmx7RlZwmct/ZqR56Zrw942ff +Hkts9psniyeiHBr2cnpRrEJry69T0q6JIeP+5doWewCqzPl+9rMyKiT6RV3uJKpu +Z7ZavthNl6Xj+FTDHdcGJ0v5Bg/llQ8Qb6f/FsLImM6IgBwlj4AXXMiP1SboolEo +TDgt4DE0sd7o5ZU+5gjI2E9l1JLk68Rh64YIY1pr7CDURkWYwGyR3Bs9mG3RVKZr +ANl08YeqtTH8LnqASJyKq6+xthDSCRbEP6uFM/Y5jjjbCWXELtaWqRYo1esHjPU2 +OfVI8tE13+ewLhjCUvarf9TA4Edkut7celuNgPsm58+cA5FSQiuuGrR6RoeOCYEd +a9knZQriLebcHT8ifh1SfbuElhSMiSTUReEXzOEjs9+/kZ8BEQWWmspGVq/RiOoj +jtPrDE11wqRjjK2SjLwFQ0NlHo+sUGxH7IJMVOHONcaFde87KTggjFr2HJgOtkYe +zii/I6bVCH6IxKZ3jBqlAoIBAQD085iZEgp1uoXqnlNgL49qy8a2Zev5B3mQjS61 +1+LYurIKm2TnWFdUyrqOyY9EQ5mj7LbtntcIseW5gNPqHF9F8UQ3NfU1uXiKpBGF +dLi/NGPPOoeep4GOOo7+TFluwPQILB7UPoLrU+cNcNt6V5FMKkRaANb/nxxPdWWS +UPpCU2zoRNoeGEvXU3yPlhMJcCWYc6wP6YXClqrUsUmcX0x1MPY54ad18jxjR1P9 +msemV98tqI9/utjuL0sOIZKlfR225Bj3RQ6u1dPqwPaWVcXH+I4za/SYnJ8Ivrz4 +hokIHUPHbC1C/+wfVQhLU5Z9fT061IHKwmVX1NRC0aYdxm0tAoIBAQC4b7IiEcRW +BgmBRM4/BgKCHoqZwEojZozYTBzsEQinRY5spfEow+ONUFYubnr4rDOHVTUMHStm +GEvRfZyGMU7Xp0BkbVyMrrUCGhOLtIL7qLsN1ZryUGuZzFO0u7Q8lvYRKiuHtq1v +QVqFzOVut+Wa0e//RCStrGlex6ZXpGrHf6EISc2jeDeLTcVcQkLY6pQoMtMFkjJi +7l671AUA5ISqCiJv9DbL2XItZGw1N9zXXy4tLJGr4gOyX0+JE9FFyTUaC5Yeu8FO +0qxop1hW/ekEYISMJtEvigphAhv5hShEUp5+ZqNcnUxj5FejFDfBi6sHT9vmcNwA +RoYkzuvtpzeHAoIBACp0mitVvChhltpubKcMN0BcZ2mvyrGUARbz0XfFHlVQLpG2 +E0whvKk+pg0flExRpyyJV79hu4WPR/DaCmDWYBEAW0Fygbi5F9J3022dKHRDgVUm +oOD3yXW8YpJi61FN8j4EX6eL2ictmKt0tyXCTbW00boD0T/m9QI0p9EvZeDfEs5D +OMbkkSiWGM3ORihpnqqIyfbME9oBQUSyIb8PqXHadaLcoKjJvnu6ni0jiZ0kN9Nz +FsQdv4GxAsJFQWSbhe3wJP+eoYfeGefjYBn4bdpWE1eIS5Gz+8CJRrmQn+mfIONM +tZ+aOfPISjK8HyZK8bTjpkddYDFT+yJFshQRE1UCggEBAJC7rllIAf/Tqv/TY9pX +N/6uQuvW1xcisaJHUGb8EwNY9SRTsITh/B74DTlQn5WnZKRt/DvuZBExPcY+wWcZ +KJrY+BIXNAp+SzNEDVSTqjoctfVsS7Sd4WKG0qVAq3bkrGLZ6eENPNrSuVvIZ79T +9o1g8+ooqnPTmbi0Cdg7AURe5pqfeA0xGL1roVX99YFNzEgjYi+8A2hZUOQqxGZn +7aeWXmHmjl233P68EKJOnTIx0gXHNOVibq46Vyrl71LJS6+NqheiFVdqwbs6n3tc +s9AogbuN9phMxkpMInHTyb6b6x6cItRZ6Al3tkIWao6qsOMDCziyFiLtNPWLn98W +Wt8CggEBAPNpzv2HT1BkDOvWzPxKtKZg9dDRoPi12N2jVLoGqnrHf8r3+oqOYcHs +zsi1QgXsGoTHFGJZZ9Op5FSlcffTKiIb9IJESfkSjp7njSvYeiKyTkYzlb/N42qy +qgIph91xrT35NEMJQIvHX0wEFaJZ2BDdVRXgUo6cwhBJp5JjwQTwZ3msqCVC6wBk +iJGL46LAE7/6YalcwlvBxMudW+NrZ8TRnzEOBMzgHf5K8e6sGfhllspCG0HmzyBX +euEfBjGykCmlNTs55/p/4aXBY8ydJQK7o8aBlEgL3EYqoDTyq8kSar6O35rnXpP8 +mPykG5ZK8mWK1XSXXze7YGNUW1TjtmY= +-----END PRIVATE KEY-----