diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4c44cff9..04a2acdf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: run: cabal build --enable-tests - name: Test - timeout-minutes: 30 + timeout-minutes: 40 shell: bash run: cabal test --test-show-details=direct @@ -63,7 +63,7 @@ jobs: mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}} - name: Build changelog - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04' id: build_changelog uses: mikepenz/release-changelog-builder-action@v1 with: @@ -75,7 +75,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' && matrix.ghc == '9.6.3' + if: startsWith(github.ref, 'refs/tags/v') && matrix.ghc != '8.10.7' uses: softprops/action-gh-release@v1 with: body: | diff --git a/CHANGELOG.md b/CHANGELOG.md index ed15e7083..e161a26df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,105 @@ +# 5.8.0 + +Version 5.8.0.10 + +SMP server and client: +- protocol extension to forward messages to the destination servers, to protect sending client IP address and transport session. + +Agent: +- process timed out subscription responses to reduce the number of resubscriptions. +- avoid sending messages and commands when waiting for response timed out (except batched SUB and DEL commands). +- fix issue with stuck message reception on slow connection (when response to ACK timed out, and the new message was not processed until resubscribed). +- fix issue when temporary file sending or receiving error was treated as permanent. + +SMP server: +- include OK responses to all batched SUB requests to reduce subscription timeouts. + +XFTP server: +- report file upload timeout as TIMEOUT, to avoid delivery failure. + +# 5.7.6 + +XFTP agent: +- treat XFTP handshake timeouts and network errors as temporary, to retry file operations. + +# 5.7.5 + +SMP agent: +- fail if non-unique connection IDs are passed to sendMessages (to prevent client errors and deadlocks). + +# 5.7.4 + +SMP agent: +- remove re-subscription timeouts (as they are tracked per operation, and could cause failed subscriptions). +- reconnect XFTP clients when network settings changes. +- fix lock contention resulting in stuck subscriptions on network change. + +# 5.7.3 + +SMP/NTF protocol: +- add ALPN for handshake version negotiation, similar to XFTP (to preserve backwards compatibility with the old clients). +- upgrade clients to versions v7/v2 of the protocols. + +SMP server: +- faster responses to subscription requests. + +XFTP client: +- fix network exception during file download treated as permanent file error. + +SMP agent: +- do not report subscription timeouts while client is offline. + +# 5.7.2 + +SMP agent: +- fix connections failing when connecting via link due to race condition on slow network. +- remove concurrency limit when waiting for connection subscription. +- remove TLS timeout. + +# 5.7.1 + +SMP agent: +- increase timeout for TLS connection via SOCKS + +# 5.7.0 + +Version 5.7.0.4 + +_Please note_: the earliest SimpleX Chat clients supported by this version of the servers is 5.5.3 (released on February 11, 2024). + +SMP server: +- increase max SMP protocol version to 7 (support for deniable authenticators). + +NTF server: +- increase max NTF protocol version to 2 (support for deniable authenticators). + +XFTP server: +- version handshake using ALPN. + +SMP agent: +- increase timeouts for XFTP files. +- don't send commands after timeout. +- PQ encryption support. + +# 5.6.2 + +Version 5.6.2.2. + +SMP agent: +- Lower memory consumption (~20-25%). +- More stable XFTP file uploads and downloads. +- API to receive network connectivity changes from the apps. +- to reduce battery consumption: connection attempts interval growing to every 2 hours when app reports as offline. +- to reduce retries and traffic: 50% increased timeouts when on mobile network. + +XFTP server: +- expire files on start. +- version negotiation based on TLS ALPN and handshake. + +NTF server: +- reduced downtime by ~100x faster start time. +- exclude test tokens from statistics. + # 5.6.1 Version 5.6.1.0. diff --git a/apps/smp-server/Main.hs b/apps/smp-server/Main.hs index d5cc5e732..d0de4b4f1 100644 --- a/apps/smp-server/Main.hs +++ b/apps/smp-server/Main.hs @@ -1,10 +1,9 @@ -{-# LANGUAGE LambdaCase #-} - module Main where import Control.Logger.Simple import Simplex.Messaging.Server.CLI (getEnvPath) import Simplex.Messaging.Server.Main +import qualified Static defaultCfgPath :: FilePath defaultCfgPath = "/etc/opt/simplex" @@ -20,4 +19,4 @@ main = do setLogLevel LogDebug cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath - withGlobalLogging logCfg $ smpServerCLI cfgPath logPath + withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath diff --git a/apps/smp-server/static/contact/index.html b/apps/smp-server/static/contact/index.html new file mode 120000 index 000000000..1140bcf31 --- /dev/null +++ b/apps/smp-server/static/contact/index.html @@ -0,0 +1 @@ +../link.html \ No newline at end of file diff --git a/apps/smp-server/static/index.html b/apps/smp-server/static/index.html new file mode 100644 index 000000000..e3889de2a --- /dev/null +++ b/apps/smp-server/static/index.html @@ -0,0 +1,514 @@ + + + + + + + SimpleX Chat - Server Information + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ +
+
+
+

Server + information

+ +
+
+

+ Public information +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Source code:${sourceCode}
Web site:${website}
Usage conditions:${usageConditions}
Amendments:${usageAmendments}
Operator:${operatorEntity} (${operatorCountry})
Administrator contacts: + +
Complaints and feedback: + +
Hosting provider:${hostingEntity} (${hostingCountry})
Server country:${serverCountry}
+
+ +
+

+ Configuration

+ + + + + + + + + + + + + + + + + + + + + +
Persistence:${persistence}
Message expiration:${messageExpiration}
Stats enabled:${statsEnabled}
New queues allowed:${newQueuesAllowed}
Basic auth enabled:${basicAuthEnabled}
+
+
+
+
+ +
+
+

+ Join SimpleX +

+

We invite you to join the conversation

+ +
+

Get SimpleX desktop app

+
+
+
+ +
+
+ © + 2020-2023 SimpleX | Open-Source Project +
+
+ + + + + \ No newline at end of file diff --git a/apps/smp-server/static/invitation/index.html b/apps/smp-server/static/invitation/index.html new file mode 120000 index 000000000..1140bcf31 --- /dev/null +++ b/apps/smp-server/static/invitation/index.html @@ -0,0 +1 @@ +../link.html \ No newline at end of file diff --git a/apps/smp-server/static/link.html b/apps/smp-server/static/link.html new file mode 100644 index 000000000..cbab7324f --- /dev/null +++ b/apps/smp-server/static/link.html @@ -0,0 +1,527 @@ + + + + + + + SimpleX Chat - Invitation + + + + + + + + +
+
+
+ +
+
+ +
+ + + +
+
+
+

You received a + 1-time link to connect on SimpleX Chat

+

To make a + connection:

+
+

Install SimpleX app

+
+
+

1

+
+
+
+

Connect in app

Open + Simplex app +
+

2

+
+
+
+

Tap the‘connect’button in the app

+
+

3

+
+
+
+
+
+ + + +
+
+

+ Join SimpleX +

+

We invite you to join the conversation

+ +
+

Get SimpleX desktop app

+
+
+
+
+
+ © + 2020-2024 SimpleX | Open-Source Project +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/apps/smp-server/static/media/GilroyBold.woff2 b/apps/smp-server/static/media/GilroyBold.woff2 new file mode 100644 index 000000000..687474f86 Binary files /dev/null and b/apps/smp-server/static/media/GilroyBold.woff2 differ diff --git a/apps/smp-server/static/media/GilroyLight.woff2 b/apps/smp-server/static/media/GilroyLight.woff2 new file mode 100644 index 000000000..f4d4fc3e0 Binary files /dev/null and b/apps/smp-server/static/media/GilroyLight.woff2 differ diff --git a/apps/smp-server/static/media/GilroyMedium.woff2 b/apps/smp-server/static/media/GilroyMedium.woff2 new file mode 100644 index 000000000..895b6d4f1 Binary files /dev/null and b/apps/smp-server/static/media/GilroyMedium.woff2 differ diff --git a/apps/smp-server/static/media/GilroyRegular.woff2 b/apps/smp-server/static/media/GilroyRegular.woff2 new file mode 100644 index 000000000..24465fb7d Binary files /dev/null and b/apps/smp-server/static/media/GilroyRegular.woff2 differ diff --git a/apps/smp-server/static/media/GilroyRegularItalic.woff2 b/apps/smp-server/static/media/GilroyRegularItalic.woff2 new file mode 100644 index 000000000..ef01c8d5d Binary files /dev/null and b/apps/smp-server/static/media/GilroyRegularItalic.woff2 differ diff --git a/apps/smp-server/static/media/apk_icon.png b/apps/smp-server/static/media/apk_icon.png new file mode 100644 index 000000000..60ff342d3 Binary files /dev/null and b/apps/smp-server/static/media/apk_icon.png differ diff --git a/apps/smp-server/static/media/apple_store.svg b/apps/smp-server/static/media/apple_store.svg new file mode 100644 index 000000000..b977fa2cf --- /dev/null +++ b/apps/smp-server/static/media/apple_store.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/contact.js b/apps/smp-server/static/media/contact.js new file mode 100644 index 000000000..b1a99b74f --- /dev/null +++ b/apps/smp-server/static/media/contact.js @@ -0,0 +1,66 @@ +(function () { + + let complete = false + run() + window.onload = run + + async function run() { + const connURIel = document.getElementById("conn_req_uri_text"); + const mobileConnURIanchor = document.getElementById("mobile_conn_req_uri"); + const connQRCodes = document.getElementsByClassName("conn_req_uri_qrcode"); + console.log(connQRCodes); + if (complete || !connURIel || !mobileConnURIanchor || connQRCodes < 2) return + complete = true + let connURI = document.location.toString() + const parsedURI = new URL(connURI) + const path = parsedURI.pathname.split("/") + const len = path.length + const action = path[len - (path[len - 1] == "" ? 2 : 1)] + parsedURI.protocol = "https" + parsedURI.pathname = "/" + action + connURI = parsedURI.toString() + console.log("connection URI: ", connURI) + mobileConnURIanchor.href = "simplex:" + parsedURI.pathname + parsedURI.hash + connURIel.innerText = "/c " + connURI + for (const connQRCode of connQRCodes) { + try { + await QRCode.toCanvas(connQRCode, connURI, { + errorCorrectionLevel: "M", + color: {dark: "#062D56"} + }); + connQRCode.style.width = "320px"; + connQRCode.style.height = "320px"; + } catch (err) { + console.error(err); + } + } + + function contentCopyWithTooltip(parent) { + const content = parent.querySelector(".content"); + const tooltip = parent.querySelector(".tooltiptext"); + console.log(parent.querySelector(".content_copy"), 111) + console.log(parent) + const copyButton = parent.querySelector(".content_copy"); + copyButton.addEventListener("click", copyAddress) + copyButton.addEventListener("mouseout", resetTooltip) + + function copyAddress() { + navigator.clipboard.writeText(content.innerText || content.value); + tooltip.innerHTML = "Copied!"; + } + + function resetTooltip() { + tooltip.innerHTML = "Copy to clipboard"; + } + } + + function copyAddress() { + navigator.clipboard.writeText(connURI); + tooltipEl.innerHTML = "Copied!"; + } + + function resetTooltip() { + tooltipEl.innerHTML = "Copy to clipboard"; + } + } +})(); diff --git a/apps/smp-server/static/media/contact_page_mobile.png b/apps/smp-server/static/media/contact_page_mobile.png new file mode 100644 index 000000000..7d829965a Binary files /dev/null and b/apps/smp-server/static/media/contact_page_mobile.png differ diff --git a/apps/smp-server/static/media/f_droid.svg b/apps/smp-server/static/media/f_droid.svg new file mode 100644 index 000000000..2d6ec6871 --- /dev/null +++ b/apps/smp-server/static/media/f_droid.svg @@ -0,0 +1,372 @@ + + + + + + + + + + + + + GET IT ON + F-Droid + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/favicon.ico b/apps/smp-server/static/media/favicon.ico new file mode 100644 index 000000000..c45f3acf0 Binary files /dev/null and b/apps/smp-server/static/media/favicon.ico differ diff --git a/apps/smp-server/static/media/google_play.svg b/apps/smp-server/static/media/google_play.svg new file mode 100644 index 000000000..6cda5f6d3 --- /dev/null +++ b/apps/smp-server/static/media/google_play.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/logo-dark.png b/apps/smp-server/static/media/logo-dark.png new file mode 100644 index 000000000..1886df819 Binary files /dev/null and b/apps/smp-server/static/media/logo-dark.png differ diff --git a/apps/smp-server/static/media/logo-light.png b/apps/smp-server/static/media/logo-light.png new file mode 100644 index 000000000..3da724650 Binary files /dev/null and b/apps/smp-server/static/media/logo-light.png differ diff --git a/apps/smp-server/static/media/logo-symbol-dark.svg b/apps/smp-server/static/media/logo-symbol-dark.svg new file mode 100644 index 000000000..fa598acf3 --- /dev/null +++ b/apps/smp-server/static/media/logo-symbol-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/smp-server/static/media/logo-symbol-light.svg b/apps/smp-server/static/media/logo-symbol-light.svg new file mode 100644 index 000000000..d8b5951a0 --- /dev/null +++ b/apps/smp-server/static/media/logo-symbol-light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/smp-server/static/media/moon.svg b/apps/smp-server/static/media/moon.svg new file mode 100644 index 000000000..1054735b5 --- /dev/null +++ b/apps/smp-server/static/media/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/smp-server/static/media/qrcode.js b/apps/smp-server/static/media/qrcode.js new file mode 100644 index 000000000..c5c56eb37 --- /dev/null +++ b/apps/smp-server/static/media/qrcode.js @@ -0,0 +1 @@ +var QRCode=function(t){"use strict";var r,e=function(){return"function"==typeof Promise&&Promise.prototype&&Promise.prototype.then},n=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706],o=function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return 4*t+17},a=function(t){return n[t]},i=function(t){for(var r=0;0!==t;)r++,t>>>=1;return r},u=function(t){if("function"!=typeof t)throw new Error('"toSJISFunc" is not a valid function.');r=t},s=function(){return void 0!==r},f=function(t){return r(t)};function h(t,r){return t(r={exports:{}},r.exports),r.exports}var c=h((function(t,r){r.L={bit:1},r.M={bit:0},r.Q={bit:3},r.H={bit:2},r.isValid=function(t){return t&&void 0!==t.bit&&t.bit>=0&&t.bit<4},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"l":case"low":return r.L;case"m":case"medium":return r.M;case"q":case"quartile":return r.Q;case"h":case"high":return r.H;default:throw new Error("Unknown EC Level: "+t)}}(t)}catch(t){return e}}}));function g(){this.buffer=[],this.length=0}c.L,c.M,c.Q,c.H,c.isValid,g.prototype={get:function(t){var r=Math.floor(t/8);return 1==(this.buffer[r]>>>7-t%8&1)},put:function(t,r){for(var e=0;e>>r-e-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var r=Math.floor(this.length/8);this.buffer.length<=r&&this.buffer.push(0),t&&(this.buffer[r]|=128>>>this.length%8),this.length++}};var d=g;function l(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}l.prototype.set=function(t,r,e,n){var o=t*this.size+r;this.data[o]=e,n&&(this.reservedBit[o]=!0)},l.prototype.get=function(t,r){return this.data[t*this.size+r]},l.prototype.xor=function(t,r,e){this.data[t*this.size+r]^=e},l.prototype.isReserved=function(t,r){return this.reservedBit[t*this.size+r]};var v=l,p=h((function(t,r){var e=o;r.getRowColCoords=function(t){if(1===t)return[];for(var r=Math.floor(t/7)+2,n=e(t),o=145===n?26:2*Math.ceil((n-13)/(2*r-2)),a=[n-7],i=1;i=0&&t<=7},r.from=function(t){return r.isValid(t)?parseInt(t,10):void 0},r.getPenaltyN1=function(t){for(var r=t.size,n=0,o=0,a=0,i=null,u=null,s=0;s=5&&(n+=e+(o-5)),i=h,o=1),(h=t.get(f,s))===u?a++:(a>=5&&(n+=e+(a-5)),u=h,a=1)}o>=5&&(n+=e+(o-5)),a>=5&&(n+=e+(a-5))}return n},r.getPenaltyN2=function(t){for(var r=t.size,e=0,o=0;o=10&&(1488===n||93===n)&&e++,a=a<<1&2047|t.get(u,i),u>=10&&(1488===a||93===a)&&e++}return e*o},r.getPenaltyN4=function(t){for(var r=0,e=t.data.length,n=0;n=0;){for(var n=e[0],o=0;o0){var o=new Uint8Array(this.degree);return o.set(e,n),o}return e};var L=T,b=function(t){return!isNaN(t)&&t>=1&&t<=40},U="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+",x="(?:(?![A-Z0-9 $%*+\\-./:]|"+(U=U.replace(/u/g,"\\u"))+")(?:.|[\r\n]))+",k=new RegExp(U,"g"),F=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),S=new RegExp(x,"g"),D=new RegExp("[0-9]+","g"),Y=new RegExp("[A-Z $%*+\\-./:]+","g"),_=new RegExp("^"+U+"$"),z=new RegExp("^[0-9]+$"),H=new RegExp("^[A-Z0-9 $%*+\\-./:]+$"),J={KANJI:k,BYTE_KANJI:F,BYTE:S,NUMERIC:D,ALPHANUMERIC:Y,testKanji:function(t){return _.test(t)},testNumeric:function(t){return z.test(t)},testAlphanumeric:function(t){return H.test(t)}},K=h((function(t,r){r.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},r.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},r.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},r.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},r.MIXED={bit:-1},r.getCharCountIndicator=function(t,r){if(!t.ccBits)throw new Error("Invalid mode: "+t);if(!b(r))throw new Error("Invalid version: "+r);return r>=1&&r<10?t.ccBits[0]:r<27?t.ccBits[1]:t.ccBits[2]},r.getBestModeForData=function(t){return J.testNumeric(t)?r.NUMERIC:J.testAlphanumeric(t)?r.ALPHANUMERIC:J.testKanji(t)?r.KANJI:r.BYTE},r.toString=function(t){if(t&&t.id)return t.id;throw new Error("Invalid mode")},r.isValid=function(t){return t&&t.bit&&t.ccBits},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"numeric":return r.NUMERIC;case"alphanumeric":return r.ALPHANUMERIC;case"kanji":return r.KANJI;case"byte":return r.BYTE;default:throw new Error("Unknown mode: "+t)}}(t)}catch(t){return e}}}));K.NUMERIC,K.ALPHANUMERIC,K.BYTE,K.KANJI,K.MIXED,K.getCharCountIndicator,K.getBestModeForData,K.isValid;var O=h((function(t,r){var e=i(7973);function n(t,r){return K.getCharCountIndicator(t,r)+4}function o(t,r){var e=0;return t.forEach((function(t){var o=n(t.mode,r);e+=o+t.getBitsLength()})),e}r.from=function(t,r){return b(t)?parseInt(t,10):r},r.getCapacity=function(t,r,e){if(!b(t))throw new Error("Invalid QR Code version");void 0===e&&(e=K.BYTE);var o=8*(a(t)-M(t,r));if(e===K.MIXED)return o;var i=o-n(e,t);switch(e){case K.NUMERIC:return Math.floor(i/10*3);case K.ALPHANUMERIC:return Math.floor(i/11*2);case K.KANJI:return Math.floor(i/13);case K.BYTE:default:return Math.floor(i/8)}},r.getBestVersionForData=function(t,e){var n,a=c.from(e,c.M);if(Array.isArray(t)){if(t.length>1)return function(t,e){for(var n=1;n<=40;n++){if(o(t,n)<=r.getCapacity(n,e,K.MIXED))return n}}(t,a);if(0===t.length)return 1;n=t[0]}else n=t;return function(t,e,n){for(var o=1;o<=40;o++)if(e<=r.getCapacity(o,n,t))return o}(n.mode,n.getLength(),a)},r.getEncodedBits=function(t){if(!b(t)||t<7)throw new Error("Invalid QR Code version");for(var r=t<<12;i(r)-e>=0;)r^=7973<=0;)n^=1335<0&&(e=this.data.substr(r),n=parseInt(e,10),t.put(n,3*o+1))};var j=q,$=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function X(t){this.mode=K.ALPHANUMERIC,this.data=t}X.getBitsLength=function(t){return 11*Math.floor(t/2)+t%2*6},X.prototype.getLength=function(){return this.data.length},X.prototype.getBitsLength=function(){return X.getBitsLength(this.data.length)},X.prototype.write=function(t){var r;for(r=0;r+2<=this.data.length;r+=2){var e=45*$.indexOf(this.data[r]);e+=$.indexOf(this.data[r+1]),t.put(e,11)}this.data.length%2&&t.put($.indexOf(this.data[r]),6)};var Z=X;function W(t){this.mode=K.BYTE,"string"==typeof t&&(t=function(t){for(var r=[],e=t.length,n=0;n=55296&&o<=56319&&e>n+1){var a=t.charCodeAt(n+1);a>=56320&&a<=57343&&(o=1024*(o-55296)+a-56320+65536,n+=1)}o<128?r.push(o):o<2048?(r.push(o>>6|192),r.push(63&o|128)):o<55296||o>=57344&&o<65536?(r.push(o>>12|224),r.push(o>>6&63|128),r.push(63&o|128)):o>=65536&&o<=1114111?(r.push(o>>18|240),r.push(o>>12&63|128),r.push(o>>6&63|128),r.push(63&o|128)):r.push(239,191,189)}return new Uint8Array(r).buffer}(t)),this.data=new Uint8Array(t)}W.getBitsLength=function(t){return 8*t},W.prototype.getLength=function(){return this.data.length},W.prototype.getBitsLength=function(){return W.getBitsLength(this.data.length)},W.prototype.write=function(t){for(var r=0,e=this.data.length;r=33088&&e<=40956)e-=33088;else{if(!(e>=57408&&e<=60351))throw new Error("Invalid SJIS character: "+this.data[r]+"\nMake sure your charset is UTF-8");e-=49472}e=192*(e>>>8&255)+(255&e),t.put(e,13)}};var rt=tt,et=h((function(t){var r={single_source_shortest_paths:function(t,e,n){var o={},a={};a[e]=0;var i,u,s,f,h,c,g,d=r.PriorityQueue.make();for(d.push(e,0);!d.empty();)for(s in u=(i=d.pop()).value,f=i.cost,h=t[u]||{})h.hasOwnProperty(s)&&(c=f+h[s],g=a[s],(void 0===a[s]||g>c)&&(a[s]=c,d.push(s,c),o[s]=u));if(void 0!==n&&void 0===a[n]){var l=["Could not find a path from ",e," to ",n,"."].join("");throw new Error(l)}return o},extract_shortest_path_from_predecessor_list:function(t,r){for(var e=[],n=r;n;)e.push(n),n=t[n];return e.reverse(),e},find_path:function(t,e,n){var o=r.single_source_shortest_paths(t,e,n);return r.extract_shortest_path_from_predecessor_list(o,n)},PriorityQueue:{make:function(t){var e,n=r.PriorityQueue,o={};for(e in t=t||{},n)n.hasOwnProperty(e)&&(o[e]=n[e]);return o.queue=[],o.sorter=t.sorter||n.default_sorter,o},default_sorter:function(t,r){return t.cost-r.cost},push:function(t,r){var e={value:t,cost:r};this.queue.push(e),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return 0===this.queue.length}}};t.exports=r})),nt=h((function(t,r){function e(t){return unescape(encodeURIComponent(t)).length}function n(t,r,e){for(var n,o=[];null!==(n=t.exec(e));)o.push({data:n[0],index:n.index,mode:r,length:n[0].length});return o}function o(t){var r,e,o=n(J.NUMERIC,K.NUMERIC,t),a=n(J.ALPHANUMERIC,K.ALPHANUMERIC,t);return s()?(r=n(J.BYTE,K.BYTE,t),e=n(J.KANJI,K.KANJI,t)):(r=n(J.BYTE_KANJI,K.BYTE,t),e=[]),o.concat(a,r,e).sort((function(t,r){return t.index-r.index})).map((function(t){return{data:t.data,mode:t.mode,length:t.length}}))}function a(t,r){switch(r){case K.NUMERIC:return j.getBitsLength(t);case K.ALPHANUMERIC:return Z.getBitsLength(t);case K.KANJI:return rt.getBitsLength(t);case K.BYTE:return G.getBitsLength(t)}}function i(t,r){var e,n=K.getBestModeForData(t);if((e=K.from(r,n))!==K.BYTE&&e.bit=0?t[t.length-1]:null;return e&&e.mode===r.mode?(t[t.length-1].data+=r.data,t):(t.push(r),t)}),[])}(s))},r.rawSplit=function(t){return r.fromArray(o(t))}}));function ot(t,r,e){var n,o,a=t.size,i=V(r,e);for(n=0;n<15;n++)o=1==(i>>n&1),n<6?t.set(n,8,o,!0):n<8?t.set(n+1,8,o,!0):t.set(a-15+n,8,o,!0),n<8?t.set(8,a-n-1,o,!0):n<9?t.set(8,15-n-1+1,o,!0):t.set(8,15-n-1,o,!0);t.set(a-8,8,1,!0)}function at(t,r,e){var n=new d;e.forEach((function(r){n.put(r.mode.bit,4),n.put(r.getLength(),K.getCharCountIndicator(r.mode,t)),r.write(n)}));var o=8*(a(t)-M(t,r));for(n.getLengthInBits()+4<=o&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(0);for(var i=(o-n.getLengthInBits())/8,u=0;u=0&&u<=6&&(0===s||6===s)||s>=0&&s<=6&&(0===u||6===u)||u>=2&&u<=4&&s>=2&&s<=4?t.set(a+u,i+s,!0,!0):t.set(a+u,i+s,!1,!0))}(c,r),function(t){for(var r=t.size,e=8;e=7&&function(t,r){for(var e,n,o,a=t.size,i=O.getEncodedBits(r),u=0;u<18;u++)e=Math.floor(u/3),n=u%3+a-8-3,o=1==(i>>u&1),t.set(e,n,o,!0),t.set(n,e,o,!0)}(c,r),function(t,r){for(var e=t.size,n=-1,o=e-1,a=7,i=0,u=e-1;u>0;u-=2)for(6===u&&u--;;){for(var s=0;s<2;s++)if(!t.isReserved(o,u-s)){var f=!1;i>>a&1)),t.set(o,u-s,f),-1===--a&&(i++,a=7)}if((o+=n)<0||e<=o){o-=n,n=-n;break}}}(c,f),isNaN(n)&&(n=E.getBestMask(c,ot.bind(null,c,e))),E.applyMask(n,c),ot(c,e,n),{modules:c,version:r,errorCorrectionLevel:e,maskPattern:n,segments:a}}nt.fromArray,nt.fromString,nt.rawSplit;var ut=function(t,r){if(void 0===t||""===t)throw new Error("No input text");var e,n,o=c.M;return void 0!==r&&(o=c.from(r.errorCorrectionLevel,c.M),e=O.from(r.version),n=E.from(r.maskPattern),r.toSJISFunc&&u(r.toSJISFunc)),it(t,e,o,n)},st=h((function(t,r){function e(t){if("number"==typeof t&&(t=t.toString()),"string"!=typeof t)throw new Error("Color should be defined as hex string");var r=t.slice().replace("#","").split("");if(r.length<3||5===r.length||r.length>8)throw new Error("Invalid hex color: "+t);3!==r.length&&4!==r.length||(r=Array.prototype.concat.apply([],r.map((function(t){return[t,t]})))),6===r.length&&r.push("F","F");var e=parseInt(r.join(""),16);return{r:e>>24&255,g:e>>16&255,b:e>>8&255,a:255&e,hex:"#"+r.slice(0,6).join("")}}r.getOptions=function(t){t||(t={}),t.color||(t.color={});var r=void 0===t.margin||null===t.margin||t.margin<0?4:t.margin,n=t.width&&t.width>=21?t.width:void 0,o=t.scale||4;return{width:n,scale:n?4:o,margin:r,color:{dark:e(t.color.dark||"#000000ff"),light:e(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},r.getScale=function(t,r){return r.width&&r.width>=t+2*r.margin?r.width/(t+2*r.margin):r.scale},r.getImageWidth=function(t,e){var n=r.getScale(t,e);return Math.floor((t+2*e.margin)*n)},r.qrToImageData=function(t,e,n){for(var o=e.modules.size,a=e.modules.data,i=r.getScale(o,n),u=Math.floor((o+2*n.margin)*i),s=n.margin*i,f=[n.color.light,n.color.dark],h=0;h=s&&c>=s&&h':"",s="0&&s>0&&t[u-1]||(n+=a?ct("M",s+e,.5+f+e):ct("m",o,0),o=0,a=!1),s+1',f='viewBox="0 0 '+i+" "+i+'"',h=''+u+s+"\n";return"function"==typeof e&&e(null,h),h};function dt(t,r,n,o,a){var i=[].slice.call(arguments,1),u=i.length,s="function"==typeof i[u-1];if(!s&&!e())throw new Error("Callback required as last argument");if(!s){if(u<1)throw new Error("Too few arguments provided");return 1===u?(n=r,r=o=void 0):2!==u||r.getContext||(o=n,n=r,r=void 0),new Promise((function(e,a){try{var i=ut(n,o);e(t(i,r,o))}catch(t){a(t)}}))}if(u<2)throw new Error("Too few arguments provided");2===u?(a=n,n=r,r=o=void 0):3===u&&(r.getContext&&void 0===a?(a=o,o=void 0):(a=o,o=n,n=r,r=void 0));try{var f=ut(n,o);a(null,t(f,r,o))}catch(t){a(t)}}var lt=ut,vt=dt.bind(null,ft.render),pt=dt.bind(null,ft.renderToDataURL),wt=dt.bind(null,(function(t,r,e){return gt(t,e)})),mt={create:lt,toCanvas:vt,toDataURL:pt,toString:wt};return t.create=lt,t.default=mt,t.toCanvas=vt,t.toDataURL=pt,t.toString=wt,Object.defineProperty(t,"__esModule",{value:!0}),t}({}); diff --git a/apps/smp-server/static/media/script.js b/apps/smp-server/static/media/script.js new file mode 100644 index 000000000..aa5d35293 --- /dev/null +++ b/apps/smp-server/static/media/script.js @@ -0,0 +1,39 @@ +const isMobile = { + Android: () => navigator.userAgent.match(/Android/i), + iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i) +}; + +window.addEventListener('click', clickHandler) + +if (isMobile.iOS) { + for (const btn of document.getElementsByClassName("close-overlay-btn")) { + btn.addEventListener("touchend", (e) => setTimeout(() => closeOverlay(e), 100)) + } +} + +function clickHandler(e) { + if (e.target.closest('.contact-tab-btn')) { + e.target.closest('.contact-tab').classList.toggle('active') + } +} + +window.addEventListener('load', () => { + const googlePlayBtn = document.querySelector('.google-play-btn'); + const appleStoreBtn = document.querySelector('.apple-store-btn'); + const fDroidBtn = document.querySelector('.f-droid-btn'); + if (!googlePlayBtn || !appleStoreBtn || !fDroidBtn) return; + + + if (isMobile.Android()) { + googlePlayBtn.classList.remove('hidden'); + fDroidBtn.classList.remove('hidden'); + } + else if (isMobile.iOS()) { + appleStoreBtn.classList.remove('hidden'); + } + else { + appleStoreBtn.classList.remove('hidden'); + googlePlayBtn.classList.remove('hidden'); + fDroidBtn.classList.remove('hidden'); + } +}) diff --git a/apps/smp-server/static/media/style.css b/apps/smp-server/static/media/style.css new file mode 100644 index 000000000..d9d2e829e --- /dev/null +++ b/apps/smp-server/static/media/style.css @@ -0,0 +1,410 @@ +@font-face { + font-family: Gilroy; + src: url("GilroyRegular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyLight.woff2") format("woff2"); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyMedium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyBold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: Gilroy; + src: url("GilroyRegularItalic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; +} + +html { + scroll-behavior: smooth; + font-family: Gilroy, Helvetica, sans-serif; + ; + letter-spacing: 0.003em; +} + +img { + user-select: none; + -webkit-user-select: none; + /* For Safari and older Chrome versions */ + -moz-user-select: none; + /* For Firefox */ + -ms-user-select: none; + /* For Internet Explorer and Edge */ +} + +/* NEW SITE */ +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + width: 100%; + /* padding: 0 20px; */ + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + + .container-sm, + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + + .container-md, + .container-sm, + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + + .container-lg, + .container-md, + .container-sm, + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + + .container-xl, + .container-lg, + .container-md, + .container-sm, + .container { + max-width: 1140px; + } +} + +@media (min-width: 1400px) { + + .container-xxl, + .container-xl, + .container-lg, + .container-md, + .container-sm, + .container { + max-width: 1320px; + } +} + +.gradient-text { + background: -webkit-linear-gradient(to bottom, #53C1FF -50%, #0053D0 160%); + background: linear-gradient(to bottom, #53C1FF -50%, #0053D0 160%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-fill-color: transparent; +} + +.dark .border-gradient { + background: + linear-gradient(#11182F, #11182F) padding-box, + linear-gradient(to bottom, transparent, #01F1FF 58%) border-box; + border: 1px solid transparent; +} + +.dark .only-light { + display: none; +} + +.only-dark { + display: none; +} + +.dark .only-dark { + display: inherit; +} + +.menu-link { + font-size: 16px; + line-height: 33.42px; + color: #0D0E12; +} + +.dark .menu-link { + color: #fff; +} + +.nav-link ul li a.active { + color: #0053D0; + +} + +.dark .nav-link ul li a.active { + color: #66D9E2; +} + +@media (min-width:1024px) { + + .nav-link-text, + .menu-link { + display: inline-block; + position: relative; + color: #0D0E12; + } + + .nav-link-text::before, + .active .nav-link-text::before, + .menu-link::before { + content: ""; + position: absolute; + width: 0; + height: 1px; + bottom: 0; + right: 0; + /* background-color: initial; */ + transition: width 0.25s ease-out; + } + + .menu-link::before { + background-color: #0D0E12; + } + + .dark .menu-link::before { + background-color: #fff; + } + + .active .nav-link-text::before { + width: 100%; + } + + .nav-link:hover .nav-link-text::before, + .menu-link:hover::before { + width: 100%; + left: 0; + right: auto; + } +} + + +.sub-menu { + visibility: hidden; + opacity: 0; + color: #505158; +} + +.sub-menu .no-hover { + color: #505158 !important; +} + +.dark .sub-menu, +.dark .sub-menu .no-hover { + color: #fff !important; +} + +.dark .sub-menu li:hover { + color: #66D9E2; +} + +.sub-menu li:hover { + color: #0053D0; +} + +.sub-menu { + transition: all .3s ease !important; +} + +.nav-link span svg, +header nav { + transition: all 0.5s ease; +} + +.nav-link:hover span svg { + transform: rotate(180deg); +} + +@media (min-width:1024px) { + + .nav-link:hover .sub-menu, + .nav-link:focus-within .sub-menu { + visibility: visible; + opacity: 1; + margin-top: 0; + } +} + +@media (max-width: 1024px) { + .sub-menu { + max-height: 0; + transform: translateY(-10px); + transition: all .7s ease !important; + } + + .active .sub-menu { + max-height: 600px; + transform: translateY(0px); + opacity: 1; + visibility: visible; + margin-top: 0; + } + + header nav { + visibility: hidden; + opacity: 0; + transform: translateX(100%); + } + + header nav.open { + visibility: visible; + opacity: 1; + transform: translateX(0); + } +} + +.lock-scroll { + overflow: hidden; +} + +/* hero */ +header { + transition: all .7s ease; +} + +.primary-header { + background: linear-gradient(270deg, #0053D0 35.85%, #0197FF 94.78%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .primary-header { + background: linear-gradient(270deg, #70F0F9 100%, #70F0F9 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.secondary-header { + color: #606c71; + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .secondary-header { + color: #fff; + text-shadow: none; +} + +.description { + width: 31rem; +} + +p a { + color: #0053D0; + text-decoration: underline; + text-underline-offset: 2px; +} + +.dark p a { + color: #70F0F9; +} + +/* For Contact & Invitation Page */ +.primary-header-contact { + background: linear-gradient(251.16deg, #53c1ff 1.1%, #0053d0 100.82%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .primary-header-contact { + background: linear-gradient(270deg, #70F0F9 100%, #70F0F9 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.secondary-header-contact { + text-shadow: 0px 4px 74px #e9e7e2; +} + +.dark .secondary-header-contact { + text-shadow: none; +} + +.content_copy_with_tooltip { + background-color: #f8f8f6; + border-radius: 50px; + padding-bottom: 4px; + padding-top: 8px; + margin-top: 16px; + margin-bottom: 16px; +} + +.content_copy_with_tooltip .tooltip { + vertical-align: -6px; +} + +.content_copy_with_tooltip .content { + font-size: 15px; +} + +.contact-tab>.contact-tab-content, +.job-tab>.job-tab-content { + opacity: 0; + max-height: 0; + transition: all 0.5s ease; + visibility: hidden; + transform: translateY(10px); + overflow: hidden; +} + +.contact-tab svg, +.job-tab svg { + transform: rotate(-180deg); + transition: all .5s ease; +} + +.contact-tab.active>.contact-tab-content, +.job-tab.active>.job-tab-content { + opacity: 1; + max-height: 300px; + visibility: visible; + transform: translateY(0px); +} + +.for-tablet .contact-tab.active>.contact-tab-content, +.for-tablet .job-tab.active>.job-tab-content { + min-height: 450px; +} + +.contact-tab.active svg, +.contact-tab:hover svg, +.job-tab.active svg, +.job-tab:hover svg { + transform: rotate(0deg); +} + +.d-none-if-js-disabled { + display: none !important; +} \ No newline at end of file diff --git a/apps/smp-server/static/media/sun.svg b/apps/smp-server/static/media/sun.svg new file mode 100644 index 000000000..8407b98e5 --- /dev/null +++ b/apps/smp-server/static/media/sun.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/smp-server/static/media/swiper-bundle.min.css b/apps/smp-server/static/media/swiper-bundle.min.css new file mode 100644 index 000000000..916173ac0 --- /dev/null +++ b/apps/smp-server/static/media/swiper-bundle.min.css @@ -0,0 +1,13 @@ +/** + * Swiper 8.4.3 + * Most modern mobile touch slider and framework with hardware accelerated transitions + * https://swiperjs.com + * + * Copyright 2014-2022 Vladimir Kharlampidi + * + * Released under the MIT License + * + * Released on: October 6, 2022 + */ + +@font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;box-sizing:content-box}.swiper-android .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-pointer-events{touch-action:pan-y}.swiper-pointer-events.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d,.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-cube-shadow,.swiper-3d .swiper-slide,.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top,.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-horizontal.swiper-css-mode>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-vertical.swiper-css-mode>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-centered>.swiper-wrapper::before{content:'';flex-shrink:0;order:9999}.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-centered.swiper-horizontal>.swiper-wrapper::before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-centered.swiper-vertical>.swiper-wrapper::before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center}.swiper-virtual .swiper-slide{-webkit-backface-visibility:hidden;transform:translateZ(0)}.swiper-virtual.swiper-css-mode .swiper-wrapper::after{content:'';position:absolute;left:0;top:0;pointer-events:none}.swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after{height:1px;width:var(--swiper-virtual-size)}.swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after{width:1px;height:var(--swiper-virtual-size)}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:50%;width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:initial;line-height:1}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:10px;right:auto}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:10px;left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:'next'}.swiper-button-lock{display:none}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-pagination-disabled>.swiper-pagination,.swiper-pagination.swiper-pagination-disabled{display:none!important}.swiper-horizontal>.swiper-pagination-bullets,.swiper-pagination-bullets.swiper-pagination-horizontal,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:10px;left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:var(--swiper-pagination-bullet-width,var(--swiper-pagination-bullet-size,8px));height:var(--swiper-pagination-bullet-height,var(--swiper-pagination-bullet-size,8px));display:inline-block;border-radius:50%;background:var(--swiper-pagination-bullet-inactive-color,#000);opacity:var(--swiper-pagination-bullet-inactive-opacity, .2)}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:var(--swiper-pagination-bullet-opacity, 1);background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-pagination-vertical.swiper-pagination-bullets,.swiper-vertical>.swiper-pagination-bullets{right:10px;top:50%;transform:translate3d(0px,-50%,0)}.swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:var(--swiper-pagination-bullet-vertical-gap,6px) 0;display:block}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 var(--swiper-pagination-bullet-horizontal-gap,4px)}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-horizontal.swiper-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-progressbar{background:rgba(0,0,0,.25);position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-horizontal>.swiper-pagination-progressbar,.swiper-pagination-progressbar.swiper-pagination-horizontal,.swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite,.swiper-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:4px;left:0;top:0}.swiper-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-vertical,.swiper-vertical>.swiper-pagination-progressbar{width:4px;height:100%;left:0;top:0}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:10px;position:relative;-ms-touch-action:none;background:rgba(0,0,0,.1)}.swiper-scrollbar-disabled>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-disabled{display:none!important}.swiper-horizontal>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-horizontal{position:absolute;left:1%;bottom:3px;z-index:50;height:5px;width:98%}.swiper-scrollbar.swiper-scrollbar-vertical,.swiper-vertical>.swiper-scrollbar{position:absolute;right:3px;top:1%;z-index:50;width:5px;height:98%}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:rgba(0,0,0,.5);border-radius:10px;left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader,.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.swiper .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-grid>.swiper-wrapper{flex-wrap:wrap}.swiper-grid-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-fade.swiper-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-fade .swiper-slide-active,.swiper-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube{overflow:visible}.swiper-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-cube.swiper-rtl .swiper-slide{transform-origin:100% 0}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-next,.swiper-cube .swiper-slide-next+.swiper-slide,.swiper-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-cube .swiper-slide-shadow-bottom,.swiper-cube .swiper-slide-shadow-left,.swiper-cube .swiper-slide-shadow-right,.swiper-cube .swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-flip{overflow:visible}.swiper-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-flip .swiper-slide-active,.swiper-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-flip .swiper-slide-shadow-bottom,.swiper-flip .swiper-slide-shadow-left,.swiper-flip .swiper-slide-shadow-right,.swiper-flip .swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-creative .swiper-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;transition-property:transform,opacity,height}.swiper-cards{overflow:visible}.swiper-cards .swiper-slide{transform-origin:center bottom;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden} \ No newline at end of file diff --git a/apps/smp-server/static/media/swiper-bundle.min.js b/apps/smp-server/static/media/swiper-bundle.min.js new file mode 100644 index 000000000..0c347e3e0 --- /dev/null +++ b/apps/smp-server/static/media/swiper-bundle.min.js @@ -0,0 +1,14 @@ +/** + * Swiper 8.4.3 + * Most modern mobile touch slider and framework with hardware accelerated transitions + * https://swiperjs.com + * + * Copyright 2014-2022 Vladimir Kharlampidi + * + * Released under the MIT License + * + * Released on: October 6, 2022 + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Swiper=t()}(this,(function(){"use strict";function e(e){return null!==e&&"object"==typeof e&&"constructor"in e&&e.constructor===Object}function t(s,a){void 0===s&&(s={}),void 0===a&&(a={}),Object.keys(a).forEach((i=>{void 0===s[i]?s[i]=a[i]:e(a[i])&&e(s[i])&&Object.keys(a[i]).length>0&&t(s[i],a[i])}))}const s={body:{},addEventListener(){},removeEventListener(){},activeElement:{blur(){},nodeName:""},querySelector:()=>null,querySelectorAll:()=>[],getElementById:()=>null,createEvent:()=>({initEvent(){}}),createElement:()=>({children:[],childNodes:[],style:{},setAttribute(){},getElementsByTagName:()=>[]}),createElementNS:()=>({}),importNode:()=>null,location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""}};function a(){const e="undefined"!=typeof document?document:{};return t(e,s),e}const i={document:s,navigator:{userAgent:""},location:{hash:"",host:"",hostname:"",href:"",origin:"",pathname:"",protocol:"",search:""},history:{replaceState(){},pushState(){},go(){},back(){}},CustomEvent:function(){return this},addEventListener(){},removeEventListener(){},getComputedStyle:()=>({getPropertyValue:()=>""}),Image(){},Date(){},screen:{},setTimeout(){},clearTimeout(){},matchMedia:()=>({}),requestAnimationFrame:e=>"undefined"==typeof setTimeout?(e(),null):setTimeout(e,0),cancelAnimationFrame(e){"undefined"!=typeof setTimeout&&clearTimeout(e)}};function r(){const e="undefined"!=typeof window?window:{};return t(e,i),e}class n extends Array{constructor(e){"number"==typeof e?super(e):(super(...e||[]),function(e){const t=e.__proto__;Object.defineProperty(e,"__proto__",{get:()=>t,set(e){t.__proto__=e}})}(this))}}function l(e){void 0===e&&(e=[]);const t=[];return e.forEach((e=>{Array.isArray(e)?t.push(...l(e)):t.push(e)})),t}function o(e,t){return Array.prototype.filter.call(e,t)}function d(e,t){const s=r(),i=a();let l=[];if(!t&&e instanceof n)return e;if(!e)return new n(l);if("string"==typeof e){const s=e.trim();if(s.indexOf("<")>=0&&s.indexOf(">")>=0){let e="div";0===s.indexOf("e.split(" "))));return this.forEach((e=>{e.classList.add(...a)})),this},removeClass:function(){for(var e=arguments.length,t=new Array(e),s=0;se.split(" "))));return this.forEach((e=>{e.classList.remove(...a)})),this},hasClass:function(){for(var e=arguments.length,t=new Array(e),s=0;se.split(" "))));return o(this,(e=>a.filter((t=>e.classList.contains(t))).length>0)).length>0},toggleClass:function(){for(var e=arguments.length,t=new Array(e),s=0;se.split(" "))));this.forEach((e=>{a.forEach((t=>{e.classList.toggle(t)}))}))},attr:function(e,t){if(1===arguments.length&&"string"==typeof e)return this[0]?this[0].getAttribute(e):void 0;for(let s=0;s=0;e-=1){const i=a[e];r&&i.listener===r||r&&i.listener&&i.listener.dom7proxy&&i.listener.dom7proxy===r?(s.removeEventListener(t,i.proxyListener,n),a.splice(e,1)):r||(s.removeEventListener(t,i.proxyListener,n),a.splice(e,1))}}}return this},trigger:function(){const e=r();for(var t=arguments.length,s=new Array(t),a=0;at>0)),i.dispatchEvent(t),i.dom7EventData=[],delete i.dom7EventData}}}return this},transitionEnd:function(e){const t=this;return e&&t.on("transitionend",(function s(a){a.target===this&&(e.call(this,a),t.off("transitionend",s))})),this},outerWidth:function(e){if(this.length>0){if(e){const e=this.styles();return this[0].offsetWidth+parseFloat(e.getPropertyValue("margin-right"))+parseFloat(e.getPropertyValue("margin-left"))}return this[0].offsetWidth}return null},outerHeight:function(e){if(this.length>0){if(e){const e=this.styles();return this[0].offsetHeight+parseFloat(e.getPropertyValue("margin-top"))+parseFloat(e.getPropertyValue("margin-bottom"))}return this[0].offsetHeight}return null},styles:function(){const e=r();return this[0]?e.getComputedStyle(this[0],null):{}},offset:function(){if(this.length>0){const e=r(),t=a(),s=this[0],i=s.getBoundingClientRect(),n=t.body,l=s.clientTop||n.clientTop||0,o=s.clientLeft||n.clientLeft||0,d=s===e?e.scrollY:s.scrollTop,c=s===e?e.scrollX:s.scrollLeft;return{top:i.top+d-l,left:i.left+c-o}}return null},css:function(e,t){const s=r();let a;if(1===arguments.length){if("string"!=typeof e){for(a=0;a{e.apply(t,[t,s])})),this):this},html:function(e){if(void 0===e)return this[0]?this[0].innerHTML:null;for(let t=0;tt-1)return d([]);if(e<0){const s=t+e;return d(s<0?[]:[this[s]])}return d([this[e]])},append:function(){let e;const t=a();for(let s=0;s=0;i-=1)this[s].insertBefore(a.childNodes[i],this[s].childNodes[0])}else if(e instanceof n)for(i=0;i0?e?this[0].nextElementSibling&&d(this[0].nextElementSibling).is(e)?d([this[0].nextElementSibling]):d([]):this[0].nextElementSibling?d([this[0].nextElementSibling]):d([]):d([])},nextAll:function(e){const t=[];let s=this[0];if(!s)return d([]);for(;s.nextElementSibling;){const a=s.nextElementSibling;e?d(a).is(e)&&t.push(a):t.push(a),s=a}return d(t)},prev:function(e){if(this.length>0){const t=this[0];return e?t.previousElementSibling&&d(t.previousElementSibling).is(e)?d([t.previousElementSibling]):d([]):t.previousElementSibling?d([t.previousElementSibling]):d([])}return d([])},prevAll:function(e){const t=[];let s=this[0];if(!s)return d([]);for(;s.previousElementSibling;){const a=s.previousElementSibling;e?d(a).is(e)&&t.push(a):t.push(a),s=a}return d(t)},parent:function(e){const t=[];for(let s=0;s6&&(i=i.split(", ").map((e=>e.replace(",","."))).join(", ")),n=new s.WebKitCSSMatrix("none"===i?"":i)):(n=l.MozTransform||l.OTransform||l.MsTransform||l.msTransform||l.transform||l.getPropertyValue("transform").replace("translate(","matrix(1, 0, 0, 1,"),a=n.toString().split(",")),"x"===t&&(i=s.WebKitCSSMatrix?n.m41:16===a.length?parseFloat(a[12]):parseFloat(a[4])),"y"===t&&(i=s.WebKitCSSMatrix?n.m42:16===a.length?parseFloat(a[13]):parseFloat(a[5])),i||0}function m(e){return"object"==typeof e&&null!==e&&e.constructor&&"Object"===Object.prototype.toString.call(e).slice(8,-1)}function f(e){return"undefined"!=typeof window&&void 0!==window.HTMLElement?e instanceof HTMLElement:e&&(1===e.nodeType||11===e.nodeType)}function g(){const e=Object(arguments.length<=0?void 0:arguments[0]),t=["__proto__","constructor","prototype"];for(let s=1;st.indexOf(e)<0));for(let t=0,i=s.length;tn?"next":"prev",p=(e,t)=>"next"===c&&e>=t||"prev"===c&&e<=t,u=()=>{l=(new Date).getTime(),null===o&&(o=l);const e=Math.max(Math.min((l-o)/d,1),0),r=.5-Math.cos(e*Math.PI)/2;let c=n+r*(s-n);if(p(c,s)&&(c=s),t.wrapperEl.scrollTo({[a]:c}),p(c,s))return t.wrapperEl.style.overflow="hidden",t.wrapperEl.style.scrollSnapType="",setTimeout((()=>{t.wrapperEl.style.overflow="",t.wrapperEl.scrollTo({[a]:c})})),void i.cancelAnimationFrame(t.cssModeFrameID);t.cssModeFrameID=i.requestAnimationFrame(u)};u()}let b,x,y;function E(){return b||(b=function(){const e=r(),t=a();return{smoothScroll:t.documentElement&&"scrollBehavior"in t.documentElement.style,touch:!!("ontouchstart"in e||e.DocumentTouch&&t instanceof e.DocumentTouch),passiveListener:function(){let t=!1;try{const s=Object.defineProperty({},"passive",{get(){t=!0}});e.addEventListener("testPassiveListener",null,s)}catch(e){}return t}(),gestures:"ongesturestart"in e}}()),b}function C(e){return void 0===e&&(e={}),x||(x=function(e){let{userAgent:t}=void 0===e?{}:e;const s=E(),a=r(),i=a.navigator.platform,n=t||a.navigator.userAgent,l={ios:!1,android:!1},o=a.screen.width,d=a.screen.height,c=n.match(/(Android);?[\s\/]+([\d.]+)?/);let p=n.match(/(iPad).*OS\s([\d_]+)/);const u=n.match(/(iPod)(.*OS\s([\d_]+))?/),h=!p&&n.match(/(iPhone\sOS|iOS)\s([\d_]+)/),m="Win32"===i;let f="MacIntel"===i;return!p&&f&&s.touch&&["1024x1366","1366x1024","834x1194","1194x834","834x1112","1112x834","768x1024","1024x768","820x1180","1180x820","810x1080","1080x810"].indexOf(`${o}x${d}`)>=0&&(p=n.match(/(Version)\/([\d.]+)/),p||(p=[0,1,"13_0_0"]),f=!1),c&&!m&&(l.os="android",l.android=!0),(p||h||u)&&(l.os="ios",l.ios=!0),l}(e)),x}function T(){return y||(y=function(){const e=r();return{isSafari:function(){const t=e.navigator.userAgent.toLowerCase();return t.indexOf("safari")>=0&&t.indexOf("chrome")<0&&t.indexOf("android")<0}(),isWebView:/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(e.navigator.userAgent)}}()),y}Object.keys(c).forEach((e=>{Object.defineProperty(d.fn,e,{value:c[e],writable:!0})}));var $={on(e,t,s){const a=this;if(!a.eventsListeners||a.destroyed)return a;if("function"!=typeof t)return a;const i=s?"unshift":"push";return e.split(" ").forEach((e=>{a.eventsListeners[e]||(a.eventsListeners[e]=[]),a.eventsListeners[e][i](t)})),a},once(e,t,s){const a=this;if(!a.eventsListeners||a.destroyed)return a;if("function"!=typeof t)return a;function i(){a.off(e,i),i.__emitterProxy&&delete i.__emitterProxy;for(var s=arguments.length,r=new Array(s),n=0;n=0&&t.eventsAnyListeners.splice(s,1),t},off(e,t){const s=this;return!s.eventsListeners||s.destroyed?s:s.eventsListeners?(e.split(" ").forEach((e=>{void 0===t?s.eventsListeners[e]=[]:s.eventsListeners[e]&&s.eventsListeners[e].forEach(((a,i)=>{(a===t||a.__emitterProxy&&a.__emitterProxy===t)&&s.eventsListeners[e].splice(i,1)}))})),s):s},emit(){const e=this;if(!e.eventsListeners||e.destroyed)return e;if(!e.eventsListeners)return e;let t,s,a;for(var i=arguments.length,r=new Array(i),n=0;n{e.eventsAnyListeners&&e.eventsAnyListeners.length&&e.eventsAnyListeners.forEach((e=>{e.apply(a,[t,...s])})),e.eventsListeners&&e.eventsListeners[t]&&e.eventsListeners[t].forEach((e=>{e.apply(a,s)}))})),e}};var S={updateSize:function(){const e=this;let t,s;const a=e.$el;t=void 0!==e.params.width&&null!==e.params.width?e.params.width:a[0].clientWidth,s=void 0!==e.params.height&&null!==e.params.height?e.params.height:a[0].clientHeight,0===t&&e.isHorizontal()||0===s&&e.isVertical()||(t=t-parseInt(a.css("padding-left")||0,10)-parseInt(a.css("padding-right")||0,10),s=s-parseInt(a.css("padding-top")||0,10)-parseInt(a.css("padding-bottom")||0,10),Number.isNaN(t)&&(t=0),Number.isNaN(s)&&(s=0),Object.assign(e,{width:t,height:s,size:e.isHorizontal()?t:s}))},updateSlides:function(){const e=this;function t(t){return e.isHorizontal()?t:{width:"height","margin-top":"margin-left","margin-bottom ":"margin-right","margin-left":"margin-top","margin-right":"margin-bottom","padding-left":"padding-top","padding-right":"padding-bottom",marginRight:"marginBottom"}[t]}function s(e,s){return parseFloat(e.getPropertyValue(t(s))||0)}const a=e.params,{$wrapperEl:i,size:r,rtlTranslate:n,wrongRTL:l}=e,o=e.virtual&&a.virtual.enabled,d=o?e.virtual.slides.length:e.slides.length,c=i.children(`.${e.params.slideClass}`),p=o?e.virtual.slides.length:c.length;let u=[];const h=[],m=[];let f=a.slidesOffsetBefore;"function"==typeof f&&(f=a.slidesOffsetBefore.call(e));let g=a.slidesOffsetAfter;"function"==typeof g&&(g=a.slidesOffsetAfter.call(e));const w=e.snapGrid.length,b=e.slidesGrid.length;let x=a.spaceBetween,y=-f,E=0,C=0;if(void 0===r)return;"string"==typeof x&&x.indexOf("%")>=0&&(x=parseFloat(x.replace("%",""))/100*r),e.virtualSize=-x,n?c.css({marginLeft:"",marginBottom:"",marginTop:""}):c.css({marginRight:"",marginBottom:"",marginTop:""}),a.centeredSlides&&a.cssMode&&(v(e.wrapperEl,"--swiper-centered-offset-before",""),v(e.wrapperEl,"--swiper-centered-offset-after",""));const T=a.grid&&a.grid.rows>1&&e.grid;let $;T&&e.grid.initSlides(p);const S="auto"===a.slidesPerView&&a.breakpoints&&Object.keys(a.breakpoints).filter((e=>void 0!==a.breakpoints[e].slidesPerView)).length>0;for(let i=0;i1&&u.push(e.virtualSize-r)}if(0===u.length&&(u=[0]),0!==a.spaceBetween){const s=e.isHorizontal()&&n?"marginLeft":t("marginRight");c.filter(((e,t)=>!a.cssMode||t!==c.length-1)).css({[s]:`${x}px`})}if(a.centeredSlides&&a.centeredSlidesBounds){let e=0;m.forEach((t=>{e+=t+(a.spaceBetween?a.spaceBetween:0)})),e-=a.spaceBetween;const t=e-r;u=u.map((e=>e<0?-f:e>t?t+g:e))}if(a.centerInsufficientSlides){let e=0;if(m.forEach((t=>{e+=t+(a.spaceBetween?a.spaceBetween:0)})),e-=a.spaceBetween,e{u[s]=e-t})),h.forEach(((e,s)=>{h[s]=e+t}))}}if(Object.assign(e,{slides:c,snapGrid:u,slidesGrid:h,slidesSizesGrid:m}),a.centeredSlides&&a.cssMode&&!a.centeredSlidesBounds){v(e.wrapperEl,"--swiper-centered-offset-before",-u[0]+"px"),v(e.wrapperEl,"--swiper-centered-offset-after",e.size/2-m[m.length-1]/2+"px");const t=-e.snapGrid[0],s=-e.slidesGrid[0];e.snapGrid=e.snapGrid.map((e=>e+t)),e.slidesGrid=e.slidesGrid.map((e=>e+s))}if(p!==d&&e.emit("slidesLengthChange"),u.length!==w&&(e.params.watchOverflow&&e.checkOverflow(),e.emit("snapGridLengthChange")),h.length!==b&&e.emit("slidesGridLengthChange"),a.watchSlidesProgress&&e.updateSlidesOffset(),!(o||a.cssMode||"slide"!==a.effect&&"fade"!==a.effect)){const t=`${a.containerModifierClass}backface-hidden`,s=e.$el.hasClass(t);p<=a.maxBackfaceHiddenSlides?s||e.$el.addClass(t):s&&e.$el.removeClass(t)}},updateAutoHeight:function(e){const t=this,s=[],a=t.virtual&&t.params.virtual.enabled;let i,r=0;"number"==typeof e?t.setTransition(e):!0===e&&t.setTransition(t.params.speed);const n=e=>a?t.slides.filter((t=>parseInt(t.getAttribute("data-swiper-slide-index"),10)===e))[0]:t.slides.eq(e)[0];if("auto"!==t.params.slidesPerView&&t.params.slidesPerView>1)if(t.params.centeredSlides)(t.visibleSlides||d([])).each((e=>{s.push(e)}));else for(i=0;it.slides.length&&!a)break;s.push(n(e))}else s.push(n(t.activeIndex));for(i=0;ir?e:r}(r||0===r)&&t.$wrapperEl.css("height",`${r}px`)},updateSlidesOffset:function(){const e=this,t=e.slides;for(let s=0;s=0&&p1&&u<=t.size||p<=0&&u>=t.size)&&(t.visibleSlides.push(l),t.visibleSlidesIndexes.push(e),a.eq(e).addClass(s.slideVisibleClass)),l.progress=i?-d:d,l.originalProgress=i?-c:c}t.visibleSlides=d(t.visibleSlides)},updateProgress:function(e){const t=this;if(void 0===e){const s=t.rtlTranslate?-1:1;e=t&&t.translate&&t.translate*s||0}const s=t.params,a=t.maxTranslate()-t.minTranslate();let{progress:i,isBeginning:r,isEnd:n}=t;const l=r,o=n;0===a?(i=0,r=!0,n=!0):(i=(e-t.minTranslate())/a,r=i<=0,n=i>=1),Object.assign(t,{progress:i,isBeginning:r,isEnd:n}),(s.watchSlidesProgress||s.centeredSlides&&s.autoHeight)&&t.updateSlidesProgress(e),r&&!l&&t.emit("reachBeginning toEdge"),n&&!o&&t.emit("reachEnd toEdge"),(l&&!r||o&&!n)&&t.emit("fromEdge"),t.emit("progress",i)},updateSlidesClasses:function(){const e=this,{slides:t,params:s,$wrapperEl:a,activeIndex:i,realIndex:r}=e,n=e.virtual&&s.virtual.enabled;let l;t.removeClass(`${s.slideActiveClass} ${s.slideNextClass} ${s.slidePrevClass} ${s.slideDuplicateActiveClass} ${s.slideDuplicateNextClass} ${s.slideDuplicatePrevClass}`),l=n?e.$wrapperEl.find(`.${s.slideClass}[data-swiper-slide-index="${i}"]`):t.eq(i),l.addClass(s.slideActiveClass),s.loop&&(l.hasClass(s.slideDuplicateClass)?a.children(`.${s.slideClass}:not(.${s.slideDuplicateClass})[data-swiper-slide-index="${r}"]`).addClass(s.slideDuplicateActiveClass):a.children(`.${s.slideClass}.${s.slideDuplicateClass}[data-swiper-slide-index="${r}"]`).addClass(s.slideDuplicateActiveClass));let o=l.nextAll(`.${s.slideClass}`).eq(0).addClass(s.slideNextClass);s.loop&&0===o.length&&(o=t.eq(0),o.addClass(s.slideNextClass));let d=l.prevAll(`.${s.slideClass}`).eq(0).addClass(s.slidePrevClass);s.loop&&0===d.length&&(d=t.eq(-1),d.addClass(s.slidePrevClass)),s.loop&&(o.hasClass(s.slideDuplicateClass)?a.children(`.${s.slideClass}:not(.${s.slideDuplicateClass})[data-swiper-slide-index="${o.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicateNextClass):a.children(`.${s.slideClass}.${s.slideDuplicateClass}[data-swiper-slide-index="${o.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicateNextClass),d.hasClass(s.slideDuplicateClass)?a.children(`.${s.slideClass}:not(.${s.slideDuplicateClass})[data-swiper-slide-index="${d.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicatePrevClass):a.children(`.${s.slideClass}.${s.slideDuplicateClass}[data-swiper-slide-index="${d.attr("data-swiper-slide-index")}"]`).addClass(s.slideDuplicatePrevClass)),e.emitSlidesClasses()},updateActiveIndex:function(e){const t=this,s=t.rtlTranslate?t.translate:-t.translate,{slidesGrid:a,snapGrid:i,params:r,activeIndex:n,realIndex:l,snapIndex:o}=t;let d,c=e;if(void 0===c){for(let e=0;e=a[e]&&s=a[e]&&s=a[e]&&(c=e);r.normalizeSlideIndex&&(c<0||void 0===c)&&(c=0)}if(i.indexOf(s)>=0)d=i.indexOf(s);else{const e=Math.min(r.slidesPerGroupSkip,c);d=e+Math.floor((c-e)/r.slidesPerGroup)}if(d>=i.length&&(d=i.length-1),c===n)return void(d!==o&&(t.snapIndex=d,t.emit("snapIndexChange")));const p=parseInt(t.slides.eq(c).attr("data-swiper-slide-index")||c,10);Object.assign(t,{snapIndex:d,realIndex:p,previousIndex:n,activeIndex:c}),t.emit("activeIndexChange"),t.emit("snapIndexChange"),l!==p&&t.emit("realIndexChange"),(t.initialized||t.params.runCallbacksOnInit)&&t.emit("slideChange")},updateClickedSlide:function(e){const t=this,s=t.params,a=d(e).closest(`.${s.slideClass}`)[0];let i,r=!1;if(a)for(let e=0;eo?o:a&&en?"next":r=o.length&&(g=o.length-1);const v=-o[g];if(l.normalizeSlideIndex)for(let e=0;e=s&&t=s&&t=s&&(n=e)}if(r.initialized&&n!==p){if(!r.allowSlideNext&&vr.translate&&v>r.maxTranslate()&&(p||0)!==n)return!1}let b;if(n!==(c||0)&&s&&r.emit("beforeSlideChangeStart"),r.updateProgress(v),b=n>p?"next":n{r.wrapperEl.style.scrollSnapType="",r._swiperImmediateVirtual=!1}))}else{if(!r.support.smoothScroll)return w({swiper:r,targetPosition:s,side:e?"left":"top"}),!0;h.scrollTo({[e?"left":"top"]:s,behavior:"smooth"})}return!0}return r.setTransition(t),r.setTranslate(v),r.updateActiveIndex(n),r.updateSlidesClasses(),r.emit("beforeTransitionStart",t,a),r.transitionStart(s,b),0===t?r.transitionEnd(s,b):r.animating||(r.animating=!0,r.onSlideToWrapperTransitionEnd||(r.onSlideToWrapperTransitionEnd=function(e){r&&!r.destroyed&&e.target===this&&(r.$wrapperEl[0].removeEventListener("transitionend",r.onSlideToWrapperTransitionEnd),r.$wrapperEl[0].removeEventListener("webkitTransitionEnd",r.onSlideToWrapperTransitionEnd),r.onSlideToWrapperTransitionEnd=null,delete r.onSlideToWrapperTransitionEnd,r.transitionEnd(s,b))}),r.$wrapperEl[0].addEventListener("transitionend",r.onSlideToWrapperTransitionEnd),r.$wrapperEl[0].addEventListener("webkitTransitionEnd",r.onSlideToWrapperTransitionEnd)),!0},slideToLoop:function(e,t,s,a){if(void 0===e&&(e=0),void 0===t&&(t=this.params.speed),void 0===s&&(s=!0),"string"==typeof e){const t=parseInt(e,10);if(!isFinite(t))throw new Error(`The passed-in 'index' (string) couldn't be converted to 'number'. [${e}] given.`);e=t}const i=this;let r=e;return i.params.loop&&(r+=i.loopedSlides),i.slideTo(r,t,s,a)},slideNext:function(e,t,s){void 0===e&&(e=this.params.speed),void 0===t&&(t=!0);const a=this,{animating:i,enabled:r,params:n}=a;if(!r)return a;let l=n.slidesPerGroup;"auto"===n.slidesPerView&&1===n.slidesPerGroup&&n.slidesPerGroupAuto&&(l=Math.max(a.slidesPerViewDynamic("current",!0),1));const o=a.activeIndexc(e)));let h=n[u.indexOf(p)-1];if(void 0===h&&i.cssMode){let e;n.forEach(((t,s)=>{p>=t&&(e=s)})),void 0!==e&&(h=n[e>0?e-1:e])}let m=0;if(void 0!==h&&(m=l.indexOf(h),m<0&&(m=a.activeIndex-1),"auto"===i.slidesPerView&&1===i.slidesPerGroup&&i.slidesPerGroupAuto&&(m=m-a.slidesPerViewDynamic("previous",!0)+1,m=Math.max(m,0))),i.rewind&&a.isBeginning){const i=a.params.virtual&&a.params.virtual.enabled&&a.virtual?a.virtual.slides.length-1:a.slides.length-1;return a.slideTo(i,e,t,s)}return a.slideTo(m,e,t,s)},slideReset:function(e,t,s){return void 0===e&&(e=this.params.speed),void 0===t&&(t=!0),this.slideTo(this.activeIndex,e,t,s)},slideToClosest:function(e,t,s,a){void 0===e&&(e=this.params.speed),void 0===t&&(t=!0),void 0===a&&(a=.5);const i=this;let r=i.activeIndex;const n=Math.min(i.params.slidesPerGroupSkip,r),l=n+Math.floor((r-n)/i.params.slidesPerGroup),o=i.rtlTranslate?i.translate:-i.translate;if(o>=i.snapGrid[l]){const e=i.snapGrid[l];o-e>(i.snapGrid[l+1]-e)*a&&(r+=i.params.slidesPerGroup)}else{const e=i.snapGrid[l-1];o-e<=(i.snapGrid[l]-e)*a&&(r-=i.params.slidesPerGroup)}return r=Math.max(r,0),r=Math.min(r,i.slidesGrid.length-1),i.slideTo(r,e,t,s)},slideToClickedSlide:function(){const e=this,{params:t,$wrapperEl:s}=e,a="auto"===t.slidesPerView?e.slidesPerViewDynamic():t.slidesPerView;let i,r=e.clickedIndex;if(t.loop){if(e.animating)return;i=parseInt(d(e.clickedSlide).attr("data-swiper-slide-index"),10),t.centeredSlides?re.slides.length-e.loopedSlides+a/2?(e.loopFix(),r=s.children(`.${t.slideClass}[data-swiper-slide-index="${i}"]:not(.${t.slideDuplicateClass})`).eq(0).index(),p((()=>{e.slideTo(r)}))):e.slideTo(r):r>e.slides.length-a?(e.loopFix(),r=s.children(`.${t.slideClass}[data-swiper-slide-index="${i}"]:not(.${t.slideDuplicateClass})`).eq(0).index(),p((()=>{e.slideTo(r)}))):e.slideTo(r)}else e.slideTo(r)}};var z={loopCreate:function(){const e=this,t=a(),{params:s,$wrapperEl:i}=e,r=i.children().length>0?d(i.children()[0].parentNode):i;r.children(`.${s.slideClass}.${s.slideDuplicateClass}`).remove();let n=r.children(`.${s.slideClass}`);if(s.loopFillGroupWithBlank){const e=s.slidesPerGroup-n.length%s.slidesPerGroup;if(e!==s.slidesPerGroup){for(let a=0;an.length&&e.params.loopedSlidesLimit&&(e.loopedSlides=n.length);const l=[],o=[];n.each(((e,t)=>{d(e).attr("data-swiper-slide-index",t)}));for(let t=0;t=0;e-=1)r.prepend(d(l[e].cloneNode(!0)).addClass(s.slideDuplicateClass))},loopFix:function(){const e=this;e.emit("beforeLoopFix");const{activeIndex:t,slides:s,loopedSlides:a,allowSlidePrev:i,allowSlideNext:r,snapGrid:n,rtlTranslate:l}=e;let o;e.allowSlidePrev=!0,e.allowSlideNext=!0;const d=-n[t]-e.getTranslate();if(t=s.length-a){o=-s.length+t+a,o+=a;e.slideTo(o,0,!1,!0)&&0!==d&&e.setTranslate((l?-e.translate:e.translate)-d)}e.allowSlidePrev=i,e.allowSlideNext=r,e.emit("loopFix")},loopDestroy:function(){const{$wrapperEl:e,params:t,slides:s}=this;e.children(`.${t.slideClass}.${t.slideDuplicateClass},.${t.slideClass}.${t.slideBlankClass}`).remove(),s.removeAttr("data-swiper-slide-index")}};function L(e){const t=this,s=a(),i=r(),n=t.touchEventsData,{params:l,touches:o,enabled:c}=t;if(!c)return;if(t.animating&&l.preventInteractionOnTransition)return;!t.animating&&l.cssMode&&l.loop&&t.loopFix();let p=e;p.originalEvent&&(p=p.originalEvent);let h=d(p.target);if("wrapper"===l.touchEventsTarget&&!h.closest(t.wrapperEl).length)return;if(n.isTouchEvent="touchstart"===p.type,!n.isTouchEvent&&"which"in p&&3===p.which)return;if(!n.isTouchEvent&&"button"in p&&p.button>0)return;if(n.isTouched&&n.isMoved)return;const m=!!l.noSwipingClass&&""!==l.noSwipingClass,f=e.composedPath?e.composedPath():e.path;m&&p.target&&p.target.shadowRoot&&f&&(h=d(f[0]));const g=l.noSwipingSelector?l.noSwipingSelector:`.${l.noSwipingClass}`,v=!(!p.target||!p.target.shadowRoot);if(l.noSwiping&&(v?function(e,t){return void 0===t&&(t=this),function t(s){if(!s||s===a()||s===r())return null;s.assignedSlot&&(s=s.assignedSlot);const i=s.closest(e);return i||s.getRootNode?i||t(s.getRootNode().host):null}(t)}(g,h[0]):h.closest(g)[0]))return void(t.allowClick=!0);if(l.swipeHandler&&!h.closest(l.swipeHandler)[0])return;o.currentX="touchstart"===p.type?p.targetTouches[0].pageX:p.pageX,o.currentY="touchstart"===p.type?p.targetTouches[0].pageY:p.pageY;const w=o.currentX,b=o.currentY,x=l.edgeSwipeDetection||l.iOSEdgeSwipeDetection,y=l.edgeSwipeThreshold||l.iOSEdgeSwipeThreshold;if(x&&(w<=y||w>=i.innerWidth-y)){if("prevent"!==x)return;e.preventDefault()}if(Object.assign(n,{isTouched:!0,isMoved:!1,allowTouchCallbacks:!0,isScrolling:void 0,startMoving:void 0}),o.startX=w,o.startY=b,n.touchStartTime=u(),t.allowClick=!0,t.updateSize(),t.swipeDirection=void 0,l.threshold>0&&(n.allowThresholdMove=!1),"touchstart"!==p.type){let e=!0;h.is(n.focusableElements)&&(e=!1,"SELECT"===h[0].nodeName&&(n.isTouched=!1)),s.activeElement&&d(s.activeElement).is(n.focusableElements)&&s.activeElement!==h[0]&&s.activeElement.blur();const a=e&&t.allowTouchMove&&l.touchStartPreventDefault;!l.touchStartForcePreventDefault&&!a||h[0].isContentEditable||p.preventDefault()}t.params.freeMode&&t.params.freeMode.enabled&&t.freeMode&&t.animating&&!l.cssMode&&t.freeMode.onTouchStart(),t.emit("touchStart",p)}function O(e){const t=a(),s=this,i=s.touchEventsData,{params:r,touches:n,rtlTranslate:l,enabled:o}=s;if(!o)return;let c=e;if(c.originalEvent&&(c=c.originalEvent),!i.isTouched)return void(i.startMoving&&i.isScrolling&&s.emit("touchMoveOpposite",c));if(i.isTouchEvent&&"touchmove"!==c.type)return;const p="touchmove"===c.type&&c.targetTouches&&(c.targetTouches[0]||c.changedTouches[0]),h="touchmove"===c.type?p.pageX:c.pageX,m="touchmove"===c.type?p.pageY:c.pageY;if(c.preventedByNestedSwiper)return n.startX=h,void(n.startY=m);if(!s.allowTouchMove)return d(c.target).is(i.focusableElements)||(s.allowClick=!1),void(i.isTouched&&(Object.assign(n,{startX:h,startY:m,currentX:h,currentY:m}),i.touchStartTime=u()));if(i.isTouchEvent&&r.touchReleaseOnEdges&&!r.loop)if(s.isVertical()){if(mn.startY&&s.translate>=s.minTranslate())return i.isTouched=!1,void(i.isMoved=!1)}else if(hn.startX&&s.translate>=s.minTranslate())return;if(i.isTouchEvent&&t.activeElement&&c.target===t.activeElement&&d(c.target).is(i.focusableElements))return i.isMoved=!0,void(s.allowClick=!1);if(i.allowTouchCallbacks&&s.emit("touchMove",c),c.targetTouches&&c.targetTouches.length>1)return;n.currentX=h,n.currentY=m;const f=n.currentX-n.startX,g=n.currentY-n.startY;if(s.params.threshold&&Math.sqrt(f**2+g**2)=25&&(e=180*Math.atan2(Math.abs(g),Math.abs(f))/Math.PI,i.isScrolling=s.isHorizontal()?e>r.touchAngle:90-e>r.touchAngle)}if(i.isScrolling&&s.emit("touchMoveOpposite",c),void 0===i.startMoving&&(n.currentX===n.startX&&n.currentY===n.startY||(i.startMoving=!0)),i.isScrolling)return void(i.isTouched=!1);if(!i.startMoving)return;s.allowClick=!1,!r.cssMode&&c.cancelable&&c.preventDefault(),r.touchMoveStopPropagation&&!r.nested&&c.stopPropagation(),i.isMoved||(r.loop&&!r.cssMode&&s.loopFix(),i.startTranslate=s.getTranslate(),s.setTransition(0),s.animating&&s.$wrapperEl.trigger("webkitTransitionEnd transitionend"),i.allowMomentumBounce=!1,!r.grabCursor||!0!==s.allowSlideNext&&!0!==s.allowSlidePrev||s.setGrabCursor(!0),s.emit("sliderFirstMove",c)),s.emit("sliderMove",c),i.isMoved=!0;let v=s.isHorizontal()?f:g;n.diff=v,v*=r.touchRatio,l&&(v=-v),s.swipeDirection=v>0?"prev":"next",i.currentTranslate=v+i.startTranslate;let w=!0,b=r.resistanceRatio;if(r.touchReleaseOnEdges&&(b=0),v>0&&i.currentTranslate>s.minTranslate()?(w=!1,r.resistance&&(i.currentTranslate=s.minTranslate()-1+(-s.minTranslate()+i.startTranslate+v)**b)):v<0&&i.currentTranslatei.startTranslate&&(i.currentTranslate=i.startTranslate),s.allowSlidePrev||s.allowSlideNext||(i.currentTranslate=i.startTranslate),r.threshold>0){if(!(Math.abs(v)>r.threshold||i.allowThresholdMove))return void(i.currentTranslate=i.startTranslate);if(!i.allowThresholdMove)return i.allowThresholdMove=!0,n.startX=n.currentX,n.startY=n.currentY,i.currentTranslate=i.startTranslate,void(n.diff=s.isHorizontal()?n.currentX-n.startX:n.currentY-n.startY)}r.followFinger&&!r.cssMode&&((r.freeMode&&r.freeMode.enabled&&s.freeMode||r.watchSlidesProgress)&&(s.updateActiveIndex(),s.updateSlidesClasses()),s.params.freeMode&&r.freeMode.enabled&&s.freeMode&&s.freeMode.onTouchMove(),s.updateProgress(i.currentTranslate),s.setTranslate(i.currentTranslate))}function I(e){const t=this,s=t.touchEventsData,{params:a,touches:i,rtlTranslate:r,slidesGrid:n,enabled:l}=t;if(!l)return;let o=e;if(o.originalEvent&&(o=o.originalEvent),s.allowTouchCallbacks&&t.emit("touchEnd",o),s.allowTouchCallbacks=!1,!s.isTouched)return s.isMoved&&a.grabCursor&&t.setGrabCursor(!1),s.isMoved=!1,void(s.startMoving=!1);a.grabCursor&&s.isMoved&&s.isTouched&&(!0===t.allowSlideNext||!0===t.allowSlidePrev)&&t.setGrabCursor(!1);const d=u(),c=d-s.touchStartTime;if(t.allowClick){const e=o.path||o.composedPath&&o.composedPath();t.updateClickedSlide(e&&e[0]||o.target),t.emit("tap click",o),c<300&&d-s.lastClickTime<300&&t.emit("doubleTap doubleClick",o)}if(s.lastClickTime=u(),p((()=>{t.destroyed||(t.allowClick=!0)})),!s.isTouched||!s.isMoved||!t.swipeDirection||0===i.diff||s.currentTranslate===s.startTranslate)return s.isTouched=!1,s.isMoved=!1,void(s.startMoving=!1);let h;if(s.isTouched=!1,s.isMoved=!1,s.startMoving=!1,h=a.followFinger?r?t.translate:-t.translate:-s.currentTranslate,a.cssMode)return;if(t.params.freeMode&&a.freeMode.enabled)return void t.freeMode.onTouchEnd({currentPos:h});let m=0,f=t.slidesSizesGrid[0];for(let e=0;e=n[e]&&h=n[e]&&(m=e,f=n[n.length-1]-n[n.length-2])}let g=null,v=null;a.rewind&&(t.isBeginning?v=t.params.virtual&&t.params.virtual.enabled&&t.virtual?t.virtual.slides.length-1:t.slides.length-1:t.isEnd&&(g=0));const w=(h-n[m])/f,b=ma.longSwipesMs){if(!a.longSwipes)return void t.slideTo(t.activeIndex);"next"===t.swipeDirection&&(w>=a.longSwipesRatio?t.slideTo(a.rewind&&t.isEnd?g:m+b):t.slideTo(m)),"prev"===t.swipeDirection&&(w>1-a.longSwipesRatio?t.slideTo(m+b):null!==v&&w<0&&Math.abs(w)>a.longSwipesRatio?t.slideTo(v):t.slideTo(m))}else{if(!a.shortSwipes)return void t.slideTo(t.activeIndex);t.navigation&&(o.target===t.navigation.nextEl||o.target===t.navigation.prevEl)?o.target===t.navigation.nextEl?t.slideTo(m+b):t.slideTo(m):("next"===t.swipeDirection&&t.slideTo(null!==g?g:m+b),"prev"===t.swipeDirection&&t.slideTo(null!==v?v:m))}}function A(){const e=this,{params:t,el:s}=e;if(s&&0===s.offsetWidth)return;t.breakpoints&&e.setBreakpoint();const{allowSlideNext:a,allowSlidePrev:i,snapGrid:r}=e;e.allowSlideNext=!0,e.allowSlidePrev=!0,e.updateSize(),e.updateSlides(),e.updateSlidesClasses(),("auto"===t.slidesPerView||t.slidesPerView>1)&&e.isEnd&&!e.isBeginning&&!e.params.centeredSlides?e.slideTo(e.slides.length-1,0,!1,!0):e.slideTo(e.activeIndex,0,!1,!0),e.autoplay&&e.autoplay.running&&e.autoplay.paused&&e.autoplay.run(),e.allowSlidePrev=i,e.allowSlideNext=a,e.params.watchOverflow&&r!==e.snapGrid&&e.checkOverflow()}function D(e){const t=this;t.enabled&&(t.allowClick||(t.params.preventClicks&&e.preventDefault(),t.params.preventClicksPropagation&&t.animating&&(e.stopPropagation(),e.stopImmediatePropagation())))}function G(){const e=this,{wrapperEl:t,rtlTranslate:s,enabled:a}=e;if(!a)return;let i;e.previousTranslate=e.translate,e.isHorizontal()?e.translate=-t.scrollLeft:e.translate=-t.scrollTop,0===e.translate&&(e.translate=0),e.updateActiveIndex(),e.updateSlidesClasses();const r=e.maxTranslate()-e.minTranslate();i=0===r?0:(e.translate-e.minTranslate())/r,i!==e.progress&&e.updateProgress(s?-e.translate:e.translate),e.emit("setTranslate",e.translate,!1)}let N=!1;function B(){}const H=(e,t)=>{const s=a(),{params:i,touchEvents:r,el:n,wrapperEl:l,device:o,support:d}=e,c=!!i.nested,p="on"===t?"addEventListener":"removeEventListener",u=t;if(d.touch){const t=!("touchstart"!==r.start||!d.passiveListener||!i.passiveListeners)&&{passive:!0,capture:!1};n[p](r.start,e.onTouchStart,t),n[p](r.move,e.onTouchMove,d.passiveListener?{passive:!1,capture:c}:c),n[p](r.end,e.onTouchEnd,t),r.cancel&&n[p](r.cancel,e.onTouchEnd,t)}else n[p](r.start,e.onTouchStart,!1),s[p](r.move,e.onTouchMove,c),s[p](r.end,e.onTouchEnd,!1);(i.preventClicks||i.preventClicksPropagation)&&n[p]("click",e.onClick,!0),i.cssMode&&l[p]("scroll",e.onScroll),i.updateOnWindowResize?e[u](o.ios||o.android?"resize orientationchange observerUpdate":"resize observerUpdate",A,!0):e[u]("observerUpdate",A,!0)};var X={attachEvents:function(){const e=this,t=a(),{params:s,support:i}=e;e.onTouchStart=L.bind(e),e.onTouchMove=O.bind(e),e.onTouchEnd=I.bind(e),s.cssMode&&(e.onScroll=G.bind(e)),e.onClick=D.bind(e),i.touch&&!N&&(t.addEventListener("touchstart",B),N=!0),H(e,"on")},detachEvents:function(){H(this,"off")}};const Y=(e,t)=>e.grid&&t.grid&&t.grid.rows>1;var R={addClasses:function(){const e=this,{classNames:t,params:s,rtl:a,$el:i,device:r,support:n}=e,l=function(e,t){const s=[];return e.forEach((e=>{"object"==typeof e?Object.keys(e).forEach((a=>{e[a]&&s.push(t+a)})):"string"==typeof e&&s.push(t+e)})),s}(["initialized",s.direction,{"pointer-events":!n.touch},{"free-mode":e.params.freeMode&&s.freeMode.enabled},{autoheight:s.autoHeight},{rtl:a},{grid:s.grid&&s.grid.rows>1},{"grid-column":s.grid&&s.grid.rows>1&&"column"===s.grid.fill},{android:r.android},{ios:r.ios},{"css-mode":s.cssMode},{centered:s.cssMode&&s.centeredSlides},{"watch-progress":s.watchSlidesProgress}],s.containerModifierClass);t.push(...l),i.addClass([...t].join(" ")),e.emitContainerClasses()},removeClasses:function(){const{$el:e,classNames:t}=this;e.removeClass(t.join(" ")),this.emitContainerClasses()}};var W={init:!0,direction:"horizontal",touchEventsTarget:"wrapper",initialSlide:0,speed:300,cssMode:!1,updateOnWindowResize:!0,resizeObserver:!0,nested:!1,createElements:!1,enabled:!0,focusableElements:"input, select, option, textarea, button, video, label",width:null,height:null,preventInteractionOnTransition:!1,userAgent:null,url:null,edgeSwipeDetection:!1,edgeSwipeThreshold:20,autoHeight:!1,setWrapperSize:!1,virtualTranslate:!1,effect:"slide",breakpoints:void 0,breakpointsBase:"window",spaceBetween:0,slidesPerView:1,slidesPerGroup:1,slidesPerGroupSkip:0,slidesPerGroupAuto:!1,centeredSlides:!1,centeredSlidesBounds:!1,slidesOffsetBefore:0,slidesOffsetAfter:0,normalizeSlideIndex:!0,centerInsufficientSlides:!1,watchOverflow:!0,roundLengths:!1,touchRatio:1,touchAngle:45,simulateTouch:!0,shortSwipes:!0,longSwipes:!0,longSwipesRatio:.5,longSwipesMs:300,followFinger:!0,allowTouchMove:!0,threshold:0,touchMoveStopPropagation:!1,touchStartPreventDefault:!0,touchStartForcePreventDefault:!1,touchReleaseOnEdges:!1,uniqueNavElements:!0,resistance:!0,resistanceRatio:.85,watchSlidesProgress:!1,grabCursor:!1,preventClicks:!0,preventClicksPropagation:!0,slideToClickedSlide:!1,preloadImages:!0,updateOnImagesReady:!0,loop:!1,loopAdditionalSlides:0,loopedSlides:null,loopedSlidesLimit:!0,loopFillGroupWithBlank:!1,loopPreventsSlide:!0,rewind:!1,allowSlidePrev:!0,allowSlideNext:!0,swipeHandler:null,noSwiping:!0,noSwipingClass:"swiper-no-swiping",noSwipingSelector:null,passiveListeners:!0,maxBackfaceHiddenSlides:10,containerModifierClass:"swiper-",slideClass:"swiper-slide",slideBlankClass:"swiper-slide-invisible-blank",slideActiveClass:"swiper-slide-active",slideDuplicateActiveClass:"swiper-slide-duplicate-active",slideVisibleClass:"swiper-slide-visible",slideDuplicateClass:"swiper-slide-duplicate",slideNextClass:"swiper-slide-next",slideDuplicateNextClass:"swiper-slide-duplicate-next",slidePrevClass:"swiper-slide-prev",slideDuplicatePrevClass:"swiper-slide-duplicate-prev",wrapperClass:"swiper-wrapper",runCallbacksOnInit:!0,_emitClasses:!1};function q(e,t){return function(s){void 0===s&&(s={});const a=Object.keys(s)[0],i=s[a];"object"==typeof i&&null!==i?(["navigation","pagination","scrollbar"].indexOf(a)>=0&&!0===e[a]&&(e[a]={auto:!0}),a in e&&"enabled"in i?(!0===e[a]&&(e[a]={enabled:!0}),"object"!=typeof e[a]||"enabled"in e[a]||(e[a].enabled=!0),e[a]||(e[a]={enabled:!1}),g(t,s)):g(t,s)):g(t,s)}}const j={eventsEmitter:$,update:S,translate:M,transition:{setTransition:function(e,t){const s=this;s.params.cssMode||s.$wrapperEl.transition(e),s.emit("setTransition",e,t)},transitionStart:function(e,t){void 0===e&&(e=!0);const s=this,{params:a}=s;a.cssMode||(a.autoHeight&&s.updateAutoHeight(),P({swiper:s,runCallbacks:e,direction:t,step:"Start"}))},transitionEnd:function(e,t){void 0===e&&(e=!0);const s=this,{params:a}=s;s.animating=!1,a.cssMode||(s.setTransition(0),P({swiper:s,runCallbacks:e,direction:t,step:"End"}))}},slide:k,loop:z,grabCursor:{setGrabCursor:function(e){const t=this;if(t.support.touch||!t.params.simulateTouch||t.params.watchOverflow&&t.isLocked||t.params.cssMode)return;const s="container"===t.params.touchEventsTarget?t.el:t.wrapperEl;s.style.cursor="move",s.style.cursor=e?"grabbing":"grab"},unsetGrabCursor:function(){const e=this;e.support.touch||e.params.watchOverflow&&e.isLocked||e.params.cssMode||(e["container"===e.params.touchEventsTarget?"el":"wrapperEl"].style.cursor="")}},events:X,breakpoints:{setBreakpoint:function(){const e=this,{activeIndex:t,initialized:s,loopedSlides:a=0,params:i,$el:r}=e,n=i.breakpoints;if(!n||n&&0===Object.keys(n).length)return;const l=e.getBreakpoint(n,e.params.breakpointsBase,e.el);if(!l||e.currentBreakpoint===l)return;const o=(l in n?n[l]:void 0)||e.originalParams,d=Y(e,i),c=Y(e,o),p=i.enabled;d&&!c?(r.removeClass(`${i.containerModifierClass}grid ${i.containerModifierClass}grid-column`),e.emitContainerClasses()):!d&&c&&(r.addClass(`${i.containerModifierClass}grid`),(o.grid.fill&&"column"===o.grid.fill||!o.grid.fill&&"column"===i.grid.fill)&&r.addClass(`${i.containerModifierClass}grid-column`),e.emitContainerClasses()),["navigation","pagination","scrollbar"].forEach((t=>{const s=i[t]&&i[t].enabled,a=o[t]&&o[t].enabled;s&&!a&&e[t].disable(),!s&&a&&e[t].enable()}));const u=o.direction&&o.direction!==i.direction,h=i.loop&&(o.slidesPerView!==i.slidesPerView||u);u&&s&&e.changeDirection(),g(e.params,o);const m=e.params.enabled;Object.assign(e,{allowTouchMove:e.params.allowTouchMove,allowSlideNext:e.params.allowSlideNext,allowSlidePrev:e.params.allowSlidePrev}),p&&!m?e.disable():!p&&m&&e.enable(),e.currentBreakpoint=l,e.emit("_beforeBreakpoint",o),h&&s&&(e.loopDestroy(),e.loopCreate(),e.updateSlides(),e.slideTo(t-a+e.loopedSlides,0,!1)),e.emit("breakpoint",o)},getBreakpoint:function(e,t,s){if(void 0===t&&(t="window"),!e||"container"===t&&!s)return;let a=!1;const i=r(),n="window"===t?i.innerHeight:s.clientHeight,l=Object.keys(e).map((e=>{if("string"==typeof e&&0===e.indexOf("@")){const t=parseFloat(e.substr(1));return{value:n*t,point:e}}return{value:e,point:e}}));l.sort(((e,t)=>parseInt(e.value,10)-parseInt(t.value,10)));for(let e=0;es}else e.isLocked=1===e.snapGrid.length;!0===s.allowSlideNext&&(e.allowSlideNext=!e.isLocked),!0===s.allowSlidePrev&&(e.allowSlidePrev=!e.isLocked),t&&t!==e.isLocked&&(e.isEnd=!1),t!==e.isLocked&&e.emit(e.isLocked?"lock":"unlock")}},classes:R,images:{loadImage:function(e,t,s,a,i,n){const l=r();let o;function c(){n&&n()}d(e).parent("picture")[0]||e.complete&&i?c():t?(o=new l.Image,o.onload=c,o.onerror=c,a&&(o.sizes=a),s&&(o.srcset=s),t&&(o.src=t)):c()},preloadImages:function(){const e=this;function t(){null!=e&&e&&!e.destroyed&&(void 0!==e.imagesLoaded&&(e.imagesLoaded+=1),e.imagesLoaded===e.imagesToLoad.length&&(e.params.updateOnImagesReady&&e.update(),e.emit("imagesReady")))}e.imagesToLoad=e.$el.find("img");for(let s=0;s1){const e=[];return d(t.el).each((s=>{const a=g({},t,{el:s});e.push(new V(a))})),e}const r=this;r.__swiper__=!0,r.support=E(),r.device=C({userAgent:t.userAgent}),r.browser=T(),r.eventsListeners={},r.eventsAnyListeners=[],r.modules=[...r.__modules__],t.modules&&Array.isArray(t.modules)&&r.modules.push(...t.modules);const n={};r.modules.forEach((e=>{e({swiper:r,extendParams:q(t,n),on:r.on.bind(r),once:r.once.bind(r),off:r.off.bind(r),emit:r.emit.bind(r)})}));const l=g({},W,n);return r.params=g({},l,_,t),r.originalParams=g({},r.params),r.passedParams=g({},t),r.params&&r.params.on&&Object.keys(r.params.on).forEach((e=>{r.on(e,r.params.on[e])})),r.params&&r.params.onAny&&r.onAny(r.params.onAny),r.$=d,Object.assign(r,{enabled:r.params.enabled,el:e,classNames:[],slides:d(),slidesGrid:[],snapGrid:[],slidesSizesGrid:[],isHorizontal:()=>"horizontal"===r.params.direction,isVertical:()=>"vertical"===r.params.direction,activeIndex:0,realIndex:0,isBeginning:!0,isEnd:!1,translate:0,previousTranslate:0,progress:0,velocity:0,animating:!1,allowSlideNext:r.params.allowSlideNext,allowSlidePrev:r.params.allowSlidePrev,touchEvents:function(){const e=["touchstart","touchmove","touchend","touchcancel"],t=["pointerdown","pointermove","pointerup"];return r.touchEventsTouch={start:e[0],move:e[1],end:e[2],cancel:e[3]},r.touchEventsDesktop={start:t[0],move:t[1],end:t[2]},r.support.touch||!r.params.simulateTouch?r.touchEventsTouch:r.touchEventsDesktop}(),touchEventsData:{isTouched:void 0,isMoved:void 0,allowTouchCallbacks:void 0,touchStartTime:void 0,isScrolling:void 0,currentTranslate:void 0,startTranslate:void 0,allowThresholdMove:void 0,focusableElements:r.params.focusableElements,lastClickTime:u(),clickTimeout:void 0,velocities:[],allowMomentumBounce:void 0,isTouchEvent:void 0,startMoving:void 0},allowClick:!0,allowTouchMove:r.params.allowTouchMove,touches:{startX:0,startY:0,currentX:0,currentY:0,diff:0},imagesToLoad:[],imagesLoaded:0}),r.emit("_swiper"),r.params.init&&r.init(),r}enable(){const e=this;e.enabled||(e.enabled=!0,e.params.grabCursor&&e.setGrabCursor(),e.emit("enable"))}disable(){const e=this;e.enabled&&(e.enabled=!1,e.params.grabCursor&&e.unsetGrabCursor(),e.emit("disable"))}setProgress(e,t){const s=this;e=Math.min(Math.max(e,0),1);const a=s.minTranslate(),i=(s.maxTranslate()-a)*e+a;s.translateTo(i,void 0===t?0:t),s.updateActiveIndex(),s.updateSlidesClasses()}emitContainerClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=e.el.className.split(" ").filter((t=>0===t.indexOf("swiper")||0===t.indexOf(e.params.containerModifierClass)));e.emit("_containerClasses",t.join(" "))}getSlideClasses(e){const t=this;return t.destroyed?"":e.className.split(" ").filter((e=>0===e.indexOf("swiper-slide")||0===e.indexOf(t.params.slideClass))).join(" ")}emitSlidesClasses(){const e=this;if(!e.params._emitClasses||!e.el)return;const t=[];e.slides.each((s=>{const a=e.getSlideClasses(s);t.push({slideEl:s,classNames:a}),e.emit("_slideClass",s,a)})),e.emit("_slideClasses",t)}slidesPerViewDynamic(e,t){void 0===e&&(e="current"),void 0===t&&(t=!1);const{params:s,slides:a,slidesGrid:i,slidesSizesGrid:r,size:n,activeIndex:l}=this;let o=1;if(s.centeredSlides){let e,t=a[l].swiperSlideSize;for(let s=l+1;sn&&(e=!0));for(let s=l-1;s>=0;s-=1)a[s]&&!e&&(t+=a[s].swiperSlideSize,o+=1,t>n&&(e=!0))}else if("current"===e)for(let e=l+1;e=0;e-=1){i[l]-i[e]1)&&e.isEnd&&!e.params.centeredSlides?e.slideTo(e.slides.length-1,0,!1,!0):e.slideTo(e.activeIndex,0,!1,!0),i||a()),s.watchOverflow&&t!==e.snapGrid&&e.checkOverflow(),e.emit("update")}changeDirection(e,t){void 0===t&&(t=!0);const s=this,a=s.params.direction;return e||(e="horizontal"===a?"vertical":"horizontal"),e===a||"horizontal"!==e&&"vertical"!==e||(s.$el.removeClass(`${s.params.containerModifierClass}${a}`).addClass(`${s.params.containerModifierClass}${e}`),s.emitContainerClasses(),s.params.direction=e,s.slides.each((t=>{"vertical"===e?t.style.width="":t.style.height=""})),s.emit("changeDirection"),t&&s.update()),s}changeLanguageDirection(e){const t=this;t.rtl&&"rtl"===e||!t.rtl&&"ltr"===e||(t.rtl="rtl"===e,t.rtlTranslate="horizontal"===t.params.direction&&t.rtl,t.rtl?(t.$el.addClass(`${t.params.containerModifierClass}rtl`),t.el.dir="rtl"):(t.$el.removeClass(`${t.params.containerModifierClass}rtl`),t.el.dir="ltr"),t.update())}mount(e){const t=this;if(t.mounted)return!0;const s=d(e||t.params.el);if(!(e=s[0]))return!1;e.swiper=t;const i=()=>`.${(t.params.wrapperClass||"").trim().split(" ").join(".")}`;let r=(()=>{if(e&&e.shadowRoot&&e.shadowRoot.querySelector){const t=d(e.shadowRoot.querySelector(i()));return t.children=e=>s.children(e),t}return s.children?s.children(i()):d(s).children(i())})();if(0===r.length&&t.params.createElements){const e=a().createElement("div");r=d(e),e.className=t.params.wrapperClass,s.append(e),s.children(`.${t.params.slideClass}`).each((e=>{r.append(e)}))}return Object.assign(t,{$el:s,el:e,$wrapperEl:r,wrapperEl:r[0],mounted:!0,rtl:"rtl"===e.dir.toLowerCase()||"rtl"===s.css("direction"),rtlTranslate:"horizontal"===t.params.direction&&("rtl"===e.dir.toLowerCase()||"rtl"===s.css("direction")),wrongRTL:"-webkit-box"===r.css("display")}),!0}init(e){const t=this;if(t.initialized)return t;return!1===t.mount(e)||(t.emit("beforeInit"),t.params.breakpoints&&t.setBreakpoint(),t.addClasses(),t.params.loop&&t.loopCreate(),t.updateSize(),t.updateSlides(),t.params.watchOverflow&&t.checkOverflow(),t.params.grabCursor&&t.enabled&&t.setGrabCursor(),t.params.preloadImages&&t.preloadImages(),t.params.loop?t.slideTo(t.params.initialSlide+t.loopedSlides,0,t.params.runCallbacksOnInit,!1,!0):t.slideTo(t.params.initialSlide,0,t.params.runCallbacksOnInit,!1,!0),t.attachEvents(),t.initialized=!0,t.emit("init"),t.emit("afterInit")),t}destroy(e,t){void 0===e&&(e=!0),void 0===t&&(t=!0);const s=this,{params:a,$el:i,$wrapperEl:r,slides:n}=s;return void 0===s.params||s.destroyed||(s.emit("beforeDestroy"),s.initialized=!1,s.detachEvents(),a.loop&&s.loopDestroy(),t&&(s.removeClasses(),i.removeAttr("style"),r.removeAttr("style"),n&&n.length&&n.removeClass([a.slideVisibleClass,a.slideActiveClass,a.slideNextClass,a.slidePrevClass].join(" ")).removeAttr("style").removeAttr("data-swiper-slide-index")),s.emit("destroy"),Object.keys(s.eventsListeners).forEach((e=>{s.off(e)})),!1!==e&&(s.$el[0].swiper=null,function(e){const t=e;Object.keys(t).forEach((e=>{try{t[e]=null}catch(e){}try{delete t[e]}catch(e){}}))}(s)),s.destroyed=!0),null}static extendDefaults(e){g(_,e)}static get extendedDefaults(){return _}static get defaults(){return W}static installModule(e){V.prototype.__modules__||(V.prototype.__modules__=[]);const t=V.prototype.__modules__;"function"==typeof e&&t.indexOf(e)<0&&t.push(e)}static use(e){return Array.isArray(e)?(e.forEach((e=>V.installModule(e))),V):(V.installModule(e),V)}}function F(e,t,s,i){const r=a();return e.params.createElements&&Object.keys(i).forEach((a=>{if(!s[a]&&!0===s.auto){let n=e.$el.children(`.${i[a]}`)[0];n||(n=r.createElement("div"),n.className=i[a],e.$el.append(n)),s[a]=n,t[a]=n}})),s}function U(e){return void 0===e&&(e=""),`.${e.trim().replace(/([\.:!\/])/g,"\\$1").replace(/ /g,".")}`}function K(e){const t=this,{$wrapperEl:s,params:a}=t;if(a.loop&&t.loopDestroy(),"object"==typeof e&&"length"in e)for(let t=0;t=l)return void s.appendSlide(t);let o=n>e?n+1:n;const d=[];for(let t=l-1;t>=e;t-=1){const e=s.slides.eq(t);e.remove(),d.unshift(e)}if("object"==typeof t&&"length"in t){for(let e=0;ee?n+t.length:n}else a.append(t);for(let e=0;e{if(s.params.effect!==t)return;s.classNames.push(`${s.params.containerModifierClass}${t}`),l&&l()&&s.classNames.push(`${s.params.containerModifierClass}3d`);const e=n?n():{};Object.assign(s.params,e),Object.assign(s.originalParams,e)})),a("setTranslate",(()=>{s.params.effect===t&&i()})),a("setTransition",((e,a)=>{s.params.effect===t&&r(a)})),a("transitionEnd",(()=>{if(s.params.effect===t&&o){if(!d||!d().slideShadows)return;s.slides.each((e=>{s.$(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").remove()})),o()}})),a("virtualUpdate",(()=>{s.params.effect===t&&(s.slides.length||(c=!0),requestAnimationFrame((()=>{c&&s.slides&&s.slides.length&&(i(),c=!1)})))}))}function se(e,t){return e.transformEl?t.find(e.transformEl).css({"backface-visibility":"hidden","-webkit-backface-visibility":"hidden"}):t}function ae(e){let{swiper:t,duration:s,transformEl:a,allSlides:i}=e;const{slides:r,activeIndex:n,$wrapperEl:l}=t;if(t.params.virtualTranslate&&0!==s){let e,s=!1;e=i?a?r.find(a):r:a?r.eq(n).find(a):r.eq(n),e.transitionEnd((()=>{if(s)return;if(!t||t.destroyed)return;s=!0,t.animating=!1;const e=["webkitTransitionEnd","transitionend"];for(let t=0;t`),i.append(r)),r}Object.keys(j).forEach((e=>{Object.keys(j[e]).forEach((t=>{V.prototype[t]=j[e][t]}))})),V.use([function(e){let{swiper:t,on:s,emit:a}=e;const i=r();let n=null,l=null;const o=()=>{t&&!t.destroyed&&t.initialized&&(a("beforeResize"),a("resize"))},d=()=>{t&&!t.destroyed&&t.initialized&&a("orientationchange")};s("init",(()=>{t.params.resizeObserver&&void 0!==i.ResizeObserver?t&&!t.destroyed&&t.initialized&&(n=new ResizeObserver((e=>{l=i.requestAnimationFrame((()=>{const{width:s,height:a}=t;let i=s,r=a;e.forEach((e=>{let{contentBoxSize:s,contentRect:a,target:n}=e;n&&n!==t.el||(i=a?a.width:(s[0]||s).inlineSize,r=a?a.height:(s[0]||s).blockSize)})),i===s&&r===a||o()}))})),n.observe(t.el)):(i.addEventListener("resize",o),i.addEventListener("orientationchange",d))})),s("destroy",(()=>{l&&i.cancelAnimationFrame(l),n&&n.unobserve&&t.el&&(n.unobserve(t.el),n=null),i.removeEventListener("resize",o),i.removeEventListener("orientationchange",d)}))},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const n=[],l=r(),o=function(e,t){void 0===t&&(t={});const s=new(l.MutationObserver||l.WebkitMutationObserver)((e=>{if(1===e.length)return void i("observerUpdate",e[0]);const t=function(){i("observerUpdate",e[0])};l.requestAnimationFrame?l.requestAnimationFrame(t):l.setTimeout(t,0)}));s.observe(e,{attributes:void 0===t.attributes||t.attributes,childList:void 0===t.childList||t.childList,characterData:void 0===t.characterData||t.characterData}),n.push(s)};s({observer:!1,observeParents:!1,observeSlideChildren:!1}),a("init",(()=>{if(t.params.observer){if(t.params.observeParents){const e=t.$el.parents();for(let t=0;t{n.forEach((e=>{e.disconnect()})),n.splice(0,n.length)}))}]);const re=[function(e){let t,{swiper:s,extendParams:a,on:i,emit:r}=e;function n(e,t){const a=s.params.virtual;if(a.cache&&s.virtual.cache[t])return s.virtual.cache[t];const i=a.renderSlide?d(a.renderSlide.call(s,e,t)):d(`
${e}
`);return i.attr("data-swiper-slide-index")||i.attr("data-swiper-slide-index",t),a.cache&&(s.virtual.cache[t]=i),i}function l(e){const{slidesPerView:t,slidesPerGroup:a,centeredSlides:i}=s.params,{addSlidesBefore:l,addSlidesAfter:o}=s.params.virtual,{from:d,to:c,slides:p,slidesGrid:u,offset:h}=s.virtual;s.params.cssMode||s.updateActiveIndex();const m=s.activeIndex||0;let f,g,v;f=s.rtlTranslate?"right":s.isHorizontal()?"left":"top",i?(g=Math.floor(t/2)+a+o,v=Math.floor(t/2)+a+l):(g=t+(a-1)+o,v=a+l);const w=Math.max((m||0)-v,0),b=Math.min((m||0)+g,p.length-1),x=(s.slidesGrid[w]||0)-(s.slidesGrid[0]||0);function y(){s.updateSlides(),s.updateProgress(),s.updateSlidesClasses(),s.lazy&&s.params.lazy.enabled&&s.lazy.load(),r("virtualUpdate")}if(Object.assign(s.virtual,{from:w,to:b,offset:x,slidesGrid:s.slidesGrid}),d===w&&c===b&&!e)return s.slidesGrid!==u&&x!==h&&s.slides.css(f,`${x}px`),s.updateProgress(),void r("virtualUpdate");if(s.params.virtual.renderExternal)return s.params.virtual.renderExternal.call(s,{offset:x,from:w,to:b,slides:function(){const e=[];for(let t=w;t<=b;t+=1)e.push(p[t]);return e}()}),void(s.params.virtual.renderExternalUpdate?y():r("virtualUpdate"));const E=[],C=[];if(e)s.$wrapperEl.find(`.${s.params.slideClass}`).remove();else for(let e=d;e<=c;e+=1)(eb)&&s.$wrapperEl.find(`.${s.params.slideClass}[data-swiper-slide-index="${e}"]`).remove();for(let t=0;t=w&&t<=b&&(void 0===c||e?C.push(t):(t>c&&C.push(t),t{s.$wrapperEl.append(n(p[e],e))})),E.sort(((e,t)=>t-e)).forEach((e=>{s.$wrapperEl.prepend(n(p[e],e))})),s.$wrapperEl.children(".swiper-slide").css(f,`${x}px`),y()}a({virtual:{enabled:!1,slides:[],cache:!0,renderSlide:null,renderExternal:null,renderExternalUpdate:!0,addSlidesBefore:0,addSlidesAfter:0}}),s.virtual={cache:{},from:void 0,to:void 0,slides:[],offset:0,slidesGrid:[]},i("beforeInit",(()=>{s.params.virtual.enabled&&(s.virtual.slides=s.params.virtual.slides,s.classNames.push(`${s.params.containerModifierClass}virtual`),s.params.watchSlidesProgress=!0,s.originalParams.watchSlidesProgress=!0,s.params.initialSlide||l())})),i("setTranslate",(()=>{s.params.virtual.enabled&&(s.params.cssMode&&!s._immediateVirtual?(clearTimeout(t),t=setTimeout((()=>{l()}),100)):l())})),i("init update resize",(()=>{s.params.virtual.enabled&&s.params.cssMode&&v(s.wrapperEl,"--swiper-virtual-size",`${s.virtualSize}px`)})),Object.assign(s.virtual,{appendSlide:function(e){if("object"==typeof e&&"length"in e)for(let t=0;t{const a=e[s],r=a.attr("data-swiper-slide-index");r&&a.attr("data-swiper-slide-index",parseInt(r,10)+i),t[parseInt(s,10)+i]=a})),s.virtual.cache=t}l(!0),s.slideTo(a,0)},removeSlide:function(e){if(null==e)return;let t=s.activeIndex;if(Array.isArray(e))for(let a=e.length-1;a>=0;a-=1)s.virtual.slides.splice(e[a],1),s.params.virtual.cache&&delete s.virtual.cache[e[a]],e[a]0&&0===t.$el.parents(`.${t.params.slideActiveClass}`).length)return;const a=t.$el,i=a[0].clientWidth,r=a[0].clientHeight,n=o.innerWidth,l=o.innerHeight,d=t.$el.offset();s&&(d.left-=t.$el[0].scrollLeft);const c=[[d.left,d.top],[d.left+i,d.top],[d.left,d.top+r],[d.left+i,d.top+r]];for(let t=0;t=0&&s[0]<=n&&s[1]>=0&&s[1]<=l){if(0===s[0]&&0===s[1])continue;e=!0}}if(!e)return}t.isHorizontal()?((d||c||p||u)&&(a.preventDefault?a.preventDefault():a.returnValue=!1),((c||u)&&!s||(d||p)&&s)&&t.slideNext(),((d||p)&&!s||(c||u)&&s)&&t.slidePrev()):((d||c||h||m)&&(a.preventDefault?a.preventDefault():a.returnValue=!1),(c||m)&&t.slideNext(),(d||h)&&t.slidePrev()),n("keyPress",i)}}function p(){t.keyboard.enabled||(d(l).on("keydown",c),t.keyboard.enabled=!0)}function u(){t.keyboard.enabled&&(d(l).off("keydown",c),t.keyboard.enabled=!1)}t.keyboard={enabled:!1},s({keyboard:{enabled:!1,onlyInViewport:!0,pageUpDown:!0}}),i("init",(()=>{t.params.keyboard.enabled&&p()})),i("destroy",(()=>{t.keyboard.enabled&&u()})),Object.assign(t.keyboard,{enable:p,disable:u})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const n=r();let l;s({mousewheel:{enabled:!1,releaseOnEdges:!1,invert:!1,forceToAxis:!1,sensitivity:1,eventsTarget:"container",thresholdDelta:null,thresholdTime:null}}),t.mousewheel={enabled:!1};let o,c=u();const h=[];function m(){t.enabled&&(t.mouseEntered=!0)}function f(){t.enabled&&(t.mouseEntered=!1)}function g(e){return!(t.params.mousewheel.thresholdDelta&&e.delta=6&&u()-c<60||(e.direction<0?t.isEnd&&!t.params.loop||t.animating||(t.slideNext(),i("scroll",e.raw)):t.isBeginning&&!t.params.loop||t.animating||(t.slidePrev(),i("scroll",e.raw)),c=(new n.Date).getTime(),!1)))}function v(e){let s=e,a=!0;if(!t.enabled)return;const r=t.params.mousewheel;t.params.cssMode&&s.preventDefault();let n=t.$el;if("container"!==t.params.mousewheel.eventsTarget&&(n=d(t.params.mousewheel.eventsTarget)),!t.mouseEntered&&!n[0].contains(s.target)&&!r.releaseOnEdges)return!0;s.originalEvent&&(s=s.originalEvent);let c=0;const m=t.rtlTranslate?-1:1,f=function(e){let t=0,s=0,a=0,i=0;return"detail"in e&&(s=e.detail),"wheelDelta"in e&&(s=-e.wheelDelta/120),"wheelDeltaY"in e&&(s=-e.wheelDeltaY/120),"wheelDeltaX"in e&&(t=-e.wheelDeltaX/120),"axis"in e&&e.axis===e.HORIZONTAL_AXIS&&(t=s,s=0),a=10*t,i=10*s,"deltaY"in e&&(i=e.deltaY),"deltaX"in e&&(a=e.deltaX),e.shiftKey&&!a&&(a=i,i=0),(a||i)&&e.deltaMode&&(1===e.deltaMode?(a*=40,i*=40):(a*=800,i*=800)),a&&!t&&(t=a<1?-1:1),i&&!s&&(s=i<1?-1:1),{spinX:t,spinY:s,pixelX:a,pixelY:i}}(s);if(r.forceToAxis)if(t.isHorizontal()){if(!(Math.abs(f.pixelX)>Math.abs(f.pixelY)))return!0;c=-f.pixelX*m}else{if(!(Math.abs(f.pixelY)>Math.abs(f.pixelX)))return!0;c=-f.pixelY}else c=Math.abs(f.pixelX)>Math.abs(f.pixelY)?-f.pixelX*m:-f.pixelY;if(0===c)return!0;r.invert&&(c=-c);let v=t.getTranslate()+c*r.sensitivity;if(v>=t.minTranslate()&&(v=t.minTranslate()),v<=t.maxTranslate()&&(v=t.maxTranslate()),a=!!t.params.loop||!(v===t.minTranslate()||v===t.maxTranslate()),a&&t.params.nested&&s.stopPropagation(),t.params.freeMode&&t.params.freeMode.enabled){const e={time:u(),delta:Math.abs(c),direction:Math.sign(c)},a=o&&e.time=t.minTranslate()&&(n=t.minTranslate()),n<=t.maxTranslate()&&(n=t.maxTranslate()),t.setTransition(0),t.setTranslate(n),t.updateProgress(),t.updateActiveIndex(),t.updateSlidesClasses(),(!d&&t.isBeginning||!u&&t.isEnd)&&t.updateSlidesClasses(),t.params.freeMode.sticky){clearTimeout(l),l=void 0,h.length>=15&&h.shift();const s=h.length?h[h.length-1]:void 0,a=h[0];if(h.push(e),s&&(e.delta>s.delta||e.direction!==s.direction))h.splice(0);else if(h.length>=15&&e.time-a.time<500&&a.delta-e.delta>=1&&e.delta<=6){const s=c>0?.8:.2;o=e,h.splice(0),l=p((()=>{t.slideToClosest(t.params.speed,!0,void 0,s)}),0)}l||(l=p((()=>{o=e,h.splice(0),t.slideToClosest(t.params.speed,!0,void 0,.5)}),500))}if(a||i("scroll",s),t.params.autoplay&&t.params.autoplayDisableOnInteraction&&t.autoplay.stop(),n===t.minTranslate()||n===t.maxTranslate())return!0}}else{const s={time:u(),delta:Math.abs(c),direction:Math.sign(c),raw:e};h.length>=2&&h.shift();const a=h.length?h[h.length-1]:void 0;if(h.push(s),a?(s.direction!==a.direction||s.delta>a.delta||s.time>a.time+150)&&g(s):g(s),function(e){const s=t.params.mousewheel;if(e.direction<0){if(t.isEnd&&!t.params.loop&&s.releaseOnEdges)return!0}else if(t.isBeginning&&!t.params.loop&&s.releaseOnEdges)return!0;return!1}(s))return!0}return s.preventDefault?s.preventDefault():s.returnValue=!1,!1}function w(e){let s=t.$el;"container"!==t.params.mousewheel.eventsTarget&&(s=d(t.params.mousewheel.eventsTarget)),s[e]("mouseenter",m),s[e]("mouseleave",f),s[e]("wheel",v)}function b(){return t.params.cssMode?(t.wrapperEl.removeEventListener("wheel",v),!0):!t.mousewheel.enabled&&(w("on"),t.mousewheel.enabled=!0,!0)}function x(){return t.params.cssMode?(t.wrapperEl.addEventListener(event,v),!0):!!t.mousewheel.enabled&&(w("off"),t.mousewheel.enabled=!1,!0)}a("init",(()=>{!t.params.mousewheel.enabled&&t.params.cssMode&&x(),t.params.mousewheel.enabled&&b()})),a("destroy",(()=>{t.params.cssMode&&b(),t.mousewheel.enabled&&x()})),Object.assign(t.mousewheel,{enable:b,disable:x})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;function r(e){let s;return e&&(s=d(e),t.params.uniqueNavElements&&"string"==typeof e&&s.length>1&&1===t.$el.find(e).length&&(s=t.$el.find(e))),s}function n(e,s){const a=t.params.navigation;e&&e.length>0&&(e[s?"addClass":"removeClass"](a.disabledClass),e[0]&&"BUTTON"===e[0].tagName&&(e[0].disabled=s),t.params.watchOverflow&&t.enabled&&e[t.isLocked?"addClass":"removeClass"](a.lockClass))}function l(){if(t.params.loop)return;const{$nextEl:e,$prevEl:s}=t.navigation;n(s,t.isBeginning&&!t.params.rewind),n(e,t.isEnd&&!t.params.rewind)}function o(e){e.preventDefault(),(!t.isBeginning||t.params.loop||t.params.rewind)&&(t.slidePrev(),i("navigationPrev"))}function c(e){e.preventDefault(),(!t.isEnd||t.params.loop||t.params.rewind)&&(t.slideNext(),i("navigationNext"))}function p(){const e=t.params.navigation;if(t.params.navigation=F(t,t.originalParams.navigation,t.params.navigation,{nextEl:"swiper-button-next",prevEl:"swiper-button-prev"}),!e.nextEl&&!e.prevEl)return;const s=r(e.nextEl),a=r(e.prevEl);s&&s.length>0&&s.on("click",c),a&&a.length>0&&a.on("click",o),Object.assign(t.navigation,{$nextEl:s,nextEl:s&&s[0],$prevEl:a,prevEl:a&&a[0]}),t.enabled||(s&&s.addClass(e.lockClass),a&&a.addClass(e.lockClass))}function u(){const{$nextEl:e,$prevEl:s}=t.navigation;e&&e.length&&(e.off("click",c),e.removeClass(t.params.navigation.disabledClass)),s&&s.length&&(s.off("click",o),s.removeClass(t.params.navigation.disabledClass))}s({navigation:{nextEl:null,prevEl:null,hideOnClick:!1,disabledClass:"swiper-button-disabled",hiddenClass:"swiper-button-hidden",lockClass:"swiper-button-lock",navigationDisabledClass:"swiper-navigation-disabled"}}),t.navigation={nextEl:null,$nextEl:null,prevEl:null,$prevEl:null},a("init",(()=>{!1===t.params.navigation.enabled?h():(p(),l())})),a("toEdge fromEdge lock unlock",(()=>{l()})),a("destroy",(()=>{u()})),a("enable disable",(()=>{const{$nextEl:e,$prevEl:s}=t.navigation;e&&e[t.enabled?"removeClass":"addClass"](t.params.navigation.lockClass),s&&s[t.enabled?"removeClass":"addClass"](t.params.navigation.lockClass)})),a("click",((e,s)=>{const{$nextEl:a,$prevEl:r}=t.navigation,n=s.target;if(t.params.navigation.hideOnClick&&!d(n).is(r)&&!d(n).is(a)){if(t.pagination&&t.params.pagination&&t.params.pagination.clickable&&(t.pagination.el===n||t.pagination.el.contains(n)))return;let e;a?e=a.hasClass(t.params.navigation.hiddenClass):r&&(e=r.hasClass(t.params.navigation.hiddenClass)),i(!0===e?"navigationShow":"navigationHide"),a&&a.toggleClass(t.params.navigation.hiddenClass),r&&r.toggleClass(t.params.navigation.hiddenClass)}}));const h=()=>{t.$el.addClass(t.params.navigation.navigationDisabledClass),u()};Object.assign(t.navigation,{enable:()=>{t.$el.removeClass(t.params.navigation.navigationDisabledClass),p(),l()},disable:h,update:l,init:p,destroy:u})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const r="swiper-pagination";let n;s({pagination:{el:null,bulletElement:"span",clickable:!1,hideOnClick:!1,renderBullet:null,renderProgressbar:null,renderFraction:null,renderCustom:null,progressbarOpposite:!1,type:"bullets",dynamicBullets:!1,dynamicMainBullets:1,formatFractionCurrent:e=>e,formatFractionTotal:e=>e,bulletClass:`${r}-bullet`,bulletActiveClass:`${r}-bullet-active`,modifierClass:`${r}-`,currentClass:`${r}-current`,totalClass:`${r}-total`,hiddenClass:`${r}-hidden`,progressbarFillClass:`${r}-progressbar-fill`,progressbarOppositeClass:`${r}-progressbar-opposite`,clickableClass:`${r}-clickable`,lockClass:`${r}-lock`,horizontalClass:`${r}-horizontal`,verticalClass:`${r}-vertical`,paginationDisabledClass:`${r}-disabled`}}),t.pagination={el:null,$el:null,bullets:[]};let l=0;function o(){return!t.params.pagination.el||!t.pagination.el||!t.pagination.$el||0===t.pagination.$el.length}function c(e,s){const{bulletActiveClass:a}=t.params.pagination;e[s]().addClass(`${a}-${s}`)[s]().addClass(`${a}-${s}-${s}`)}function p(){const e=t.rtl,s=t.params.pagination;if(o())return;const a=t.virtual&&t.params.virtual.enabled?t.virtual.slides.length:t.slides.length,r=t.pagination.$el;let p;const u=t.params.loop?Math.ceil((a-2*t.loopedSlides)/t.params.slidesPerGroup):t.snapGrid.length;if(t.params.loop?(p=Math.ceil((t.activeIndex-t.loopedSlides)/t.params.slidesPerGroup),p>a-1-2*t.loopedSlides&&(p-=a-2*t.loopedSlides),p>u-1&&(p-=u),p<0&&"bullets"!==t.params.paginationType&&(p=u+p)):p=void 0!==t.snapIndex?t.snapIndex:t.activeIndex||0,"bullets"===s.type&&t.pagination.bullets&&t.pagination.bullets.length>0){const a=t.pagination.bullets;let i,o,u;if(s.dynamicBullets&&(n=a.eq(0)[t.isHorizontal()?"outerWidth":"outerHeight"](!0),r.css(t.isHorizontal()?"width":"height",n*(s.dynamicMainBullets+4)+"px"),s.dynamicMainBullets>1&&void 0!==t.previousIndex&&(l+=p-(t.previousIndex-t.loopedSlides||0),l>s.dynamicMainBullets-1?l=s.dynamicMainBullets-1:l<0&&(l=0)),i=Math.max(p-l,0),o=i+(Math.min(a.length,s.dynamicMainBullets)-1),u=(o+i)/2),a.removeClass(["","-next","-next-next","-prev","-prev-prev","-main"].map((e=>`${s.bulletActiveClass}${e}`)).join(" ")),r.length>1)a.each((e=>{const t=d(e),a=t.index();a===p&&t.addClass(s.bulletActiveClass),s.dynamicBullets&&(a>=i&&a<=o&&t.addClass(`${s.bulletActiveClass}-main`),a===i&&c(t,"prev"),a===o&&c(t,"next"))}));else{const e=a.eq(p),r=e.index();if(e.addClass(s.bulletActiveClass),s.dynamicBullets){const e=a.eq(i),n=a.eq(o);for(let e=i;e<=o;e+=1)a.eq(e).addClass(`${s.bulletActiveClass}-main`);if(t.params.loop)if(r>=a.length){for(let e=s.dynamicMainBullets;e>=0;e-=1)a.eq(a.length-e).addClass(`${s.bulletActiveClass}-main`);a.eq(a.length-s.dynamicMainBullets-1).addClass(`${s.bulletActiveClass}-prev`)}else c(e,"prev"),c(n,"next");else c(e,"prev"),c(n,"next")}}if(s.dynamicBullets){const i=Math.min(a.length,s.dynamicMainBullets+4),r=(n*i-n)/2-u*n,l=e?"right":"left";a.css(t.isHorizontal()?l:"top",`${r}px`)}}if("fraction"===s.type&&(r.find(U(s.currentClass)).text(s.formatFractionCurrent(p+1)),r.find(U(s.totalClass)).text(s.formatFractionTotal(u))),"progressbar"===s.type){let e;e=s.progressbarOpposite?t.isHorizontal()?"vertical":"horizontal":t.isHorizontal()?"horizontal":"vertical";const a=(p+1)/u;let i=1,n=1;"horizontal"===e?i=a:n=a,r.find(U(s.progressbarFillClass)).transform(`translate3d(0,0,0) scaleX(${i}) scaleY(${n})`).transition(t.params.speed)}"custom"===s.type&&s.renderCustom?(r.html(s.renderCustom(t,p+1,u)),i("paginationRender",r[0])):i("paginationUpdate",r[0]),t.params.watchOverflow&&t.enabled&&r[t.isLocked?"addClass":"removeClass"](s.lockClass)}function u(){const e=t.params.pagination;if(o())return;const s=t.virtual&&t.params.virtual.enabled?t.virtual.slides.length:t.slides.length,a=t.pagination.$el;let r="";if("bullets"===e.type){let i=t.params.loop?Math.ceil((s-2*t.loopedSlides)/t.params.slidesPerGroup):t.snapGrid.length;t.params.freeMode&&t.params.freeMode.enabled&&!t.params.loop&&i>s&&(i=s);for(let s=0;s`;a.html(r),t.pagination.bullets=a.find(U(e.bulletClass))}"fraction"===e.type&&(r=e.renderFraction?e.renderFraction.call(t,e.currentClass,e.totalClass):` / `,a.html(r)),"progressbar"===e.type&&(r=e.renderProgressbar?e.renderProgressbar.call(t,e.progressbarFillClass):``,a.html(r)),"custom"!==e.type&&i("paginationRender",t.pagination.$el[0])}function h(){t.params.pagination=F(t,t.originalParams.pagination,t.params.pagination,{el:"swiper-pagination"});const e=t.params.pagination;if(!e.el)return;let s=d(e.el);0!==s.length&&(t.params.uniqueNavElements&&"string"==typeof e.el&&s.length>1&&(s=t.$el.find(e.el),s.length>1&&(s=s.filter((e=>d(e).parents(".swiper")[0]===t.el)))),"bullets"===e.type&&e.clickable&&s.addClass(e.clickableClass),s.addClass(e.modifierClass+e.type),s.addClass(t.isHorizontal()?e.horizontalClass:e.verticalClass),"bullets"===e.type&&e.dynamicBullets&&(s.addClass(`${e.modifierClass}${e.type}-dynamic`),l=0,e.dynamicMainBullets<1&&(e.dynamicMainBullets=1)),"progressbar"===e.type&&e.progressbarOpposite&&s.addClass(e.progressbarOppositeClass),e.clickable&&s.on("click",U(e.bulletClass),(function(e){e.preventDefault();let s=d(this).index()*t.params.slidesPerGroup;t.params.loop&&(s+=t.loopedSlides),t.slideTo(s)})),Object.assign(t.pagination,{$el:s,el:s[0]}),t.enabled||s.addClass(e.lockClass))}function m(){const e=t.params.pagination;if(o())return;const s=t.pagination.$el;s.removeClass(e.hiddenClass),s.removeClass(e.modifierClass+e.type),s.removeClass(t.isHorizontal()?e.horizontalClass:e.verticalClass),t.pagination.bullets&&t.pagination.bullets.removeClass&&t.pagination.bullets.removeClass(e.bulletActiveClass),e.clickable&&s.off("click",U(e.bulletClass))}a("init",(()=>{!1===t.params.pagination.enabled?f():(h(),u(),p())})),a("activeIndexChange",(()=>{(t.params.loop||void 0===t.snapIndex)&&p()})),a("snapIndexChange",(()=>{t.params.loop||p()})),a("slidesLengthChange",(()=>{t.params.loop&&(u(),p())})),a("snapGridLengthChange",(()=>{t.params.loop||(u(),p())})),a("destroy",(()=>{m()})),a("enable disable",(()=>{const{$el:e}=t.pagination;e&&e[t.enabled?"removeClass":"addClass"](t.params.pagination.lockClass)})),a("lock unlock",(()=>{p()})),a("click",((e,s)=>{const a=s.target,{$el:r}=t.pagination;if(t.params.pagination.el&&t.params.pagination.hideOnClick&&r&&r.length>0&&!d(a).hasClass(t.params.pagination.bulletClass)){if(t.navigation&&(t.navigation.nextEl&&a===t.navigation.nextEl||t.navigation.prevEl&&a===t.navigation.prevEl))return;const e=r.hasClass(t.params.pagination.hiddenClass);i(!0===e?"paginationShow":"paginationHide"),r.toggleClass(t.params.pagination.hiddenClass)}}));const f=()=>{t.$el.addClass(t.params.pagination.paginationDisabledClass),t.pagination.$el&&t.pagination.$el.addClass(t.params.pagination.paginationDisabledClass),m()};Object.assign(t.pagination,{enable:()=>{t.$el.removeClass(t.params.pagination.paginationDisabledClass),t.pagination.$el&&t.pagination.$el.removeClass(t.params.pagination.paginationDisabledClass),h(),u(),p()},disable:f,render:u,update:p,init:h,destroy:m})},function(e){let{swiper:t,extendParams:s,on:i,emit:r}=e;const n=a();let l,o,c,u,h=!1,m=null,f=null;function g(){if(!t.params.scrollbar.el||!t.scrollbar.el)return;const{scrollbar:e,rtlTranslate:s,progress:a}=t,{$dragEl:i,$el:r}=e,n=t.params.scrollbar;let l=o,d=(c-o)*a;s?(d=-d,d>0?(l=o-d,d=0):-d+o>c&&(l=c+d)):d<0?(l=o+d,d=0):d+o>c&&(l=c-d),t.isHorizontal()?(i.transform(`translate3d(${d}px, 0, 0)`),i[0].style.width=`${l}px`):(i.transform(`translate3d(0px, ${d}px, 0)`),i[0].style.height=`${l}px`),n.hide&&(clearTimeout(m),r[0].style.opacity=1,m=setTimeout((()=>{r[0].style.opacity=0,r.transition(400)}),1e3))}function v(){if(!t.params.scrollbar.el||!t.scrollbar.el)return;const{scrollbar:e}=t,{$dragEl:s,$el:a}=e;s[0].style.width="",s[0].style.height="",c=t.isHorizontal()?a[0].offsetWidth:a[0].offsetHeight,u=t.size/(t.virtualSize+t.params.slidesOffsetBefore-(t.params.centeredSlides?t.snapGrid[0]:0)),o="auto"===t.params.scrollbar.dragSize?c*u:parseInt(t.params.scrollbar.dragSize,10),t.isHorizontal()?s[0].style.width=`${o}px`:s[0].style.height=`${o}px`,a[0].style.display=u>=1?"none":"",t.params.scrollbar.hide&&(a[0].style.opacity=0),t.params.watchOverflow&&t.enabled&&e.$el[t.isLocked?"addClass":"removeClass"](t.params.scrollbar.lockClass)}function w(e){return t.isHorizontal()?"touchstart"===e.type||"touchmove"===e.type?e.targetTouches[0].clientX:e.clientX:"touchstart"===e.type||"touchmove"===e.type?e.targetTouches[0].clientY:e.clientY}function b(e){const{scrollbar:s,rtlTranslate:a}=t,{$el:i}=s;let r;r=(w(e)-i.offset()[t.isHorizontal()?"left":"top"]-(null!==l?l:o/2))/(c-o),r=Math.max(Math.min(r,1),0),a&&(r=1-r);const n=t.minTranslate()+(t.maxTranslate()-t.minTranslate())*r;t.updateProgress(n),t.setTranslate(n),t.updateActiveIndex(),t.updateSlidesClasses()}function x(e){const s=t.params.scrollbar,{scrollbar:a,$wrapperEl:i}=t,{$el:n,$dragEl:o}=a;h=!0,l=e.target===o[0]||e.target===o?w(e)-e.target.getBoundingClientRect()[t.isHorizontal()?"left":"top"]:null,e.preventDefault(),e.stopPropagation(),i.transition(100),o.transition(100),b(e),clearTimeout(f),n.transition(0),s.hide&&n.css("opacity",1),t.params.cssMode&&t.$wrapperEl.css("scroll-snap-type","none"),r("scrollbarDragStart",e)}function y(e){const{scrollbar:s,$wrapperEl:a}=t,{$el:i,$dragEl:n}=s;h&&(e.preventDefault?e.preventDefault():e.returnValue=!1,b(e),a.transition(0),i.transition(0),n.transition(0),r("scrollbarDragMove",e))}function E(e){const s=t.params.scrollbar,{scrollbar:a,$wrapperEl:i}=t,{$el:n}=a;h&&(h=!1,t.params.cssMode&&(t.$wrapperEl.css("scroll-snap-type",""),i.transition("")),s.hide&&(clearTimeout(f),f=p((()=>{n.css("opacity",0),n.transition(400)}),1e3)),r("scrollbarDragEnd",e),s.snapOnRelease&&t.slideToClosest())}function C(e){const{scrollbar:s,touchEventsTouch:a,touchEventsDesktop:i,params:r,support:l}=t,o=s.$el;if(!o)return;const d=o[0],c=!(!l.passiveListener||!r.passiveListeners)&&{passive:!1,capture:!1},p=!(!l.passiveListener||!r.passiveListeners)&&{passive:!0,capture:!1};if(!d)return;const u="on"===e?"addEventListener":"removeEventListener";l.touch?(d[u](a.start,x,c),d[u](a.move,y,c),d[u](a.end,E,p)):(d[u](i.start,x,c),n[u](i.move,y,c),n[u](i.end,E,p))}function T(){const{scrollbar:e,$el:s}=t;t.params.scrollbar=F(t,t.originalParams.scrollbar,t.params.scrollbar,{el:"swiper-scrollbar"});const a=t.params.scrollbar;if(!a.el)return;let i=d(a.el);t.params.uniqueNavElements&&"string"==typeof a.el&&i.length>1&&1===s.find(a.el).length&&(i=s.find(a.el)),i.addClass(t.isHorizontal()?a.horizontalClass:a.verticalClass);let r=i.find(`.${t.params.scrollbar.dragClass}`);0===r.length&&(r=d(`
`),i.append(r)),Object.assign(e,{$el:i,el:i[0],$dragEl:r,dragEl:r[0]}),a.draggable&&t.params.scrollbar.el&&t.scrollbar.el&&C("on"),i&&i[t.enabled?"removeClass":"addClass"](t.params.scrollbar.lockClass)}function $(){const e=t.params.scrollbar,s=t.scrollbar.$el;s&&s.removeClass(t.isHorizontal()?e.horizontalClass:e.verticalClass),t.params.scrollbar.el&&t.scrollbar.el&&C("off")}s({scrollbar:{el:null,dragSize:"auto",hide:!1,draggable:!1,snapOnRelease:!0,lockClass:"swiper-scrollbar-lock",dragClass:"swiper-scrollbar-drag",scrollbarDisabledClass:"swiper-scrollbar-disabled",horizontalClass:"swiper-scrollbar-horizontal",verticalClass:"swiper-scrollbar-vertical"}}),t.scrollbar={el:null,dragEl:null,$el:null,$dragEl:null},i("init",(()=>{!1===t.params.scrollbar.enabled?S():(T(),v(),g())})),i("update resize observerUpdate lock unlock",(()=>{v()})),i("setTranslate",(()=>{g()})),i("setTransition",((e,s)=>{!function(e){t.params.scrollbar.el&&t.scrollbar.el&&t.scrollbar.$dragEl.transition(e)}(s)})),i("enable disable",(()=>{const{$el:e}=t.scrollbar;e&&e[t.enabled?"removeClass":"addClass"](t.params.scrollbar.lockClass)})),i("destroy",(()=>{$()}));const S=()=>{t.$el.addClass(t.params.scrollbar.scrollbarDisabledClass),t.scrollbar.$el&&t.scrollbar.$el.addClass(t.params.scrollbar.scrollbarDisabledClass),$()};Object.assign(t.scrollbar,{enable:()=>{t.$el.removeClass(t.params.scrollbar.scrollbarDisabledClass),t.scrollbar.$el&&t.scrollbar.$el.removeClass(t.params.scrollbar.scrollbarDisabledClass),T(),v(),g()},disable:S,updateSize:v,setTranslate:g,init:T,destroy:$})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({parallax:{enabled:!1}});const i=(e,s)=>{const{rtl:a}=t,i=d(e),r=a?-1:1,n=i.attr("data-swiper-parallax")||"0";let l=i.attr("data-swiper-parallax-x"),o=i.attr("data-swiper-parallax-y");const c=i.attr("data-swiper-parallax-scale"),p=i.attr("data-swiper-parallax-opacity");if(l||o?(l=l||"0",o=o||"0"):t.isHorizontal()?(l=n,o="0"):(o=n,l="0"),l=l.indexOf("%")>=0?parseInt(l,10)*s*r+"%":l*s*r+"px",o=o.indexOf("%")>=0?parseInt(o,10)*s+"%":o*s+"px",null!=p){const e=p-(p-1)*(1-Math.abs(s));i[0].style.opacity=e}if(null==c)i.transform(`translate3d(${l}, ${o}, 0px)`);else{const e=c-(c-1)*(1-Math.abs(s));i.transform(`translate3d(${l}, ${o}, 0px) scale(${e})`)}},r=()=>{const{$el:e,slides:s,progress:a,snapGrid:r}=t;e.children("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((e=>{i(e,a)})),s.each(((e,s)=>{let n=e.progress;t.params.slidesPerGroup>1&&"auto"!==t.params.slidesPerView&&(n+=Math.ceil(s/2)-a*(r.length-1)),n=Math.min(Math.max(n,-1),1),d(e).find("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((e=>{i(e,n)}))}))};a("beforeInit",(()=>{t.params.parallax.enabled&&(t.params.watchSlidesProgress=!0,t.originalParams.watchSlidesProgress=!0)})),a("init",(()=>{t.params.parallax.enabled&&r()})),a("setTranslate",(()=>{t.params.parallax.enabled&&r()})),a("setTransition",((e,s)=>{t.params.parallax.enabled&&function(e){void 0===e&&(e=t.params.speed);const{$el:s}=t;s.find("[data-swiper-parallax], [data-swiper-parallax-x], [data-swiper-parallax-y], [data-swiper-parallax-opacity], [data-swiper-parallax-scale]").each((t=>{const s=d(t);let a=parseInt(s.attr("data-swiper-parallax-duration"),10)||e;0===e&&(a=0),s.transition(a)}))}(s)}))},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;const n=r();s({zoom:{enabled:!1,maxRatio:3,minRatio:1,toggle:!0,containerClass:"swiper-zoom-container",zoomedSlideClass:"swiper-slide-zoomed"}}),t.zoom={enabled:!1};let l,o,c,p=1,u=!1;const m={$slideEl:void 0,slideWidth:void 0,slideHeight:void 0,$imageEl:void 0,$imageWrapEl:void 0,maxRatio:3},f={isTouched:void 0,isMoved:void 0,currentX:void 0,currentY:void 0,minX:void 0,minY:void 0,maxX:void 0,maxY:void 0,width:void 0,height:void 0,startX:void 0,startY:void 0,touchesStart:{},touchesCurrent:{}},g={x:void 0,y:void 0,prevPositionX:void 0,prevPositionY:void 0,prevTime:void 0};let v=1;function w(e){if(e.targetTouches.length<2)return 1;const t=e.targetTouches[0].pageX,s=e.targetTouches[0].pageY,a=e.targetTouches[1].pageX,i=e.targetTouches[1].pageY;return Math.sqrt((a-t)**2+(i-s)**2)}function b(e){const s=t.support,a=t.params.zoom;if(o=!1,c=!1,!s.gestures){if("touchstart"!==e.type||"touchstart"===e.type&&e.targetTouches.length<2)return;o=!0,m.scaleStart=w(e)}m.$slideEl&&m.$slideEl.length||(m.$slideEl=d(e.target).closest(`.${t.params.slideClass}`),0===m.$slideEl.length&&(m.$slideEl=t.slides.eq(t.activeIndex)),m.$imageEl=m.$slideEl.find(`.${a.containerClass}`).eq(0).find("picture, img, svg, canvas, .swiper-zoom-target").eq(0),m.$imageWrapEl=m.$imageEl.parent(`.${a.containerClass}`),m.maxRatio=m.$imageWrapEl.attr("data-swiper-zoom")||a.maxRatio,0!==m.$imageWrapEl.length)?(m.$imageEl&&m.$imageEl.transition(0),u=!0):m.$imageEl=void 0}function x(e){const s=t.support,a=t.params.zoom,i=t.zoom;if(!s.gestures){if("touchmove"!==e.type||"touchmove"===e.type&&e.targetTouches.length<2)return;c=!0,m.scaleMove=w(e)}m.$imageEl&&0!==m.$imageEl.length?(s.gestures?i.scale=e.scale*p:i.scale=m.scaleMove/m.scaleStart*p,i.scale>m.maxRatio&&(i.scale=m.maxRatio-1+(i.scale-m.maxRatio+1)**.5),i.scalef.touchesStart.x))return void(f.isTouched=!1);if(!t.isHorizontal()&&(Math.floor(f.minY)===Math.floor(f.startY)&&f.touchesCurrent.yf.touchesStart.y))return void(f.isTouched=!1)}e.cancelable&&e.preventDefault(),e.stopPropagation(),f.isMoved=!0,f.currentX=f.touchesCurrent.x-f.touchesStart.x+f.startX,f.currentY=f.touchesCurrent.y-f.touchesStart.y+f.startY,f.currentXf.maxX&&(f.currentX=f.maxX-1+(f.currentX-f.maxX+1)**.8),f.currentYf.maxY&&(f.currentY=f.maxY-1+(f.currentY-f.maxY+1)**.8),g.prevPositionX||(g.prevPositionX=f.touchesCurrent.x),g.prevPositionY||(g.prevPositionY=f.touchesCurrent.y),g.prevTime||(g.prevTime=Date.now()),g.x=(f.touchesCurrent.x-g.prevPositionX)/(Date.now()-g.prevTime)/2,g.y=(f.touchesCurrent.y-g.prevPositionY)/(Date.now()-g.prevTime)/2,Math.abs(f.touchesCurrent.x-g.prevPositionX)<2&&(g.x=0),Math.abs(f.touchesCurrent.y-g.prevPositionY)<2&&(g.y=0),g.prevPositionX=f.touchesCurrent.x,g.prevPositionY=f.touchesCurrent.y,g.prevTime=Date.now(),m.$imageWrapEl.transform(`translate3d(${f.currentX}px, ${f.currentY}px,0)`)}}function C(){const e=t.zoom;m.$slideEl&&t.previousIndex!==t.activeIndex&&(m.$imageEl&&m.$imageEl.transform("translate3d(0,0,0) scale(1)"),m.$imageWrapEl&&m.$imageWrapEl.transform("translate3d(0,0,0)"),e.scale=1,p=1,m.$slideEl=void 0,m.$imageEl=void 0,m.$imageWrapEl=void 0)}function T(e){const s=t.zoom,a=t.params.zoom;if(m.$slideEl||(e&&e.target&&(m.$slideEl=d(e.target).closest(`.${t.params.slideClass}`)),m.$slideEl||(t.params.virtual&&t.params.virtual.enabled&&t.virtual?m.$slideEl=t.$wrapperEl.children(`.${t.params.slideActiveClass}`):m.$slideEl=t.slides.eq(t.activeIndex)),m.$imageEl=m.$slideEl.find(`.${a.containerClass}`).eq(0).find("picture, img, svg, canvas, .swiper-zoom-target").eq(0),m.$imageWrapEl=m.$imageEl.parent(`.${a.containerClass}`)),!m.$imageEl||0===m.$imageEl.length||!m.$imageWrapEl||0===m.$imageWrapEl.length)return;let i,r,l,o,c,u,h,g,v,w,b,x,y,E,C,T,$,S;t.params.cssMode&&(t.wrapperEl.style.overflow="hidden",t.wrapperEl.style.touchAction="none"),m.$slideEl.addClass(`${a.zoomedSlideClass}`),void 0===f.touchesStart.x&&e?(i="touchend"===e.type?e.changedTouches[0].pageX:e.pageX,r="touchend"===e.type?e.changedTouches[0].pageY:e.pageY):(i=f.touchesStart.x,r=f.touchesStart.y),s.scale=m.$imageWrapEl.attr("data-swiper-zoom")||a.maxRatio,p=m.$imageWrapEl.attr("data-swiper-zoom")||a.maxRatio,e?($=m.$slideEl[0].offsetWidth,S=m.$slideEl[0].offsetHeight,l=m.$slideEl.offset().left+n.scrollX,o=m.$slideEl.offset().top+n.scrollY,c=l+$/2-i,u=o+S/2-r,v=m.$imageEl[0].offsetWidth,w=m.$imageEl[0].offsetHeight,b=v*s.scale,x=w*s.scale,y=Math.min($/2-b/2,0),E=Math.min(S/2-x/2,0),C=-y,T=-E,h=c*s.scale,g=u*s.scale,hC&&(h=C),gT&&(g=T)):(h=0,g=0),m.$imageWrapEl.transition(300).transform(`translate3d(${h}px, ${g}px,0)`),m.$imageEl.transition(300).transform(`translate3d(0,0,0) scale(${s.scale})`)}function $(){const e=t.zoom,s=t.params.zoom;m.$slideEl||(t.params.virtual&&t.params.virtual.enabled&&t.virtual?m.$slideEl=t.$wrapperEl.children(`.${t.params.slideActiveClass}`):m.$slideEl=t.slides.eq(t.activeIndex),m.$imageEl=m.$slideEl.find(`.${s.containerClass}`).eq(0).find("picture, img, svg, canvas, .swiper-zoom-target").eq(0),m.$imageWrapEl=m.$imageEl.parent(`.${s.containerClass}`)),m.$imageEl&&0!==m.$imageEl.length&&m.$imageWrapEl&&0!==m.$imageWrapEl.length&&(t.params.cssMode&&(t.wrapperEl.style.overflow="",t.wrapperEl.style.touchAction=""),e.scale=1,p=1,m.$imageWrapEl.transition(300).transform("translate3d(0,0,0)"),m.$imageEl.transition(300).transform("translate3d(0,0,0) scale(1)"),m.$slideEl.removeClass(`${s.zoomedSlideClass}`),m.$slideEl=void 0)}function S(e){const s=t.zoom;s.scale&&1!==s.scale?$():T(e)}function M(){const e=t.support;return{passiveListener:!("touchstart"!==t.touchEvents.start||!e.passiveListener||!t.params.passiveListeners)&&{passive:!0,capture:!1},activeListenerWithCapture:!e.passiveListener||{passive:!1,capture:!0}}}function P(){return`.${t.params.slideClass}`}function k(e){const{passiveListener:s}=M(),a=P();t.$wrapperEl[e]("gesturestart",a,b,s),t.$wrapperEl[e]("gesturechange",a,x,s),t.$wrapperEl[e]("gestureend",a,y,s)}function z(){l||(l=!0,k("on"))}function L(){l&&(l=!1,k("off"))}function O(){const e=t.zoom;if(e.enabled)return;e.enabled=!0;const s=t.support,{passiveListener:a,activeListenerWithCapture:i}=M(),r=P();s.gestures?(t.$wrapperEl.on(t.touchEvents.start,z,a),t.$wrapperEl.on(t.touchEvents.end,L,a)):"touchstart"===t.touchEvents.start&&(t.$wrapperEl.on(t.touchEvents.start,r,b,a),t.$wrapperEl.on(t.touchEvents.move,r,x,i),t.$wrapperEl.on(t.touchEvents.end,r,y,a),t.touchEvents.cancel&&t.$wrapperEl.on(t.touchEvents.cancel,r,y,a)),t.$wrapperEl.on(t.touchEvents.move,`.${t.params.zoom.containerClass}`,E,i)}function I(){const e=t.zoom;if(!e.enabled)return;const s=t.support;e.enabled=!1;const{passiveListener:a,activeListenerWithCapture:i}=M(),r=P();s.gestures?(t.$wrapperEl.off(t.touchEvents.start,z,a),t.$wrapperEl.off(t.touchEvents.end,L,a)):"touchstart"===t.touchEvents.start&&(t.$wrapperEl.off(t.touchEvents.start,r,b,a),t.$wrapperEl.off(t.touchEvents.move,r,x,i),t.$wrapperEl.off(t.touchEvents.end,r,y,a),t.touchEvents.cancel&&t.$wrapperEl.off(t.touchEvents.cancel,r,y,a)),t.$wrapperEl.off(t.touchEvents.move,`.${t.params.zoom.containerClass}`,E,i)}Object.defineProperty(t.zoom,"scale",{get:()=>v,set(e){if(v!==e){const t=m.$imageEl?m.$imageEl[0]:void 0,s=m.$slideEl?m.$slideEl[0]:void 0;i("zoomChange",e,t,s)}v=e}}),a("init",(()=>{t.params.zoom.enabled&&O()})),a("destroy",(()=>{I()})),a("touchStart",((e,s)=>{t.zoom.enabled&&function(e){const s=t.device;m.$imageEl&&0!==m.$imageEl.length&&(f.isTouched||(s.android&&e.cancelable&&e.preventDefault(),f.isTouched=!0,f.touchesStart.x="touchstart"===e.type?e.targetTouches[0].pageX:e.pageX,f.touchesStart.y="touchstart"===e.type?e.targetTouches[0].pageY:e.pageY))}(s)})),a("touchEnd",((e,s)=>{t.zoom.enabled&&function(){const e=t.zoom;if(!m.$imageEl||0===m.$imageEl.length)return;if(!f.isTouched||!f.isMoved)return f.isTouched=!1,void(f.isMoved=!1);f.isTouched=!1,f.isMoved=!1;let s=300,a=300;const i=g.x*s,r=f.currentX+i,n=g.y*a,l=f.currentY+n;0!==g.x&&(s=Math.abs((r-f.currentX)/g.x)),0!==g.y&&(a=Math.abs((l-f.currentY)/g.y));const o=Math.max(s,a);f.currentX=r,f.currentY=l;const d=f.width*e.scale,c=f.height*e.scale;f.minX=Math.min(m.slideWidth/2-d/2,0),f.maxX=-f.minX,f.minY=Math.min(m.slideHeight/2-c/2,0),f.maxY=-f.minY,f.currentX=Math.max(Math.min(f.currentX,f.maxX),f.minX),f.currentY=Math.max(Math.min(f.currentY,f.maxY),f.minY),m.$imageWrapEl.transition(o).transform(`translate3d(${f.currentX}px, ${f.currentY}px,0)`)}()})),a("doubleTap",((e,s)=>{!t.animating&&t.params.zoom.enabled&&t.zoom.enabled&&t.params.zoom.toggle&&S(s)})),a("transitionEnd",(()=>{t.zoom.enabled&&t.params.zoom.enabled&&C()})),a("slideChange",(()=>{t.zoom.enabled&&t.params.zoom.enabled&&t.params.cssMode&&C()})),Object.assign(t.zoom,{enable:O,disable:I,in:T,out:$,toggle:S})},function(e){let{swiper:t,extendParams:s,on:a,emit:i}=e;s({lazy:{checkInView:!1,enabled:!1,loadPrevNext:!1,loadPrevNextAmount:1,loadOnTransitionStart:!1,scrollingElement:"",elementClass:"swiper-lazy",loadingClass:"swiper-lazy-loading",loadedClass:"swiper-lazy-loaded",preloaderClass:"swiper-lazy-preloader"}}),t.lazy={};let n=!1,l=!1;function o(e,s){void 0===s&&(s=!0);const a=t.params.lazy;if(void 0===e)return;if(0===t.slides.length)return;const r=t.virtual&&t.params.virtual.enabled?t.$wrapperEl.children(`.${t.params.slideClass}[data-swiper-slide-index="${e}"]`):t.slides.eq(e),n=r.find(`.${a.elementClass}:not(.${a.loadedClass}):not(.${a.loadingClass})`);!r.hasClass(a.elementClass)||r.hasClass(a.loadedClass)||r.hasClass(a.loadingClass)||n.push(r[0]),0!==n.length&&n.each((e=>{const n=d(e);n.addClass(a.loadingClass);const l=n.attr("data-background"),c=n.attr("data-src"),p=n.attr("data-srcset"),u=n.attr("data-sizes"),h=n.parent("picture");t.loadImage(n[0],c||l,p,u,!1,(()=>{if(null!=t&&t&&(!t||t.params)&&!t.destroyed){if(l?(n.css("background-image",`url("${l}")`),n.removeAttr("data-background")):(p&&(n.attr("srcset",p),n.removeAttr("data-srcset")),u&&(n.attr("sizes",u),n.removeAttr("data-sizes")),h.length&&h.children("source").each((e=>{const t=d(e);t.attr("data-srcset")&&(t.attr("srcset",t.attr("data-srcset")),t.removeAttr("data-srcset"))})),c&&(n.attr("src",c),n.removeAttr("data-src"))),n.addClass(a.loadedClass).removeClass(a.loadingClass),r.find(`.${a.preloaderClass}`).remove(),t.params.loop&&s){const e=r.attr("data-swiper-slide-index");if(r.hasClass(t.params.slideDuplicateClass)){o(t.$wrapperEl.children(`[data-swiper-slide-index="${e}"]:not(.${t.params.slideDuplicateClass})`).index(),!1)}else{o(t.$wrapperEl.children(`.${t.params.slideDuplicateClass}[data-swiper-slide-index="${e}"]`).index(),!1)}}i("lazyImageReady",r[0],n[0]),t.params.autoHeight&&t.updateAutoHeight()}})),i("lazyImageLoad",r[0],n[0])}))}function c(){const{$wrapperEl:e,params:s,slides:a,activeIndex:i}=t,r=t.virtual&&s.virtual.enabled,n=s.lazy;let c=s.slidesPerView;function p(t){if(r){if(e.children(`.${s.slideClass}[data-swiper-slide-index="${t}"]`).length)return!0}else if(a[t])return!0;return!1}function u(e){return r?d(e).attr("data-swiper-slide-index"):d(e).index()}if("auto"===c&&(c=0),l||(l=!0),t.params.watchSlidesProgress)e.children(`.${s.slideVisibleClass}`).each((e=>{o(r?d(e).attr("data-swiper-slide-index"):d(e).index())}));else if(c>1)for(let e=i;e1||n.loadPrevNextAmount&&n.loadPrevNextAmount>1){const e=n.loadPrevNextAmount,t=Math.ceil(c),s=Math.min(i+t+Math.max(e,t),a.length),r=Math.max(i-Math.max(t,e),0);for(let e=i+t;e0&&o(u(t));const a=e.children(`.${s.slidePrevClass}`);a.length>0&&o(u(a))}}function p(){const e=r();if(!t||t.destroyed)return;const s=t.params.lazy.scrollingElement?d(t.params.lazy.scrollingElement):d(e),a=s[0]===e,i=a?e.innerWidth:s[0].offsetWidth,l=a?e.innerHeight:s[0].offsetHeight,o=t.$el.offset(),{rtlTranslate:u}=t;let h=!1;u&&(o.left-=t.$el[0].scrollLeft);const m=[[o.left,o.top],[o.left+t.width,o.top],[o.left,o.top+t.height],[o.left+t.width,o.top+t.height]];for(let e=0;e=0&&t[0]<=i&&t[1]>=0&&t[1]<=l){if(0===t[0]&&0===t[1])continue;h=!0}}const f=!("touchstart"!==t.touchEvents.start||!t.support.passiveListener||!t.params.passiveListeners)&&{passive:!0,capture:!1};h?(c(),s.off("scroll",p,f)):n||(n=!0,s.on("scroll",p,f))}a("beforeInit",(()=>{t.params.lazy.enabled&&t.params.preloadImages&&(t.params.preloadImages=!1)})),a("init",(()=>{t.params.lazy.enabled&&(t.params.lazy.checkInView?p():c())})),a("scroll",(()=>{t.params.freeMode&&t.params.freeMode.enabled&&!t.params.freeMode.sticky&&c()})),a("scrollbarDragMove resize _freeModeNoMomentumRelease",(()=>{t.params.lazy.enabled&&(t.params.lazy.checkInView?p():c())})),a("transitionStart",(()=>{t.params.lazy.enabled&&(t.params.lazy.loadOnTransitionStart||!t.params.lazy.loadOnTransitionStart&&!l)&&(t.params.lazy.checkInView?p():c())})),a("transitionEnd",(()=>{t.params.lazy.enabled&&!t.params.lazy.loadOnTransitionStart&&(t.params.lazy.checkInView?p():c())})),a("slideChange",(()=>{const{lazy:e,cssMode:s,watchSlidesProgress:a,touchReleaseOnEdges:i,resistanceRatio:r}=t.params;e.enabled&&(s||a&&(i||0===r))&&c()})),a("destroy",(()=>{t.$el&&t.$el.find(`.${t.params.lazy.loadingClass}`).removeClass(t.params.lazy.loadingClass)})),Object.assign(t.lazy,{load:c,loadInSlide:o})},function(e){let{swiper:t,extendParams:s,on:a}=e;function i(e,t){const s=function(){let e,t,s;return(a,i)=>{for(t=-1,e=a.length;e-t>1;)s=e+t>>1,a[s]<=i?t=s:e=s;return e}}();let a,i;return this.x=e,this.y=t,this.lastIndex=e.length-1,this.interpolate=function(e){return e?(i=s(this.x,e),a=i-1,(e-this.x[a])*(this.y[i]-this.y[a])/(this.x[i]-this.x[a])+this.y[a]):0},this}function r(){t.controller.control&&t.controller.spline&&(t.controller.spline=void 0,delete t.controller.spline)}s({controller:{control:void 0,inverse:!1,by:"slide"}}),t.controller={control:void 0},a("beforeInit",(()=>{t.controller.control=t.params.controller.control})),a("update",(()=>{r()})),a("resize",(()=>{r()})),a("observerUpdate",(()=>{r()})),a("setTranslate",((e,s,a)=>{t.controller.control&&t.controller.setTranslate(s,a)})),a("setTransition",((e,s,a)=>{t.controller.control&&t.controller.setTransition(s,a)})),Object.assign(t.controller,{setTranslate:function(e,s){const a=t.controller.control;let r,n;const l=t.constructor;function o(e){const s=t.rtlTranslate?-t.translate:t.translate;"slide"===t.params.controller.by&&(!function(e){t.controller.spline||(t.controller.spline=t.params.loop?new i(t.slidesGrid,e.slidesGrid):new i(t.snapGrid,e.snapGrid))}(e),n=-t.controller.spline.interpolate(-s)),n&&"container"!==t.params.controller.by||(r=(e.maxTranslate()-e.minTranslate())/(t.maxTranslate()-t.minTranslate()),n=(s-t.minTranslate())*r+e.minTranslate()),t.params.controller.inverse&&(n=e.maxTranslate()-n),e.updateProgress(n),e.setTranslate(n,t),e.updateActiveIndex(),e.updateSlidesClasses()}if(Array.isArray(a))for(let e=0;e{s.updateAutoHeight()})),s.$wrapperEl.transitionEnd((()=>{i&&(s.params.loop&&"slide"===t.params.controller.by&&s.loopFix(),s.transitionEnd())})))}if(Array.isArray(i))for(r=0;r{n(e),"BUTTON"!==e[0].tagName&&(o(e,"button"),e.on("keydown",m)),p(e,s),function(e,t){e.attr("aria-controls",t)}(e,t)},w=()=>{t.a11y.clicked=!0},b=()=>{requestAnimationFrame((()=>{requestAnimationFrame((()=>{t.a11y.clicked=!1}))}))},x=e=>{if(t.a11y.clicked)return;const s=e.target.closest(`.${t.params.slideClass}`);if(!s||!t.slides.includes(s))return;const a=t.slides.indexOf(s)===t.activeIndex,i=t.params.watchSlidesProgress&&t.visibleSlides&&t.visibleSlides.includes(s);a||i||(t.isHorizontal()?t.el.scrollLeft=0:t.el.scrollTop=0,t.slideTo(t.slides.indexOf(s),0))},y=()=>{const e=t.params.a11y;e.itemRoleDescriptionMessage&&c(d(t.slides),e.itemRoleDescriptionMessage),e.slideRole&&o(d(t.slides),e.slideRole);const s=t.params.loop?t.slides.filter((e=>!e.classList.contains(t.params.slideDuplicateClass))).length:t.slides.length;e.slideLabelMessage&&t.slides.each(((a,i)=>{const r=d(a),n=t.params.loop?parseInt(r.attr("data-swiper-slide-index"),10):i;p(r,e.slideLabelMessage.replace(/\{\{index\}\}/,n+1).replace(/\{\{slidesLength\}\}/,s))}))},E=()=>{const e=t.params.a11y;t.$el.append(i);const s=t.$el;e.containerRoleDescriptionMessage&&c(s,e.containerRoleDescriptionMessage),e.containerMessage&&p(s,e.containerMessage);const a=t.$wrapperEl,r=e.id||a.attr("id")||`swiper-wrapper-${n=16,void 0===n&&(n=16),"x".repeat(n).replace(/x/g,(()=>Math.round(16*Math.random()).toString(16)))}`;var n;const l=t.params.autoplay&&t.params.autoplay.enabled?"off":"polite";var o;let d,u;o=r,a.attr("id",o),function(e,t){e.attr("aria-live",t)}(a,l),y(),t.navigation&&t.navigation.$nextEl&&(d=t.navigation.$nextEl),t.navigation&&t.navigation.$prevEl&&(u=t.navigation.$prevEl),d&&d.length&&v(d,r,e.nextSlideMessage),u&&u.length&&v(u,r,e.prevSlideMessage),g()&&t.pagination.$el.on("keydown",U(t.params.pagination.bulletClass),m),t.$el.on("focus",x,!0),t.$el.on("pointerdown",w,!0),t.$el.on("pointerup",b,!0)};a("beforeInit",(()=>{i=d(``)})),a("afterInit",(()=>{t.params.a11y.enabled&&E()})),a("slidesLengthChange snapGridLengthChange slidesGridLengthChange",(()=>{t.params.a11y.enabled&&y()})),a("fromEdge toEdge afterInit lock unlock",(()=>{t.params.a11y.enabled&&function(){if(t.params.loop||t.params.rewind||!t.navigation)return;const{$nextEl:e,$prevEl:s}=t.navigation;s&&s.length>0&&(t.isBeginning?(u(s),l(s)):(h(s),n(s))),e&&e.length>0&&(t.isEnd?(u(e),l(e)):(h(e),n(e)))}()})),a("paginationUpdate",(()=>{t.params.a11y.enabled&&function(){const e=t.params.a11y;f()&&t.pagination.bullets.each((s=>{const a=d(s);t.params.pagination.clickable&&(n(a),t.params.pagination.renderBullet||(o(a,"button"),p(a,e.paginationBulletMessage.replace(/\{\{index\}\}/,a.index()+1)))),a.is(`.${t.params.pagination.bulletActiveClass}`)?a.attr("aria-current","true"):a.removeAttr("aria-current")}))}()})),a("destroy",(()=>{t.params.a11y.enabled&&function(){let e,s;i&&i.length>0&&i.remove(),t.navigation&&t.navigation.$nextEl&&(e=t.navigation.$nextEl),t.navigation&&t.navigation.$prevEl&&(s=t.navigation.$prevEl),e&&e.off("keydown",m),s&&s.off("keydown",m),g()&&t.pagination.$el.off("keydown",U(t.params.pagination.bulletClass),m),t.$el.off("focus",x,!0),t.$el.off("pointerdown",w,!0),t.$el.off("pointerup",b,!0)}()}))},function(e){let{swiper:t,extendParams:s,on:a}=e;s({history:{enabled:!1,root:"",replaceState:!1,key:"slides",keepQuery:!1}});let i=!1,n={};const l=e=>e.toString().replace(/\s+/g,"-").replace(/[^\w-]+/g,"").replace(/--+/g,"-").replace(/^-+/,"").replace(/-+$/,""),o=e=>{const t=r();let s;s=e?new URL(e):t.location;const a=s.pathname.slice(1).split("/").filter((e=>""!==e)),i=a.length;return{key:a[i-2],value:a[i-1]}},d=(e,s)=>{const a=r();if(!i||!t.params.history.enabled)return;let n;n=t.params.url?new URL(t.params.url):a.location;const o=t.slides.eq(s);let d=l(o.attr("data-history"));if(t.params.history.root.length>0){let s=t.params.history.root;"/"===s[s.length-1]&&(s=s.slice(0,s.length-1)),d=`${s}/${e}/${d}`}else n.pathname.includes(e)||(d=`${e}/${d}`);t.params.history.keepQuery&&(d+=n.search);const c=a.history.state;c&&c.value===d||(t.params.history.replaceState?a.history.replaceState({value:d},null,d):a.history.pushState({value:d},null,d))},c=(e,s,a)=>{if(s)for(let i=0,r=t.slides.length;i{n=o(t.params.url),c(t.params.speed,n.value,!1)};a("init",(()=>{t.params.history.enabled&&(()=>{const e=r();if(t.params.history){if(!e.history||!e.history.pushState)return t.params.history.enabled=!1,void(t.params.hashNavigation.enabled=!0);i=!0,n=o(t.params.url),(n.key||n.value)&&(c(0,n.value,t.params.runCallbacksOnInit),t.params.history.replaceState||e.addEventListener("popstate",p))}})()})),a("destroy",(()=>{t.params.history.enabled&&(()=>{const e=r();t.params.history.replaceState||e.removeEventListener("popstate",p)})()})),a("transitionEnd _freeModeNoMomentumRelease",(()=>{i&&d(t.params.history.key,t.activeIndex)})),a("slideChange",(()=>{i&&t.params.cssMode&&d(t.params.history.key,t.activeIndex)}))},function(e){let{swiper:t,extendParams:s,emit:i,on:n}=e,l=!1;const o=a(),c=r();s({hashNavigation:{enabled:!1,replaceState:!1,watchState:!1}});const p=()=>{i("hashChange");const e=o.location.hash.replace("#","");if(e!==t.slides.eq(t.activeIndex).attr("data-hash")){const s=t.$wrapperEl.children(`.${t.params.slideClass}[data-hash="${e}"]`).index();if(void 0===s)return;t.slideTo(s)}},u=()=>{if(l&&t.params.hashNavigation.enabled)if(t.params.hashNavigation.replaceState&&c.history&&c.history.replaceState)c.history.replaceState(null,null,`#${t.slides.eq(t.activeIndex).attr("data-hash")}`||""),i("hashSet");else{const e=t.slides.eq(t.activeIndex),s=e.attr("data-hash")||e.attr("data-history");o.location.hash=s||"",i("hashSet")}};n("init",(()=>{t.params.hashNavigation.enabled&&(()=>{if(!t.params.hashNavigation.enabled||t.params.history&&t.params.history.enabled)return;l=!0;const e=o.location.hash.replace("#","");if(e){const s=0;for(let a=0,i=t.slides.length;a{t.params.hashNavigation.enabled&&t.params.hashNavigation.watchState&&d(c).off("hashchange",p)})),n("transitionEnd _freeModeNoMomentumRelease",(()=>{l&&u()})),n("slideChange",(()=>{l&&t.params.cssMode&&u()}))},function(e){let t,{swiper:s,extendParams:i,on:r,emit:n}=e;function l(){if(!s.size)return s.autoplay.running=!1,void(s.autoplay.paused=!1);const e=s.slides.eq(s.activeIndex);let a=s.params.autoplay.delay;e.attr("data-swiper-autoplay")&&(a=e.attr("data-swiper-autoplay")||s.params.autoplay.delay),clearTimeout(t),t=p((()=>{let e;s.params.autoplay.reverseDirection?s.params.loop?(s.loopFix(),e=s.slidePrev(s.params.speed,!0,!0),n("autoplay")):s.isBeginning?s.params.autoplay.stopOnLastSlide?d():(e=s.slideTo(s.slides.length-1,s.params.speed,!0,!0),n("autoplay")):(e=s.slidePrev(s.params.speed,!0,!0),n("autoplay")):s.params.loop?(s.loopFix(),e=s.slideNext(s.params.speed,!0,!0),n("autoplay")):s.isEnd?s.params.autoplay.stopOnLastSlide?d():(e=s.slideTo(0,s.params.speed,!0,!0),n("autoplay")):(e=s.slideNext(s.params.speed,!0,!0),n("autoplay")),(s.params.cssMode&&s.autoplay.running||!1===e)&&l()}),a)}function o(){return void 0===t&&(!s.autoplay.running&&(s.autoplay.running=!0,n("autoplayStart"),l(),!0))}function d(){return!!s.autoplay.running&&(void 0!==t&&(t&&(clearTimeout(t),t=void 0),s.autoplay.running=!1,n("autoplayStop"),!0))}function c(e){s.autoplay.running&&(s.autoplay.paused||(t&&clearTimeout(t),s.autoplay.paused=!0,0!==e&&s.params.autoplay.waitForTransition?["transitionend","webkitTransitionEnd"].forEach((e=>{s.$wrapperEl[0].addEventListener(e,h)})):(s.autoplay.paused=!1,l())))}function u(){const e=a();"hidden"===e.visibilityState&&s.autoplay.running&&c(),"visible"===e.visibilityState&&s.autoplay.paused&&(l(),s.autoplay.paused=!1)}function h(e){s&&!s.destroyed&&s.$wrapperEl&&e.target===s.$wrapperEl[0]&&(["transitionend","webkitTransitionEnd"].forEach((e=>{s.$wrapperEl[0].removeEventListener(e,h)})),s.autoplay.paused=!1,s.autoplay.running?l():d())}function m(){s.params.autoplay.disableOnInteraction?d():(n("autoplayPause"),c()),["transitionend","webkitTransitionEnd"].forEach((e=>{s.$wrapperEl[0].removeEventListener(e,h)}))}function f(){s.params.autoplay.disableOnInteraction||(s.autoplay.paused=!1,n("autoplayResume"),l())}s.autoplay={running:!1,paused:!1},i({autoplay:{enabled:!1,delay:3e3,waitForTransition:!0,disableOnInteraction:!0,stopOnLastSlide:!1,reverseDirection:!1,pauseOnMouseEnter:!1}}),r("init",(()=>{if(s.params.autoplay.enabled){o();a().addEventListener("visibilitychange",u),s.params.autoplay.pauseOnMouseEnter&&(s.$el.on("mouseenter",m),s.$el.on("mouseleave",f))}})),r("beforeTransitionStart",((e,t,a)=>{s.autoplay.running&&(a||!s.params.autoplay.disableOnInteraction?s.autoplay.pause(t):d())})),r("sliderFirstMove",(()=>{s.autoplay.running&&(s.params.autoplay.disableOnInteraction?d():c())})),r("touchEnd",(()=>{s.params.cssMode&&s.autoplay.paused&&!s.params.autoplay.disableOnInteraction&&l()})),r("destroy",(()=>{s.$el.off("mouseenter",m),s.$el.off("mouseleave",f),s.autoplay.running&&d();a().removeEventListener("visibilitychange",u)})),Object.assign(s.autoplay,{pause:c,run:l,start:o,stop:d})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({thumbs:{swiper:null,multipleActiveThumbs:!0,autoScrollOffset:0,slideThumbActiveClass:"swiper-slide-thumb-active",thumbsContainerClass:"swiper-thumbs"}});let i=!1,r=!1;function n(){const e=t.thumbs.swiper;if(!e||e.destroyed)return;const s=e.clickedIndex,a=e.clickedSlide;if(a&&d(a).hasClass(t.params.thumbs.slideThumbActiveClass))return;if(null==s)return;let i;if(i=e.params.loop?parseInt(d(e.clickedSlide).attr("data-swiper-slide-index"),10):s,t.params.loop){let e=t.activeIndex;t.slides.eq(e).hasClass(t.params.slideDuplicateClass)&&(t.loopFix(),t._clientLeft=t.$wrapperEl[0].clientLeft,e=t.activeIndex);const s=t.slides.eq(e).prevAll(`[data-swiper-slide-index="${i}"]`).eq(0).index(),a=t.slides.eq(e).nextAll(`[data-swiper-slide-index="${i}"]`).eq(0).index();i=void 0===s?a:void 0===a?s:a-e1&&!t.params.centeredSlides&&(i=t.params.slidesPerView),t.params.thumbs.multipleActiveThumbs||(i=1),i=Math.floor(i),s.slides.removeClass(r),s.params.loop||s.params.virtual&&s.params.virtual.enabled)for(let e=0;e1?a:o:a-ot.previousIndex?"next":"prev"}else i=t.realIndex,r=i>t.previousIndex?"next":"prev";l&&(i+="next"===r?n:-1*n),s.visibleSlidesIndexes&&s.visibleSlidesIndexes.indexOf(i)<0&&(s.params.centeredSlides?i=i>o?i-Math.floor(a/2)+1:i+Math.floor(a/2)-1:i>o&&s.params.slidesPerGroup,s.slideTo(i,e?0:void 0))}}t.thumbs={swiper:null},a("beforeInit",(()=>{const{thumbs:e}=t.params;e&&e.swiper&&(l(),o(!0))})),a("slideChange update resize observerUpdate",(()=>{o()})),a("setTransition",((e,s)=>{const a=t.thumbs.swiper;a&&!a.destroyed&&a.setTransition(s)})),a("beforeDestroy",(()=>{const e=t.thumbs.swiper;e&&!e.destroyed&&r&&e.destroy()})),Object.assign(t.thumbs,{init:l,update:o})},function(e){let{swiper:t,extendParams:s,emit:a,once:i}=e;s({freeMode:{enabled:!1,momentum:!0,momentumRatio:1,momentumBounce:!0,momentumBounceRatio:1,momentumVelocityRatio:1,sticky:!1,minimumVelocity:.02}}),Object.assign(t,{freeMode:{onTouchStart:function(){const e=t.getTranslate();t.setTranslate(e),t.setTransition(0),t.touchEventsData.velocities.length=0,t.freeMode.onTouchEnd({currentPos:t.rtl?t.translate:-t.translate})},onTouchMove:function(){const{touchEventsData:e,touches:s}=t;0===e.velocities.length&&e.velocities.push({position:s[t.isHorizontal()?"startX":"startY"],time:e.touchStartTime}),e.velocities.push({position:s[t.isHorizontal()?"currentX":"currentY"],time:u()})},onTouchEnd:function(e){let{currentPos:s}=e;const{params:r,$wrapperEl:n,rtlTranslate:l,snapGrid:o,touchEventsData:d}=t,c=u()-d.touchStartTime;if(s<-t.minTranslate())t.slideTo(t.activeIndex);else if(s>-t.maxTranslate())t.slides.length1){const e=d.velocities.pop(),s=d.velocities.pop(),a=e.position-s.position,i=e.time-s.time;t.velocity=a/i,t.velocity/=2,Math.abs(t.velocity)150||u()-e.time>300)&&(t.velocity=0)}else t.velocity=0;t.velocity*=r.freeMode.momentumVelocityRatio,d.velocities.length=0;let e=1e3*r.freeMode.momentumRatio;const s=t.velocity*e;let c=t.translate+s;l&&(c=-c);let p,h=!1;const m=20*Math.abs(t.velocity)*r.freeMode.momentumBounceRatio;let f;if(ct.minTranslate())r.freeMode.momentumBounce?(c-t.minTranslate()>m&&(c=t.minTranslate()+m),p=t.minTranslate(),h=!0,d.allowMomentumBounce=!0):c=t.minTranslate(),r.loop&&r.centeredSlides&&(f=!0);else if(r.freeMode.sticky){let e;for(let t=0;t-c){e=t;break}c=Math.abs(o[e]-c){t.loopFix()})),0!==t.velocity){if(e=l?Math.abs((-c-t.translate)/t.velocity):Math.abs((c-t.translate)/t.velocity),r.freeMode.sticky){const s=Math.abs((l?-c:c)-t.translate),a=t.slidesSizesGrid[t.activeIndex];e=s{t&&!t.destroyed&&d.allowMomentumBounce&&(a("momentumBounce"),t.setTransition(r.speed),setTimeout((()=>{t.setTranslate(p),n.transitionEnd((()=>{t&&!t.destroyed&&t.transitionEnd()}))}),0))}))):t.velocity?(a("_freeModeNoMomentumRelease"),t.updateProgress(c),t.setTransition(e),t.setTranslate(c),t.transitionStart(!0,t.swipeDirection),t.animating||(t.animating=!0,n.transitionEnd((()=>{t&&!t.destroyed&&t.transitionEnd()})))):t.updateProgress(c),t.updateActiveIndex(),t.updateSlidesClasses()}else{if(r.freeMode.sticky)return void t.slideToClosest();r.freeMode&&a("_freeModeNoMomentumRelease")}(!r.freeMode.momentum||c>=r.longSwipesMs)&&(t.updateProgress(),t.updateActiveIndex(),t.updateSlidesClasses())}}}})},function(e){let t,s,a,{swiper:i,extendParams:r}=e;r({grid:{rows:1,fill:"column"}}),i.grid={initSlides:e=>{const{slidesPerView:r}=i.params,{rows:n,fill:l}=i.params.grid;s=t/n,a=Math.floor(e/n),t=Math.floor(e/n)===e/n?e:Math.ceil(e/n)*n,"auto"!==r&&"row"===l&&(t=Math.max(t,r*n))},updateSlide:(e,r,n,l)=>{const{slidesPerGroup:o,spaceBetween:d}=i.params,{rows:c,fill:p}=i.params.grid;let u,h,m;if("row"===p&&o>1){const s=Math.floor(e/(o*c)),a=e-c*o*s,i=0===s?o:Math.min(Math.ceil((n-s*c*o)/c),o);m=Math.floor(a/i),h=a-m*i+s*o,u=h+m*t/c,r.css({"-webkit-order":u,order:u})}else"column"===p?(h=Math.floor(e/c),m=e-h*c,(h>a||h===a&&m===c-1)&&(m+=1,m>=c&&(m=0,h+=1))):(m=Math.floor(e/s),h=e-m*s);r.css(l("margin-top"),0!==m?d&&`${d}px`:"")},updateWrapperSize:(e,s,a)=>{const{spaceBetween:r,centeredSlides:n,roundLengths:l}=i.params,{rows:o}=i.params.grid;if(i.virtualSize=(e+r)*t,i.virtualSize=Math.ceil(i.virtualSize/o)-r,i.$wrapperEl.css({[a("width")]:`${i.virtualSize+r}px`}),n){s.splice(0,s.length);const e=[];for(let t=0;t{const{slides:e}=t,s=t.params.fadeEffect;for(let a=0;a{const{transformEl:s}=t.params.fadeEffect;(s?t.slides.find(s):t.slides).transition(e),ae({swiper:t,duration:e,transformEl:s,allSlides:!0})},overwriteParams:()=>({slidesPerView:1,slidesPerGroup:1,watchSlidesProgress:!0,spaceBetween:0,virtualTranslate:!t.params.cssMode})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({cubeEffect:{slideShadows:!0,shadow:!0,shadowOffset:20,shadowScale:.94}});const i=(e,t,s)=>{let a=s?e.find(".swiper-slide-shadow-left"):e.find(".swiper-slide-shadow-top"),i=s?e.find(".swiper-slide-shadow-right"):e.find(".swiper-slide-shadow-bottom");0===a.length&&(a=d(`
`),e.append(a)),0===i.length&&(i=d(`
`),e.append(i)),a.length&&(a[0].style.opacity=Math.max(-t,0)),i.length&&(i[0].style.opacity=Math.max(t,0))};te({effect:"cube",swiper:t,on:a,setTranslate:()=>{const{$el:e,$wrapperEl:s,slides:a,width:r,height:n,rtlTranslate:l,size:o,browser:c}=t,p=t.params.cubeEffect,u=t.isHorizontal(),h=t.virtual&&t.params.virtual.enabled;let m,f=0;p.shadow&&(u?(m=s.find(".swiper-cube-shadow"),0===m.length&&(m=d('
'),s.append(m)),m.css({height:`${r}px`})):(m=e.find(".swiper-cube-shadow"),0===m.length&&(m=d('
'),e.append(m))));for(let e=0;e-1&&(f=90*s+90*d,l&&(f=90*-s-90*d)),t.transform(v),p.slideShadows&&i(t,d,u)}if(s.css({"-webkit-transform-origin":`50% 50% -${o/2}px`,"transform-origin":`50% 50% -${o/2}px`}),p.shadow)if(u)m.transform(`translate3d(0px, ${r/2+p.shadowOffset}px, ${-r/2}px) rotateX(90deg) rotateZ(0deg) scale(${p.shadowScale})`);else{const e=Math.abs(f)-90*Math.floor(Math.abs(f)/90),t=1.5-(Math.sin(2*e*Math.PI/360)/2+Math.cos(2*e*Math.PI/360)/2),s=p.shadowScale,a=p.shadowScale/t,i=p.shadowOffset;m.transform(`scale3d(${s}, 1, ${a}) translate3d(0px, ${n/2+i}px, ${-n/2/a}px) rotateX(-90deg)`)}const g=c.isSafari||c.isWebView?-o/2:0;s.transform(`translate3d(0px,0,${g}px) rotateX(${t.isHorizontal()?0:f}deg) rotateY(${t.isHorizontal()?-f:0}deg)`),s[0].style.setProperty("--swiper-cube-translate-z",`${g}px`)},setTransition:e=>{const{$el:s,slides:a}=t;a.transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e),t.params.cubeEffect.shadow&&!t.isHorizontal()&&s.find(".swiper-cube-shadow").transition(e)},recreateShadows:()=>{const e=t.isHorizontal();t.slides.each((t=>{const s=Math.max(Math.min(t.progress,1),-1);i(d(t),s,e)}))},getEffectParams:()=>t.params.cubeEffect,perspective:()=>!0,overwriteParams:()=>({slidesPerView:1,slidesPerGroup:1,watchSlidesProgress:!0,resistanceRatio:0,spaceBetween:0,centeredSlides:!1,virtualTranslate:!0})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({flipEffect:{slideShadows:!0,limitRotation:!0,transformEl:null}});const i=(e,s,a)=>{let i=t.isHorizontal()?e.find(".swiper-slide-shadow-left"):e.find(".swiper-slide-shadow-top"),r=t.isHorizontal()?e.find(".swiper-slide-shadow-right"):e.find(".swiper-slide-shadow-bottom");0===i.length&&(i=ie(a,e,t.isHorizontal()?"left":"top")),0===r.length&&(r=ie(a,e,t.isHorizontal()?"right":"bottom")),i.length&&(i[0].style.opacity=Math.max(-s,0)),r.length&&(r[0].style.opacity=Math.max(s,0))};te({effect:"flip",swiper:t,on:a,setTranslate:()=>{const{slides:e,rtlTranslate:s}=t,a=t.params.flipEffect;for(let r=0;r{const{transformEl:s}=t.params.flipEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e),ae({swiper:t,duration:e,transformEl:s})},recreateShadows:()=>{const e=t.params.flipEffect;t.slides.each((s=>{const a=d(s);let r=a[0].progress;t.params.flipEffect.limitRotation&&(r=Math.max(Math.min(s.progress,1),-1)),i(a,r,e)}))},getEffectParams:()=>t.params.flipEffect,perspective:()=>!0,overwriteParams:()=>({slidesPerView:1,slidesPerGroup:1,watchSlidesProgress:!0,spaceBetween:0,virtualTranslate:!t.params.cssMode})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({coverflowEffect:{rotate:50,stretch:0,depth:100,scale:1,modifier:1,slideShadows:!0,transformEl:null}}),te({effect:"coverflow",swiper:t,on:a,setTranslate:()=>{const{width:e,height:s,slides:a,slidesSizesGrid:i}=t,r=t.params.coverflowEffect,n=t.isHorizontal(),l=t.translate,o=n?e/2-l:s/2-l,d=n?r.rotate:-r.rotate,c=r.depth;for(let e=0,t=a.length;e0?p:0),s.length&&(s[0].style.opacity=-p>0?-p:0)}}},setTransition:e=>{const{transformEl:s}=t.params.coverflowEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow-top, .swiper-slide-shadow-right, .swiper-slide-shadow-bottom, .swiper-slide-shadow-left").transition(e)},perspective:()=>!0,overwriteParams:()=>({watchSlidesProgress:!0})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({creativeEffect:{transformEl:null,limitProgress:1,shadowPerProgress:!1,progressMultiplier:1,perspective:!0,prev:{translate:[0,0,0],rotate:[0,0,0],opacity:1,scale:1},next:{translate:[0,0,0],rotate:[0,0,0],opacity:1,scale:1}}});const i=e=>"string"==typeof e?e:`${e}px`;te({effect:"creative",swiper:t,on:a,setTranslate:()=>{const{slides:e,$wrapperEl:s,slidesSizesGrid:a}=t,r=t.params.creativeEffect,{progressMultiplier:n}=r,l=t.params.centeredSlides;if(l){const e=a[0]/2-t.params.slidesOffsetBefore||0;s.transform(`translateX(calc(50% - ${e}px))`)}for(let s=0;s0&&(f=r.prev,m=!0),u.forEach(((e,t)=>{u[t]=`calc(${e}px + (${i(f.translate[t])} * ${Math.abs(d*n)}))`})),h.forEach(((e,t)=>{h[t]=f.rotate[t]*Math.abs(d*n)})),a[0].style.zIndex=-Math.abs(Math.round(o))+e.length;const g=u.join(", "),v=`rotateX(${h[0]}deg) rotateY(${h[1]}deg) rotateZ(${h[2]}deg)`,w=c<0?`scale(${1+(1-f.scale)*c*n})`:`scale(${1-(1-f.scale)*c*n})`,b=c<0?1+(1-f.opacity)*c*n:1-(1-f.opacity)*c*n,x=`translate3d(${g}) ${v} ${w}`;if(m&&f.shadow||!m){let e=a.children(".swiper-slide-shadow");if(0===e.length&&f.shadow&&(e=ie(r,a)),e.length){const t=r.shadowPerProgress?d*(1/r.limitProgress):d;e[0].style.opacity=Math.min(Math.max(Math.abs(t),0),1)}}const y=se(r,a);y.transform(x).css({opacity:b}),f.origin&&y.css("transform-origin",f.origin)}},setTransition:e=>{const{transformEl:s}=t.params.creativeEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow").transition(e),ae({swiper:t,duration:e,transformEl:s,allSlides:!0})},perspective:()=>t.params.creativeEffect.perspective,overwriteParams:()=>({watchSlidesProgress:!0,virtualTranslate:!t.params.cssMode})})},function(e){let{swiper:t,extendParams:s,on:a}=e;s({cardsEffect:{slideShadows:!0,transformEl:null,rotate:!0,perSlideRotate:2,perSlideOffset:8}}),te({effect:"cards",swiper:t,on:a,setTranslate:()=>{const{slides:e,activeIndex:s}=t,a=t.params.cardsEffect,{startTranslate:i,isTouched:r}=t.touchEventsData,n=t.translate;for(let l=0;l0&&c<1&&(r||t.params.cssMode)&&n-1&&(r||t.params.cssMode)&&n>i;if(b||x){const e=(1-Math.abs((Math.abs(c)-.5)/.5))**.5;g+=-28*c*e,f+=-.5*e,v+=96*e,h=-25*e*Math.abs(c)+"%"}if(u=c<0?`calc(${u}px + (${v*Math.abs(c)}%))`:c>0?`calc(${u}px + (-${v*Math.abs(c)}%))`:`${u}px`,!t.isHorizontal()){const e=h;h=u,u=e}const y=c<0?""+(1+(1-f)*c):""+(1-(1-f)*c),E=`\n translate3d(${u}, ${h}, ${m}px)\n rotateZ(${a.rotate?g:0}deg)\n scale(${y})\n `;if(a.slideShadows){let e=o.find(".swiper-slide-shadow");0===e.length&&(e=ie(a,o)),e.length&&(e[0].style.opacity=Math.min(Math.max((Math.abs(c)-.5)/.5,0),1))}o[0].style.zIndex=-Math.abs(Math.round(d))+e.length;se(a,o).transform(E)}},setTransition:e=>{const{transformEl:s}=t.params.cardsEffect;(s?t.slides.find(s):t.slides).transition(e).find(".swiper-slide-shadow").transition(e),ae({swiper:t,duration:e,transformEl:s})},perspective:()=>!0,overwriteParams:()=>({watchSlidesProgress:!0,virtualTranslate:!t.params.cssMode})})}];return V.use(re),V})); +//# sourceMappingURL=swiper-bundle.min.js.map \ No newline at end of file diff --git a/apps/smp-server/static/media/tailwind.css b/apps/smp-server/static/media/tailwind.css new file mode 100644 index 000000000..04ea69992 --- /dev/null +++ b/apps/smp-server/static/media/tailwind.css @@ -0,0 +1,3058 @@ +/* +! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.visible { + visibility: visible; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.-left-10 { + left: -2.5rem; +} + +.bottom-0 { + bottom: 0px; +} + +.bottom-\[80px\] { + bottom: 80px; +} + +.left-0 { + left: 0px; +} + +.left-1 { + left: 0.25rem; +} + +.left-\[-3px\] { + left: -3px; +} + +.right-0 { + right: 0px; +} + +.right-1 { + right: 0.25rem; +} + +.right-\[-6px\] { + right: -6px; +} + +.top-0 { + top: 0px; +} + +.top-10 { + top: 2.5rem; +} + +.top-11 { + top: 2.75rem; +} + +.top-\[52\%\] { + top: 52%; +} + +.top-\[66px\] { + top: 66px; +} + +.top-full { + top: 100%; +} + +.z-10 { + z-index: 10; +} + +.z-50 { + z-index: 50; +} + +.z-\[10000\] { + z-index: 10000; +} + +.z-\[10001\] { + z-index: 10001; +} + +.z-\[49\] { + z-index: 49; +} + +.float-right { + float: right; +} + +.m-1 { + margin: 0.25rem; +} + +.m-auto { + margin: auto; +} + +.\!my-4 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.mx-5 { + margin-left: 1.25rem; + margin-right: 1.25rem; +} + +.my-10 { + margin-top: 2.5rem; + margin-bottom: 2.5rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-5 { + margin-top: 1.25rem; + margin-bottom: 1.25rem; +} + +.my-\[40px\] { + margin-top: 40px; + margin-bottom: 40px; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-14 { + margin-bottom: 3.5rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-20 { + margin-bottom: 5rem; +} + +.mb-24 { + margin-bottom: 6rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-32 { + margin-bottom: 8rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-7 { + margin-bottom: 1.75rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mb-9 { + margin-bottom: 2.25rem; +} + +.mb-\[11px\] { + margin-bottom: 11px; +} + +.mb-\[12px\] { + margin-bottom: 12px; +} + +.mb-\[16px\] { + margin-bottom: 16px; +} + +.mb-\[20px\] { + margin-bottom: 20px; +} + +.mb-\[24px\] { + margin-bottom: 24px; +} + +.mb-\[28px\] { + margin-bottom: 28px; +} + +.mb-\[30px\] { + margin-bottom: 30px; +} + +.mb-\[32px\] { + margin-bottom: 32px; +} + +.mb-\[36px\] { + margin-bottom: 36px; +} + +.mb-\[40px\] { + margin-bottom: 40px; +} + +.mb-\[46px\] { + margin-bottom: 46px; +} + +.mb-\[50px\] { + margin-bottom: 50px; +} + +.mb-\[54px\] { + margin-bottom: 54px; +} + +.mb-\[62px\] { + margin-bottom: 62px; +} + +.mb-\[74px\] { + margin-bottom: 74px; +} + +.mb-\[75px\] { + margin-bottom: 75px; +} + +.mb-\[76px\] { + margin-bottom: 76px; +} + +.mb-\[80px\] { + margin-bottom: 80px; +} + +.ml-\[-15px\] { + margin-left: -15px; +} + +.ml-\[-6px\] { + margin-left: -6px; +} + +.mr-\[-4px\] { + margin-right: -4px; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-\[-100px\] { + margin-top: -100px; +} + +.mt-\[10px\] { + margin-top: 10px; +} + +.mt-\[14px\] { + margin-top: 14px; +} + +.mt-\[30px\] { + margin-top: 30px; +} + +.mt-\[60px\] { + margin-top: 60px; +} + +.mt-\[66px\] { + margin-top: 66px; +} + +.mt-\[74px\] { + margin-top: 74px; +} + +.mt-\[8px\] { + margin-top: 8px; +} + +.mt-auto { + margin-top: auto; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.grid { + display: grid; +} + +.hidden { + display: none; +} + +.h-1 { + height: 0.25rem; +} + +.h-11 { + height: 2.75rem; +} + +.h-4 { + height: 1rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-\[110px\] { + height: 110px; +} + +.h-\[131px\] { + height: 131px; +} + +.h-\[138px\] { + height: 138px; +} + +.h-\[180px\] { + height: 180px; +} + +.h-\[1px\] { + height: 1px; +} + +.h-\[215px\] { + height: 215px; +} + +.h-\[322px\] { + height: 322px; +} + +.h-\[32px\] { + height: 32px; +} + +.h-\[340px\] { + height: 340px; +} + +.h-\[34px\] { + height: 34px; +} + +.h-\[36px\] { + height: 36px; +} + +.h-\[40px\] { + height: 40px; +} + +.h-\[44px\] { + height: 44px; +} + +.h-\[46px\] { + height: 46px; +} + +.h-\[480px\] { + height: 480px; +} + +.h-\[52px\] { + height: 52px; +} + +.h-\[55\%\] { + height: 55%; +} + +.h-\[66px\] { + height: 66px; +} + +.h-\[80px\] { + height: 80px; +} + +.h-\[calc\(100\%-81\.42px\)\] { + height: calc(100% - 81.42px); +} + +.h-\[inherit\] { + height: inherit; +} + +.h-auto { + height: auto; +} + +.h-fit { + height: -moz-fit-content; + height: fit-content; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + +.max-h-\[226px\] { + max-height: 226px; +} + +.max-h-\[50px\] { + max-height: 50px; +} + +.min-h-\[131px\] { + min-height: 131px; +} + +.min-h-\[200px\] { + min-height: 200px; +} + +.min-h-\[620px\] { + min-height: 620px; +} + +.min-h-\[inherit\] { + min-height: inherit; +} + +.w-4 { + width: 1rem; +} + +.w-8 { + width: 2rem; +} + +.w-\[175px\] { + width: 175px; +} + +.w-\[235px\] { + width: 235px; +} + +.w-\[238px\] { + width: 238px; +} + +.w-\[36px\] { + width: 36px; +} + +.w-\[44px\] { + width: 44px; +} + +.w-\[630px\] { + width: 630px; +} + +.w-\[inherit\] { + width: inherit; +} + +.w-auto { + width: auto; +} + +.w-full { + width: 100%; +} + +.min-w-\[152px\] { + min-width: 152px; +} + +.min-w-\[170px\] { + min-width: 170px; +} + +.min-w-\[180px\] { + min-width: 180px; +} + +.min-w-\[210px\] { + min-width: 210px; +} + +.min-w-\[300px\] { + min-width: 300px; +} + +.max-w-\[223px\] { + max-width: 223px; +} + +.max-w-\[230px\] { + max-width: 230px; +} + +.max-w-\[234px\] { + max-width: 234px; +} + +.max-w-\[240px\] { + max-width: 240px; +} + +.max-w-\[265px\] { + max-width: 265px; +} + +.max-w-\[280px\] { + max-width: 280px; +} + +.max-w-\[294px\] { + max-width: 294px; +} + +.max-w-\[400px\] { + max-width: 400px; +} + +.max-w-\[448px\] { + max-width: 448px; +} + +.max-w-\[468px\] { + max-width: 468px; +} + +.max-w-\[475px\] { + max-width: 475px; +} + +.max-w-\[500px\] { + max-width: 500px; +} + +.max-w-\[540px\] { + max-width: 540px; +} + +.max-w-\[541px\] { + max-width: 541px; +} + +.max-w-\[602px\] { + max-width: 602px; +} + +.max-w-\[617px\] { + max-width: 617px; +} + +.max-w-\[900px\] { + max-width: 900px; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-\[1\] { + flex: 1; +} + +.flex-\[2\.5\] { + flex: 2.5; +} + +.border-separate { + border-collapse: separate; +} + +.border-spacing-x-5 { + --tw-border-spacing-x: 1.25rem; + border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); +} + +.border-spacing-y-2 { + --tw-border-spacing-y: 0.5rem; + border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y); +} + +.translate-x-\[-30\%\] { + --tw-translate-x: -30%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-\[-30\%\] { + --tw-translate-y: -30%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.cursor-pointer { + cursor: pointer; +} + +.resize { + resize: both; +} + +.list-inside { + list-style-position: inside; +} + +.list-decimal { + list-style-type: decimal; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-col-reverse { + flex-direction: column-reverse; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-10 { + gap: 2.5rem; +} + +.gap-12 { + gap: 3rem; +} + +.gap-16 { + gap: 4rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-5 { + gap: 1.25rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-\[150px\] { + gap: 150px; +} + +.gap-\[20px\] { + gap: 20px; +} + +.gap-\[50px\] { + gap: 50px; +} + +.gap-x-10 { + -moz-column-gap: 2.5rem; + column-gap: 2.5rem; +} + +.gap-y-32 { + row-gap: 8rem; +} + +.self-center { + align-self: center; +} + +.self-stretch { + align-self: stretch; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.break-words { + overflow-wrap: break-word; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-\[12px\] { + border-radius: 12px; +} + +.rounded-\[20px\] { + border-radius: 20px; +} + +.rounded-\[30px\] { + border-radius: 30px; +} + +.rounded-\[34px\] { + border-radius: 34px; +} + +.rounded-\[4px\] { + border-radius: 4px; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-b-\[20px\] { + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; +} + +.border { + border-width: 1px; +} + +.border-\[1px\] { + border-width: 1px; +} + +.border-solid { + border-style: solid; +} + +.border-\[\#0053D0\] { + --tw-border-opacity: 1; + border-color: rgb(0 83 208 / var(--tw-border-opacity)); +} + +.border-\[\#859096\] { + --tw-border-opacity: 1; + border-color: rgb(133 144 150 / var(--tw-border-opacity)); +} + +.border-\[\#A8B0B4\] { + --tw-border-opacity: 1; + border-color: rgb(168 176 180 / var(--tw-border-opacity)); +} + +.border-primary-light { + --tw-border-opacity: 1; + border-color: rgb(0 83 208 / var(--tw-border-opacity)); +} + +.border-opacity-60 { + --tw-border-opacity: 0.6; +} + +.bg-\[\#0053D0\] { + --tw-bg-opacity: 1; + background-color: rgb(0 83 208 / var(--tw-bg-opacity)); +} + +.bg-\[\#0197FF\] { + --tw-bg-opacity: 1; + background-color: rgb(1 151 255 / var(--tw-bg-opacity)); +} + +.bg-\[\#17203D\] { + --tw-bg-opacity: 1; + background-color: rgb(23 32 61 / var(--tw-bg-opacity)); +} + +.bg-\[\#48F6C2\] { + --tw-bg-opacity: 1; + background-color: rgb(72 246 194 / var(--tw-bg-opacity)); +} + +.bg-\[\#D9E7ED\] { + --tw-bg-opacity: 1; + background-color: rgb(217 231 237 / var(--tw-bg-opacity)); +} + +.bg-\[\#D9ECFF\] { + --tw-bg-opacity: 1; + background-color: rgb(217 236 255 / var(--tw-bg-opacity)); +} + +.bg-\[\#F0F1F2\] { + --tw-bg-opacity: 1; + background-color: rgb(240 241 242 / var(--tw-bg-opacity)); +} + +.bg-\[\#F3F6F7\] { + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); +} + +.bg-\[\#fff\] { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); +} + +.bg-card-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-card-desc-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(217 231 237 / var(--tw-bg-opacity)); +} + +.bg-primary-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-primary-light { + --tw-bg-opacity: 1; + background-color: rgb(0 83 208 / var(--tw-bg-opacity)); +} + +.bg-secondary-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-yellow-200 { + --tw-bg-opacity: 1; + background-color: rgb(254 240 138 / var(--tw-bg-opacity)); +} + +.fill-\[rgb\(60\2c 60\2c 60\)\] { + fill: rgb(60,60,60); +} + +.fill-black { + fill: #0D0E12; +} + +.fill-grey-black { + fill: #3F484B; +} + +.fill-primary-light { + fill: #0053D0; +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.\!py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.px-0 { + padding-left: 0px; + padding-right: 0px; +} + +.px-20 { + padding-left: 5rem; + padding-right: 5rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.px-\[20px\] { + padding-left: 20px; + padding-right: 20px; +} + +.px-\[34px\] { + padding-left: 34px; + padding-right: 34px; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.py-\[20px\] { + padding-top: 20px; + padding-bottom: 20px; +} + +.py-\[24px\] { + padding-top: 24px; + padding-bottom: 24px; +} + +.py-\[50px\] { + padding-top: 50px; + padding-bottom: 50px; +} + +.py-\[70px\] { + padding-top: 70px; + padding-bottom: 70px; +} + +.py-\[75px\] { + padding-top: 75px; + padding-bottom: 75px; +} + +.py-\[90px\] { + padding-top: 90px; + padding-bottom: 90px; +} + +.py-\[95px\] { + padding-top: 95px; + padding-bottom: 95px; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-8 { + padding-bottom: 2rem; +} + +.pb-\[90px\] { + padding-bottom: 90px; +} + +.pr-10 { + padding-right: 2.5rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-5 { + padding-top: 1.25rem; +} + +.pt-\[106px\] { + padding-top: 106px; +} + +.pt-\[30px\] { + padding-top: 30px; +} + +.pt-\[40px\] { + padding-top: 40px; +} + +.pt-\[66px\] { + padding-top: 66px; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-\[12px\] { + font-size: 12px; +} + +.text-\[14px\] { + font-size: 14px; +} + +.text-\[16px\] { + font-size: 16px; +} + +.text-\[18px\] { + font-size: 18px; +} + +.text-\[20px\] { + font-size: 20px; +} + +.text-\[24px\] { + font-size: 24px; +} + +.text-\[25px\] { + font-size: 25px; +} + +.text-\[28px\] { + font-size: 28px; +} + +.text-\[30px\] { + font-size: 30px; +} + +.text-\[35px\] { + font-size: 35px; +} + +.text-\[38px\] { + font-size: 38px; +} + +.text-\[42px\] { + font-size: 42px; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.font-medium { + font-weight: 500; +} + +.font-normal { + font-weight: 400; +} + +.capitalize { + text-transform: capitalize; +} + +.italic { + font-style: italic; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-\[19px\] { + line-height: 19px; +} + +.leading-\[24px\] { + line-height: 24px; +} + +.leading-\[26px\] { + line-height: 26px; +} + +.leading-\[28px\] { + line-height: 28px; +} + +.leading-\[33px\] { + line-height: 33px; +} + +.leading-\[34px\] { + line-height: 34px; +} + +.leading-\[36px\] { + line-height: 36px; +} + +.leading-\[43px\] { + line-height: 43px; +} + +.leading-\[45px\] { + line-height: 45px; +} + +.leading-\[46px\] { + line-height: 46px; +} + +.tracking-\[0\.01em\] { + letter-spacing: 0.01em; +} + +.tracking-\[0\.02em\] { + letter-spacing: 0.02em; +} + +.tracking-\[0\.03em\] { + letter-spacing: 0.03em; +} + +.tracking-\[0\.04em\] { + letter-spacing: 0.04em; +} + +.tracking-\[0\.06em\] { + letter-spacing: 0.06em; +} + +.\!text-\[rgb\(60\2c 60\2c 60\)\] { + --tw-text-opacity: 1 !important; + color: rgb(60 60 60 / var(--tw-text-opacity)) !important; +} + +.\!text-primary-pressed-light { + --tw-text-opacity: 1 !important; + color: rgb(64 122 210 / var(--tw-text-opacity)) !important; +} + +.text-\[\#606C71\] { + --tw-text-opacity: 1; + color: rgb(96 108 113 / var(--tw-text-opacity)); +} + +.text-\[\#A8B0B4\] { + --tw-text-opacity: 1; + color: rgb(168 176 180 / var(--tw-text-opacity)); +} + +.text-\[\#DD0000\] { + --tw-text-opacity: 1; + color: rgb(221 0 0 / var(--tw-text-opacity)); +} + +.text-active-blue { + --tw-text-opacity: 1; + color: rgb(1 151 255 / var(--tw-text-opacity)); +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(13 14 18 / var(--tw-text-opacity)); +} + +.text-grey-black { + --tw-text-opacity: 1; + color: rgb(63 72 75 / var(--tw-text-opacity)); +} + +.text-primary-light { + --tw-text-opacity: 1; + color: rgb(0 83 208 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.underline { + text-decoration-line: underline; +} + +.\!no-underline { + text-decoration-line: none !important; +} + +.no-underline { + text-decoration-line: none; +} + +.underline-offset-2 { + text-underline-offset: 2px; +} + +.underline-offset-4 { + text-underline-offset: 4px; +} + +.opacity-100 { + opacity: 1; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0_0_3px_rgb\(60_72_88_\/_15\%\)\] { + --tw-shadow: 0 0 3px rgb(60 72 88 / 15%); + --tw-shadow-colored: 0 0 3px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0px_20px_30px_rgba\(0\2c 0\2c 0\2c 0\.12\)\] { + --tw-shadow: 0px 20px 30px rgba(0,0,0,0.12); + --tw-shadow-colored: 0px 20px 30px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0px_3px_12px_rgba\(0\2c 0\2c 0\2c 0\.2\)\] { + --tw-shadow: 0px 3px 12px rgba(0,0,0,0.2); + --tw-shadow-colored: 0px 3px 12px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[0px_50px_61px_rgba\(0\2c 0\2c 0\2c 0\.12\)\] { + --tw-shadow: 0px 50px 61px rgba(0,0,0,0.12); + --tw-shadow-colored: 0px 50px 61px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[2px_2px_0px_\#859096\] { + --tw-shadow: 2px 2px 0px #859096; + --tw-shadow-colored: 2px 2px 0px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-\[inset_0px_2px_2px_rgba\(0\2c 0\2c 0\2c 0\.15\)\] { + --tw-shadow: inset 0px 2px 2px rgba(0,0,0,0.15); + --tw-shadow-colored: inset 0px 2px 2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.invert { + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.placeholder\:text-base::-moz-placeholder { + font-size: 1rem; + line-height: 1.5rem; +} + +.placeholder\:text-base::placeholder { + font-size: 1rem; + line-height: 1.5rem; +} + +.placeholder\:font-normal::-moz-placeholder { + font-weight: 400; +} + +.placeholder\:font-normal::placeholder { + font-weight: 400; +} + +.placeholder\:tracking-\[0\.01em\]::-moz-placeholder { + letter-spacing: 0.01em; +} + +.placeholder\:tracking-\[0\.01em\]::placeholder { + letter-spacing: 0.01em; +} + +.placeholder\:text-grey-black::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(63 72 75 / var(--tw-text-opacity)); +} + +.placeholder\:text-grey-black::placeholder { + --tw-text-opacity: 1; + color: rgb(63 72 75 / var(--tw-text-opacity)); +} + +.before\:absolute::before { + content: var(--tw-content); + position: absolute; +} + +.before\:h-full::before { + content: var(--tw-content); + height: 100%; +} + +.before\:w-full::before { + content: var(--tw-content); + width: 100%; +} + +.before\:bg-black::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); +} + +.before\:bg-secondary-bg-light::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); +} + +.before\:opacity-90::before { + content: var(--tw-content); + opacity: 0.9; +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +:is([dir="ltr"] .ltr\:ml-8) { + margin-left: 2rem; +} + +:is([dir="ltr"] .ltr\:mr-auto) { + margin-right: auto; +} + +:is([dir="ltr"] .ltr\:\!hidden) { + display: none !important; +} + +:is([dir="ltr"] .ltr\:rounded-l-\[34px\]) { + border-top-left-radius: 34px; + border-bottom-left-radius: 34px; +} + +:is([dir="ltr"] .ltr\:rounded-r-\[34px\]) { + border-top-right-radius: 34px; + border-bottom-right-radius: 34px; +} + +:is([dir="rtl"] .rtl\:ml-auto) { + margin-left: auto; +} + +:is([dir="rtl"] .rtl\:mr-8) { + margin-right: 2rem; +} + +:is([dir="rtl"] .rtl\:\!hidden) { + display: none !important; +} + +:is([dir="rtl"] .rtl\:rounded-l-\[34px\]) { + border-top-left-radius: 34px; + border-bottom-left-radius: 34px; +} + +:is([dir="rtl"] .rtl\:rounded-r-\[34px\]) { + border-top-right-radius: 34px; + border-bottom-right-radius: 34px; +} + +:is([dir="rtl"] .rtl\:text-right) { + text-align: right; +} + +:is(.dark .dark\:block) { + display: block; +} + +:is(.dark .dark\:inline-block) { + display: inline-block; +} + +:is(.dark .dark\:flex) { + display: flex; +} + +:is(.dark .dark\:\!hidden) { + display: none !important; +} + +:is(.dark .dark\:hidden) { + display: none; +} + +:is(.dark .dark\:rounded-\[6px\]) { + border-radius: 6px; +} + +:is(.dark .dark\:border) { + border-width: 1px; +} + +:is(.dark .dark\:border-none) { + border-style: none; +} + +:is(.dark .dark\:border-primary-dark) { + --tw-border-opacity: 1; + border-color: rgb(112 240 249 / var(--tw-border-opacity)); +} + +:is(.dark .dark\:border-white) { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + +:is(.dark .dark\:bg-\[\#0C0B13\]) { + --tw-bg-opacity: 1; + background-color: rgb(12 11 19 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#0E2B57\]) { + --tw-bg-opacity: 1; + background-color: rgb(14 43 87 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#11182F\]) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 47 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#171F3A\]) { + --tw-bg-opacity: 1; + background-color: rgb(23 31 58 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[\#17203D\]) { + --tw-bg-opacity: 1; + background-color: rgb(23 32 61 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-\[rgba\(112\2c 240\2c 249\2c 0\.2\)\]) { + background-color: rgba(112,240,249,0.2); +} + +:is(.dark .dark\:bg-black) { + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-card-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(23 32 61 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-card-desc-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(27 50 92 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-grey-black) { + --tw-bg-opacity: 1; + background-color: rgb(63 72 75 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-primary-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(12 11 19 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-primary-dark) { + --tw-bg-opacity: 1; + background-color: rgb(112 240 249 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-secondary-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 47 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-transparent) { + background-color: transparent; +} + +:is(.dark .dark\:bg-opacity-\[0\.2\]) { + --tw-bg-opacity: 0.2; +} + +:is(.dark .dark\:bg-gradient-radial-mobile) { + background-image: radial-gradient(77.4% 73.09% at -3.68% 100%, #17203D 0%, #0C0B13 100%); +} + +:is(.dark .dark\:fill-primary-dark) { + fill: #70F0F9; +} + +:is(.dark .dark\:fill-white) { + fill: #fff; +} + +:is(.dark .dark\:\!text-white) { + --tw-text-opacity: 1 !important; + color: rgb(255 255 255 / var(--tw-text-opacity)) !important; +} + +:is(.dark .dark\:text-\[\#70F0F9\]) { + --tw-text-opacity: 1; + color: rgb(112 240 249 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-black) { + --tw-text-opacity: 1; + color: rgb(13 14 18 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-primary-dark) { + --tw-text-opacity: 1; + color: rgb(112 240 249 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:text-white) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:opacity-\[0\.1\]) { + opacity: 0.1; +} + +:is(.dark .dark\:opacity-\[0\.2\]) { + opacity: 0.2; +} + +:is(.dark .dark\:shadow-none) { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +:is(.dark .placeholder\:dark\:text-white)::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .placeholder\:dark\:text-white)::placeholder { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +:is(.dark .dark\:before\:bg-primary-bg-dark)::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(12 11 19 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:before\:bg-white)::before { + content: var(--tw-content); + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +@media (min-width: 640px) { + .sm\:block { + display: block; + } + + .sm\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:p-14 { + padding: 3.5rem; + } + + .sm\:px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; + } + + .sm\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } +} + +@media (min-width: 768px) { + .md\:static { + position: static; + } + + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:mb-16 { + margin-bottom: 4rem; + } + + .md\:mb-6 { + margin-bottom: 1.5rem; + } + + .md\:mb-8 { + margin-bottom: 2rem; + } + + .md\:mt-6 { + margin-top: 1.5rem; + } + + .md\:block { + display: block; + } + + .md\:inline-block { + display: inline-block; + } + + .md\:flex { + display: flex; + } + + .md\:hidden { + display: none; + } + + .md\:h-fit { + height: -moz-fit-content; + height: fit-content; + } + + .md\:max-h-\[660px\] { + max-height: 660px; + } + + .md\:min-h-fit { + min-height: -moz-fit-content; + min-height: fit-content; + } + + .md\:w-\[168px\] { + width: 168px; + } + + .md\:w-\[30\%\] { + width: 30%; + } + + .md\:w-\[70\%\] { + width: 70%; + } + + .md\:w-fit { + width: -moz-fit-content; + width: fit-content; + } + + .md\:max-w-\[1276px\] { + max-width: 1276px; + } + + .md\:max-w-\[220px\] { + max-width: 220px; + } + + .md\:max-w-\[490px\] { + max-width: 490px; + } + + .md\:flex-1 { + flex: 1 1 0%; + } + + .md\:flex-\[1\] { + flex: 1; + } + + .md\:flex-\[2\] { + flex: 2; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:flex-col-reverse { + flex-direction: column-reverse; + } + + .md\:items-start { + align-items: flex-start; + } + + .md\:items-center { + align-items: center; + } + + .md\:gap-14 { + gap: 3.5rem; + } + + .md\:gap-9 { + gap: 2.25rem; + } + + .md\:p-10 { + padding: 2.5rem; + } + + .md\:p-\[60px\] { + padding: 60px; + } + + .md\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .md\:px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; + } + + .md\:py-16 { + padding-top: 4rem; + padding-bottom: 4rem; + } + + .md\:py-7 { + padding-top: 1.75rem; + padding-bottom: 1.75rem; + } + + .md\:py-8 { + padding-top: 2rem; + padding-bottom: 2rem; + } + + .md\:text-left { + text-align: left; + } + + .md\:text-\[20px\] { + font-size: 20px; + } + + .md\:text-\[35px\] { + font-size: 35px; + } + + .md\:text-\[38px\] { + font-size: 38px; + } + + .md\:text-\[55px\] { + font-size: 55px; + } + + .md\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .md\:leading-\[43px\] { + line-height: 43px; + } + + .md\:leading-\[55px\] { + line-height: 55px; + } + + .md\:leading-\[63px\] { + line-height: 63px; + } + + :is([dir="rtl"] .md\:rtl\:inline-block) { + display: inline-block; + } + + :is([dir="rtl"] .md\:rtl\:text-right) { + text-align: right; + } + + :is(.dark .dark\:md\:inline-block) { + display: inline-block; + } + + :is(.dark .md\:dark\:inline-block) { + display: inline-block; + } +} + +@media (min-width: 1024px) { + .lg\:absolute { + position: absolute; + } + + .lg\:relative { + position: relative; + } + + .lg\:top-0 { + top: 0px; + } + + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:mb-20 { + margin-bottom: 5rem; + } + + .lg\:mb-8 { + margin-bottom: 2rem; + } + + .lg\:mb-\[54px\] { + margin-bottom: 54px; + } + + .lg\:mb-\[90px\] { + margin-bottom: 90px; + } + + .lg\:mt-0 { + margin-top: 0px; + } + + .lg\:mt-\[10px\] { + margin-top: 10px; + } + + .lg\:flex { + display: flex; + } + + .lg\:hidden { + display: none; + } + + .lg\:h-0 { + height: 0px; + } + + .lg\:h-\[642px\] { + height: 642px; + } + + .lg\:h-\[855px\] { + height: 855px; + } + + .lg\:h-\[888px\] { + height: 888px; + } + + .lg\:h-\[890px\] { + height: 890px; + } + + .lg\:h-\[950px\] { + height: 950px; + } + + .lg\:h-\[calc\(100vh-66px\)\] { + height: calc(100vh - 66px); + } + + .lg\:h-auto { + height: auto; + } + + .lg\:h-fit { + height: -moz-fit-content; + height: fit-content; + } + + .lg\:max-h-\[888px\] { + max-height: 888px; + } + + .lg\:w-2\/5 { + width: 40%; + } + + .lg\:w-3\/5 { + width: 60%; + } + + .lg\:w-auto { + width: auto; + } + + .lg\:w-full { + width: 100%; + } + + .lg\:max-w-\[240px\] { + max-width: 240px; + } + + .lg\:max-w-\[448px\] { + max-width: 448px; + } + + .lg\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:flex-col { + flex-direction: column; + } + + .lg\:items-start { + align-items: flex-start; + } + + .lg\:items-center { + align-items: center; + } + + .lg\:gap-0 { + gap: 0px; + } + + .lg\:gap-16 { + gap: 4rem; + } + + .lg\:gap-28 { + gap: 7rem; + } + + .lg\:gap-5 { + gap: 1.25rem; + } + + .lg\:gap-\[350px\] { + gap: 350px; + } + + .lg\:bg-transparent { + background-color: transparent; + } + + .lg\:bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + } + + .lg\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .lg\:px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; + } + + .lg\:px-7 { + padding-left: 1.75rem; + padding-right: 1.75rem; + } + + .lg\:px-\[20px\] { + padding-left: 20px; + padding-right: 20px; + } + + .lg\:px-\[50px\] { + padding-left: 50px; + padding-right: 50px; + } + + .lg\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .lg\:py-4 { + padding-top: 1rem; + padding-bottom: 1rem; + } + + .lg\:py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; + } + + .lg\:py-\[50px\] { + padding-top: 50px; + padding-bottom: 50px; + } + + .lg\:pt-0 { + padding-top: 0px; + } + + .lg\:text-center { + text-align: center; + } + + .lg\:text-\[32px\] { + font-size: 32px; + } + + .lg\:text-\[35px\] { + font-size: 35px; + } + + .lg\:text-\[38px\] { + font-size: 38px; + } + + .lg\:text-\[45px\] { + font-size: 45px; + } + + .lg\:text-\[65px\] { + font-size: 65px; + } + + .lg\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .lg\:leading-\[36px\] { + line-height: 36px; + } + + .lg\:leading-\[45px\] { + line-height: 45px; + } + + .lg\:shadow-\[0_0_3px_rgb\(60_72_88_\/_15\%\)\] { + --tw-shadow: 0 0 3px rgb(60 72 88 / 15%); + --tw-shadow-colored: 0 0 3px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + } + + :is([dir="ltr"] .ltr\:lg\:ml-4) { + margin-left: 1rem; + } + + :is([dir="ltr"] .ltr\:lg\:ml-5) { + margin-left: 1.25rem; + } + + :is([dir="ltr"] .lg\:ltr\:text-left) { + text-align: left; + } + + :is([dir="rtl"] .rtl\:lg\:mr-4) { + margin-right: 1rem; + } + + :is([dir="rtl"] .rtl\:lg\:mr-5) { + margin-right: 1.25rem; + } + + :is([dir="rtl"] .lg\:rtl\:text-right) { + text-align: right; + } + + :is(.dark .dark\:lg\:flex) { + display: flex; + } + + :is(.dark .dark\:lg\:hidden) { + display: none; + } + + :is(.dark .dark\:lg\:bg-black) { + --tw-bg-opacity: 1; + background-color: rgb(13 14 18 / var(--tw-bg-opacity)); + } + + :is(.dark .dark\:lg\:bg-gradient-radial) { + background-image: radial-gradient(88.77% 102.03% at 92.64% -13.22%, #17203D 0%, #0C0B13 100%); + } + + :is(.dark .dark\:lg\:bg-none) { + background-image: none; + } +} + +@media (min-width: 1280px) { + .xl\:absolute { + position: absolute; + } + + .xl\:sticky { + position: sticky; + } + + .xl\:mb-0 { + margin-bottom: 0px; + } + + .xl\:mb-8 { + margin-bottom: 2rem; + } + + .xl\:mb-\[25px\] { + margin-bottom: 25px; + } + + .xl\:block { + display: block; + } + + .xl\:flex { + display: flex; + } + + .xl\:hidden { + display: none; + } + + .xl\:h-\[888px\] { + height: 888px; + } + + .xl\:h-\[calc\(100vh-66px\)\] { + height: calc(100vh - 66px); + } + + .xl\:h-full { + height: 100%; + } + + .xl\:max-h-\[888px\] { + max-height: 888px; + } + + .xl\:min-h-\[565px\] { + min-height: 565px; + } + + .xl\:min-w-\[600px\] { + min-width: 600px; + } + + .xl\:max-w-\[600px\] { + max-width: 600px; + } + + .xl\:flex-row { + flex-direction: row; + } + + .xl\:flex-row-reverse { + flex-direction: row-reverse; + } + + .xl\:items-start { + align-items: flex-start; + } + + .xl\:items-center { + align-items: center; + } + + .xl\:justify-start { + justify-content: flex-start; + } + + .xl\:justify-between { + justify-content: space-between; + } + + .xl\:justify-around { + justify-content: space-around; + } + + .xl\:gap-10 { + gap: 2.5rem; + } + + .xl\:gap-8 { + gap: 2rem; + } + + .xl\:text-left { + text-align: left; + } + + .xl\:text-justify { + text-align: justify; + } + + .xl\:text-\[16px\] { + font-size: 16px; + } + + .xl\:bg-secondary-bg-light { + --tw-bg-opacity: 1; + background-color: rgb(243 246 247 / var(--tw-bg-opacity)); + } + + :is([dir="ltr"] .ltr\:xl\:ml-10) { + margin-left: 2.5rem; + } + + :is([dir="ltr"] .ltr\:xl\:ml-8) { + margin-left: 2rem; + } + + :is([dir="rtl"] .rtl\:xl\:mr-10) { + margin-right: 2.5rem; + } + + :is([dir="rtl"] .rtl\:xl\:mr-8) { + margin-right: 2rem; + } + + :is([dir="rtl"] .xl\:rtl\:text-right) { + text-align: right; + } + + :is(.dark .dark\:xl\:bg-secondary-bg-dark) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 47 / var(--tw-bg-opacity)); + } +} diff --git a/apps/smp-server/static/media/testflight.png b/apps/smp-server/static/media/testflight.png new file mode 100644 index 000000000..8111a69d5 Binary files /dev/null and b/apps/smp-server/static/media/testflight.png differ diff --git a/apps/smp-server/web/Static.hs b/apps/smp-server/web/Static.hs new file mode 100644 index 000000000..4d25f2067 --- /dev/null +++ b/apps/smp-server/web/Static.hs @@ -0,0 +1,176 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Static where + +import Control.Logger.Simple +import Control.Monad +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +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 Simplex.Messaging.Encoding.String (strEncode) +import Simplex.Messaging.Server.Information +import Simplex.Messaging.Server.Main (EmbeddedWebParams (..), WebHttpsParams (..)) +import Simplex.Messaging.Transport.Client (TransportHost (..)) +import Simplex.Messaging.Util (tshow) +import Static.Embedded as E +import System.Directory (createDirectoryIfMissing) +import System.FilePath +import UnliftIO.Concurrent (forkFinally) + +serveStaticFiles :: EmbeddedWebParams -> IO () +serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do + forM_ webHttpPort $ \port -> flip forkFinally (\e -> logError $ "HTTP server crashed: " <> tshow e) $ do + logInfo $ "Serving static site on port " <> tshow port + 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) + where + mkSettings port = setPort port defaultSettings + +generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO () +generateSite si onionHost sitePath = do + createDirectoryIfMissing True sitePath + B.writeFile (sitePath "index.html") $ serverInformation si onionHost + createDirectoryIfMissing True $ sitePath "media" + forM_ E.mediaContent $ \(path, bs) -> B.writeFile (sitePath "media" path) bs + createDirectoryIfMissing True $ sitePath "contact" + B.writeFile (sitePath "contact" "index.html") E.linkHtml + createDirectoryIfMissing True $ sitePath "invitation" + B.writeFile (sitePath "invitation" "index.html") E.linkHtml + logInfo $ "Generated static site contents at " <> tshow sitePath + +serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString +serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs + where + substs = substConfig <> maybe [] substInfo information <> [("onionHost", strEncode <$> onionHost)] + substConfig = + [ ( "persistence", + Just $ case persistence config of + SPMMemoryOnly -> "In-memory only" + SPMQueues -> "Queues" + SPMMessages -> "Queues and messages" + ), + ("messageExpiration", Just $ maybe "Never" (fromString . timedTTLText) $ messageExpiration config), + ("statsEnabled", Just . yesNo $ statsEnabled config), + ("newQueuesAllowed", Just . yesNo $ newQueuesAllowed config), + ("basicAuthEnabled", Just . yesNo $ basicAuthEnabled config) + ] + yesNo True = "Yes" + yesNo False = "No" + substInfo spi = + concat + [ basic, + maybe [("usageConditions", Nothing), ("usageAmendments", Nothing)] conds (usageConditions spi), + maybe [("operator", Nothing)] operatorE (operator spi), + maybe [("admin", Nothing)] admin (adminContacts spi), + maybe [("complaints", Nothing)] complaints (complaintsContacts spi), + maybe [("hosting", Nothing)] hostingE (hosting spi), + server + ] + where + basic = + [ ("sourceCode", Just . encodeUtf8 $ sourceCode spi), + ("website", encodeUtf8 <$> website spi) + ] + conds ServerConditions {conditions, amendments} = + [ ("usageConditions", Just $ encodeUtf8 conditions), + ("usageAmendments", encodeUtf8 <$> amendments) + ] + operatorE Entity {name, country} = + [ ("operator", Just ""), + ("operatorEntity", Just $ encodeUtf8 name), + ("operatorCountry", encodeUtf8 <$> country) + ] + admin ServerContactAddress {simplex, email, pgp} = + [ ("admin", Just ""), + ("adminSimplex", strEncode <$> simplex), + ("adminEmail", encodeUtf8 <$> email), + ("adminPGP", encodeUtf8 . pkURI <$> pgp), + ("adminPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp) + ] + complaints ServerContactAddress {simplex, email, pgp} = + [ ("complaints", Just ""), + ("complaintsSimplex", strEncode <$> simplex), + ("complaintsEmail", encodeUtf8 <$> email), + ("complaintsPGP", encodeUtf8 . pkURI <$> pgp), + ("complaintsPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp) + ] + hostingE Entity {name, country} = + [ ("hosting", Just ""), + ("hostingEntity", Just $ encodeUtf8 name), + ("hostingCountry", encodeUtf8 <$> country) + ] + server = + [ ("serverCountry", fmap encodeUtf8 $ serverCountry =<< information) + ] + +-- Copy-pasted from simplex-chat Simplex.Chat.Types.Preferences +{-# INLINE timedTTLText #-} +timedTTLText :: (Integral i, Show i) => i -> String +timedTTLText 0 = "0 sec" +timedTTLText ttl = do + let (m', s) = ttl `quotRem` 60 + (h', m) = m' `quotRem` 60 + (d', h) = h' `quotRem` 24 + (mm, d) = d' `quotRem` 30 + unwords $ + [mms mm | mm /= 0] + <> [ds d | d /= 0] + <> [hs h | h /= 0] + <> [ms m | m /= 0] + <> [ss s | s /= 0] + where + ss s = show s <> " sec" + ms m = show m <> " min" + hs 1 = "1 hour" + hs h = show h <> " hours" + ds 1 = "1 day" + ds 7 = "1 week" + ds 14 = "2 weeks" + ds d = show d <> " days" + mms 1 = "1 month" + mms mm = show mm <> " months" + +-- | Rewrite source with provided substitutions +render :: ByteString -> [(ByteString, Maybe ByteString)] -> ByteString +render src = \case + [] -> src + (label, content') : rest -> render (section_ label content' src) rest + +-- | Rewrite section content inside @...@ markers. +-- Markers are always removed when found. Closing marker is mandatory. +-- If content is absent, whole section is removed. +-- Section content is delegated to `item_`. If no sections found, the whole source is delegated. +section_ :: ByteString -> Maybe ByteString -> ByteString -> ByteString +section_ label content' src = + case B.breakSubstring startMarker src of + (_, "") -> item_ label (fromMaybe "" content') src -- no section, just replace items + (before, afterStart') -> + -- found section start, search for end too + case B.breakSubstring endMarker $ B.drop (B.length startMarker) afterStart' of + (_, "") -> error $ "missing section end: " <> show endMarker + (inside, next') -> + let next = B.drop (B.length endMarker) next' + in case content' of + Nothing -> before <> next -- collapse section + Just content -> before <> item_ label content inside <> section_ label content' next + where + startMarker = " label <> ">" + endMarker = " label <> ">" + +-- | Replace all occurences of @${label}@ with provided content. +item_ :: ByteString -> ByteString -> ByteString -> ByteString +item_ label content' src = + case B.breakSubstring marker src of + (done, "") -> done + (before, after') -> before <> content' <> item_ label content' (B.drop (B.length marker) after') + where + marker = "${" <> label <> "}" diff --git a/apps/smp-server/web/Static/Embedded.hs b/apps/smp-server/web/Static/Embedded.hs new file mode 100644 index 000000000..23698dd6f --- /dev/null +++ b/apps/smp-server/web/Static/Embedded.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Static.Embedded where + +import Data.FileEmbed (embedDir, embedFile) +import Data.ByteString (ByteString) + +indexHtml :: ByteString +indexHtml = $(embedFile "apps/smp-server/static/index.html") + +linkHtml :: ByteString +linkHtml = $(embedFile "apps/smp-server/static/link.html") + +mediaContent :: [(FilePath, ByteString)] +mediaContent = $(embedDir "apps/smp-server/static/media/") diff --git a/benchmarks/Bench/Compression.hs b/benchmarks/Bench/Compression.hs index f1ac941e2..6597736d3 100644 --- a/benchmarks/Bench/Compression.hs +++ b/benchmarks/Bench/Compression.hs @@ -7,11 +7,7 @@ import Data.Aeson import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB import Simplex.Messaging.Compression -import Test.Tasty import Test.Tasty.Bench -import Simplex.Messaging.Encoding (smpEncode) -import Control.Monad (replicateM) --- import qualified Codec.Compression.Zstd.FFI as Z benchCompression :: [Benchmark] benchCompression = @@ -22,19 +18,9 @@ benchCompression = bench "5" $ nf (Z.compress 5) testJson, bench "9" $ nf (Z.compress 9) testJson, bench "15" $ nf (Z.compress 19) testJson - ], - bgroup - "context" - [ withCtxRes $ bench "batch-1" . nfAppIO (>>= replicateM 1 . fmap smpEncode . flip compress testJson), - withCtxRes $ bench "batch-1-pass" . nfAppIO (>>= replicateM 1 . fmap smpEncode . flip compress shortJson), - withCtxRes $ bench "batch-10" . nfAppIO (>>= replicateM 10 . fmap smpEncode . flip compress testJson), - withCtxRes $ bcompare "batch-10" . bench "native-10" . nfAppIO (const . replicateM 10 $ pure $! smpEncode $ Z.compress 3 testJson) - ] + ] ] -withCtxRes :: (IO CompressCtx -> TestTree) -> TestTree -withCtxRes = withResource (createCompressCtx 16384) freeCompressCtx - shortJson :: B.ByteString shortJson = B.take maxLengthPassthrough testJson diff --git a/notes-flow.txt b/notes-flow.txt new file mode 100644 index 000000000..93f984509 --- /dev/null +++ b/notes-flow.txt @@ -0,0 +1,23 @@ +common: + corrId - random BS, used as CbNonce + entityId - p2r tlsUniq + +# setup +s->p: "proxy", uri, auth? + # unless connected + p->r: "p_handshake" + p<-r: "r_key", tls-signed dh pub +s<-r: "r_key", tls-signed dh pub # reply entityId contains tlsUniq + +# working +s ; generate random dh priv, make shared secret +s->p: s2r("forward", random dh pub, SEND command blob) + p->r: p2r("forward", random dh pub, s2r("forward", ...))) + r->c@ "msg", ... + p<-r: p2r("r_res", s2r("ok" / "error", error)) +s<-p@ s2r("ok" / "error", error) + +# expired + p<-r@ p2r("error", "key expired") +s<-p@ "error", "key expired" +s ; reconnect \ No newline at end of file diff --git a/package.yaml b/package.yaml index 5eac6776d..f22cf645f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplexmq -version: 5.6.2.1 +version: 5.8.0.10 synopsis: SimpleXMQ message broker description: | This package includes <./docs/Simplex-Messaging-Server.html server>, @@ -22,6 +22,8 @@ extra-source-files: - CHANGELOG.md - cbits/sha512.h - cbits/sntrup761.h + - apps/smp-server/static/*.html + - apps/smp-server/static/media/* dependencies: - aeson == 2.2.* @@ -86,6 +88,9 @@ flags: manual: True default: True +# cpp-options: +# - -Dslow_servers + when: - condition: flag(swift) cpp-options: @@ -111,10 +116,16 @@ library: executables: smp-server: - source-dirs: apps/smp-server + source-dirs: + - apps/smp-server + - apps/smp-server/web main: Main.hs dependencies: + - file-embed - simplexmq + - wai-app-static + - warp + - warp-tls ghc-options: - -threaded - -rtsopts @@ -202,12 +213,31 @@ benchmarks: ghc-options: # - -haddock - - -Wall + - -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 - - -Wincomplete-uni-patterns - -Wunused-type-patterns - -O2 diff --git a/rfcs/2023-09-12-second-relays.md b/rfcs/2023-09-12-second-relays.md index 3721ee721..cad6c4a92 100644 --- a/rfcs/2023-09-12-second-relays.md +++ b/rfcs/2023-09-12-second-relays.md @@ -2,9 +2,9 @@ ## Problem -SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an audequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency. +SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an adequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency. -The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realise that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product". +The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realize that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product". Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses. @@ -43,7 +43,7 @@ Overall, this is not a viable or even appropriate option for the current stage. 3. SMP / XFTP proxy. -Introduce SMP and XFTP protocol extenstions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users. +Introduce SMP and XFTP protocol extensions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users. Pros: - no dependency on and lower latency than via Tor @@ -68,7 +68,7 @@ Below considers this design. 2. SMP proxy should not be able to observe queue addresses and their count on the destination relays. This requirement is not needed for XFTP proxies, as each file chunk is downloaded only once, so there is no need to hide its address. -3. There must be no identifiers and cyphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality). +3. There must be no identifiers and ciphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality). 4. Traffic between the client and destination relays must be e2e encrypted, with MITM-by-proxy mitigated, relying on the relay identity (certificate fingerprint), ideally without any additional fingerprint in relay address. @@ -97,11 +97,11 @@ This would also reduce the difference in how the traffic looks to the observer - The flow of the messages will be: -1. Client requests proxy to create session with the relay by sending `server` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic. +1. Client requests proxy to create session with the relay by sending `PRXY` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic. -2. Proxy connects to SMP relay, negotiating a shared secret in the handshake that will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). SMP relay also returns in handshake its temporary DH key to agree e2e encryption with the client (sender-relay encryption, to hide metadata sent to the destination relay from proxy). +2. Proxy connects to SMP relay, negotiating a shared secret via a handshake headers - it will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). DH key returned by SMP relay in handshake will also be used to encrypt client commands, combining it with random per-command keys (sender-relay encryption, to hide metadata sent to the destination relay from proxy). -3. Proxy replies with `server_id` command including relay session ID to identify it in further requests, relay DH key for e2e encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before). +3. Proxy replies to sender with `PKEY` message using "entityId" transmission field to indicate session ID for using in further requests, relay DH key for _s2r_ encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before). A possible attack here is that proxy can use this TLS session to replay commands received from the client. Possibly, it could be mitigated with a bloom filter per proxy/SMP relay connection that would reject the repeated DH keys (that need to be used for replay), and also with DH key expiration (this mitigation should allow some acceptable rate of false positives from the bloom filter). @@ -113,11 +113,11 @@ It is important that the same public key from destination relay is returned to a *Unrelated cosideration for SMP protocol privacy improvement*: instead of signing commands to the destination relay, the sender could have a ratchet per queue agreed with the destination relay that would simply use authenticated encryption with per-message symmetric key to encrypt the message on the way to relay, and this encryption would be used as a proof of sender. -4. Now the client sends `forward` to proxy, which it then forwards to SMP relay, applying additional encryption layer. +4. Now the client sends `PFWD` to proxy, which it then forwards to SMP relay as `RFWD`, applying _p2r_ encryption layer. -5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional encryption layer. +5. SMP relay sends `RRES` to proxy applying _p2r_ encryption layer, which it then forwards to the client as `PRES`, removing the _p2r_ encryption layer. -Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session. +Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients' relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session. Sequence diagram for sending the message via SMP proxy: @@ -126,33 +126,33 @@ Sequence diagram for sending the message via SMP proxy: | sending | | SMP | | SMP | | receiving | | client | | proxy | | relay | | client | ------------- ------------- ------------- ------------- - | `server` | | | - | -------------------------> | create TLS session, get keys | | + | `PRXY` | | | + | -------------------------> | | | | | ------------------------------> | | - | `server_id` | (if doesn't exist) | | + | | SMP handshake | | + | | <------------------------------ | | + | `PKEY` | | | | <------------------------- | | | | | | | - | TLS(F:s2r(SEND(e2e(msg)))) | | | - | -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | | + | `PFWD` (s2r) | | | + | -------------------------> | | | + | | `RFWD` (p2r) | | | | ------------------------------> | | - | | | | - | | TLS(R:p2r(s2r(OK/ERR))) | | - | TLS(R:s2r(OK/ERR)) | <------------------------------ | | - | <------------------------- | | TLS(MSG(r2c(e2e(msg)))) | - | | | -----------------------> | - | | | | - | | | TLS(ACK) | + | | `RRES` (p2r) | | + | | <------------------------------ | | + | `PRES` (s2r) | | `MSG` | + | <------------------------- | | -----------------------> | + | | | `ACK` | | | | <----------------------- | | | | | | | | | - ``` -Below diagram shows the encrypttion layers for `forward` and `response` commands: +Below diagram shows the encrypttion layers for `PFWD`/`RFWD` commands and `RRES`/`PRES` responses: -- s2r (added) - encryption between client and SMP relay, with relay key returned in server_id command, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. +- s2r (added) - encryption between client and SMP relay, with relay key returned in relay handshake, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address. - e2e (exists now) - end-to-end encryption per SMP queue, with double ratchet e2e encryption inside it. -- p2r (added) - additional encryption between proxy and SMP relay with key agreed in the handshake, to mitigate traffic correlation inside TLS. This key could also be signed by the same certificate, if we don't want to rely on TLS security. +- p2r (added) - additional encryption between proxy and SMP relay with the shared secret agreed in the handshake, to mitigate traffic correlation inside TLS. - r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS. ``` @@ -167,30 +167,85 @@ Below diagram shows the encrypttion layers for `forward` and `response` commands ----------------- ----------------- -- TLS -- ----------------- ----------------- ``` -When proxy connects to SMP relay it would indicate in the handshake that it will use proxy protocol and the SMP relay would expect the same `forward` commands and reply with `response`s. +Question: should proxy declare its role in handshake? When proxy connects to SMP relay it would indicate in the handshake that it will act as a proxy and the SMP relay would expect the same `forward` commands and reply with `response`s. -Below syntax aims to fit in 16kb block using spare capacity in SMP protocol. +Common SMP transmission format (v4), for reference: ```abnf -proxy_block = padded(proxy_transmission, 16384) -proxy_transmission = corr_id relay_session_id proxy_command -corr_id = length *8 OCTET -proxy_command = server / server_id / forward / response / error -server = "S" address [relay_basic_auth] ; creates transport session between proxy and relay -server_id = "I" relay_session_id tls_session_id signed_relay_key ; - ; session_id is the TLS session ID between proxy and relay, it has to be included inside encrypted block to prevent replay attacks -forward = %s"F" random_dh_pub_key encrypted_block ; it's important that a new key is used for each command, to prevent any correlation by proxy or by destination relay -response = %s"R" encrypted_block; response received from the destination SMP relay -relay_session_id = length *8 OCTET -error = %s"E" error +paddedTransmission = +transmission = signature signed +signature = 0 ; empty signatures here +signed = sessionIdentifier corrId entityId (smpCommand / brokerMsg) ``` -The overhead is: 1+8 (corrId) + 1+8 (relay_session_id) + 1 (command) + 1+32 (random_dh_pub_key) + 2 (original length) + 16 (auth tag for e2e encryption) + 16 (auth tag for proxy to relay encryption) = 86 bytes. The reserve for sent messages in SMP is ~84 bytes, so it should about fit with some reduced bytes somewhere. +- `corrId` is fully random each time and used as a nonce for encrypted blocks. +- `entityId` carries tlsUniq from the current proxy-to-relay connection. +- `smpCommand` gets extended with `s2p_command / p2r_command`. +- `brokerMsg` gets extended with `r_key / r_response`. -Another possible design is to allow mixing sent messages and normal SMP commands in the same transport connection, but it can make fitting in the block a bit harder, additional overhead would be: 1 (transmission count) + 2 (transmission size) + 1 (empty signature) = 4 bytes. +```abnf +s2p_command = proxy / forward +p2r_command = p_handshake ; forward is +proxy = %s"PRXY" SP relayUri SP basicAuth +relayUri = length %s"smp://" serverIdentity "@" srvHost [":" port] +forward = %s"PFWD" SP dhPublic SP encryptedBlock +r_key = %s"PKEY" SP dhPublic +r_response = %s"RRES" SP encryptedBlock +dhPublic = length x509encoded +``` The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response). +### Implementation considerations for the client + +While client/server protocol is rather straightforward to implement, and it is already working, there are some decisions to make about how the client makes decisions about. + +1. When to use proxy and when to connect directly to the destination relay. + +While from the perspective of threat model improvement it may be beneficial to always use the proxy, choosing the proxy that is different from other relays in the connection, initially we need to make it opt-in, with an option to only use it for unknown destination relays, to minimize any unexpected adverse effect on the delivery latency. + +Proxy mode will be passed from the client via NetworkConfig. + +2. Which proxying relays to use. + +Ability to request access to the session with the destination relay (and to create such session) is protected with the same basic auth approach as creating queues - the logic here is that opening private servers to all users as proxies would increase the scenarios for DoS attacks (which is the case with the public servers). + +The open question is whether the client should choose proxies from: +- all configured relays. +- there should be a subset of configured relays. +- there should be a separate list. + +E.g., there could be a second toggle in the relay configuration to allow using relay as proxy, in addition to the current toggle that allows creating queues. + +For simplicity, initially we will just use all enabled relays as potential proxies. + +3. How many proxying relays should be used during one session. + +This is not a simple question, and it creates a contradiction between two risks: +- collusion between proxies and destination relays simplifies correlating sending clients by session - from the point of view of this risk, clients should follow the same policy for creating connections with proxies, that is to create a new connection for each user profile, and if transport isolation is set to "per connection" - for each destination queue. +- traffic correlation by observable traffic sessions (particularly if an attacker can observe user's ISP traffic or multiple proxies) - from this point of view, it would be beneficial to use fewer proxies and fewer connections with proxies and see the risk of proxy colluding with the destination relay as lower than the risk of traffic observation that in the case of multiple sessions would allow to correlate traffic to rarely used destination relays (any private self-hosted relays) and the traffic of the user to a given proxy, to prove the fact of user communicating with the destination relay via the proxy. + +While we can transfer this choice on the users, it seems a complex decision to make, and overall the second risk (traffic correlation) seems more important to address than the first. + +In any case possible options are: +1. Extreme option 1: Create a new proxy session, with the new random proxy, for each potential transport session that would exist if the user were to be connected to destination relays directly. That is, never to mix access to multiple relays from multiple user profiles (and in case of per-connection isolation, to multiple queues) into a one client session with proxy. This is a rather radical option that nullifies any advantages of having fewer sessions with proxies than there would have been with the destination relays and removes any benefits of batching destination server session requests (PRXY comands). +2. Extreme option 2: Use only one proxy session at the time, mixing traffic from all user profiles and to all destination servers (and for all queues) into a session with one proxy. This minimizes the risks of traffic correlation in case of non-colluding proxy, but maximises the risk in case it colludes with the destination relays. +3. Balanced option: Use one proxy session per user profile, but mix traffic to multiple queues irrespective of connection isolation option and to all destination servers. Given that connection isolation is an experimental option, this makes the most sense, but it would have to be disclosed. +4. Less balanced option: take connection isolation option into account and create a new proxy connection for each destination queue. This feels worse than option 3. + +If option 3 is chosen, then the transport session key with the proxy would be different from the transport session key with the relay - proxy session will only use UserId as the key, and the relay session uses (UserId, Server, Maybe EntityId) as the key. + +If option 4 is chosen, the keys would also be different, as the proxy would then use (UserId, Maybe (Server, EntityId)) as the key. + +We could potentially key proxy sessions (and create proxy connections) per each destination relay, in the same way as we key relays themselves, but it seems to have the least sense, as we neither achieve isolation by queue in case proxy and destination relay collude, nor we sufficiently protect from traffic correlation by any observers. + +The implemented design is this: +- for each destination relay a random proxy is chosen and used to send all messages - all requests from a client coalesce to a single session. +- transport isolation mode is taken into account, that is if per-connection isolation is enabled, then a separate proxy connection will be created for each messaging queue. +- supported modes when proxy is used: always, for unknown relays, for unknown relays when IP address is not protected, never. + +This decision is made because the argument for protection against collusion between proxy and relay and more balanced traffic distribution is stronger than the argument for protection against traffic correlation, because even mixing all messages to one proxy connection does not provide protection against traffic correlation by time, so in any case it requires adding delays. + ### Threat model for SMP proxy and changes to threat model for SMP #### SMP proxy diff --git a/rfcs/2024-03-20-server-metadata.md b/rfcs/2024-03-20-server-metadata.md index f8795f130..22b163c05 100644 --- a/rfcs/2024-03-20-server-metadata.md +++ b/rfcs/2024-03-20-server-metadata.md @@ -52,8 +52,8 @@ source_code: https://github.com/simplex-chat/simplexmq # We should split this document to the model one, where specific parameters will be external to the document, # and specific to us, so that relay operators can adopt our recommended policy and publish any amendments separately. -conditions: https://github.com/simplex-chat/simplex-chat/blob/_archived-ep/ios-file-provider/PRIVACY.md -# conditions_amendments: link +usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/_archived-ep/ios-file-provider/PRIVACY.md +# condition_amendments: link server_country: SE operator: SimpleX Chat Ltd. @@ -62,9 +62,9 @@ website: https://simplex.chat admin_simplex: administrative SimpleX address admin_email: chat@simplex.chat admin_pgp: PGP key -feedback_simplex: SimpleX address for feedback, comments and complaints -feedback_email: complaints@simplex.chat -feedback_pgp: PGP key +complaints_simplex: SimpleX address for feedback, comments and complaints +complaints_email: complaints@simplex.chat +complaints_pgp: PGP key hosting: Linode / Akamai Inc. hosting_country: US ``` @@ -89,20 +89,27 @@ data ServerHandshake = ServerHandshake } data ServerInformation = ServerInformation - { -- below is based on the existing server configuration - persistence :: SMPServerPersistenceMode, + { config :: ServerPublicConfig, + info :: ServerPublicInfo + } + +-- based on server configuration +data ServerPublicConfig = ServerPublicConfig + { persistence :: SMPServerPersistenceMode, messageExpiration :: Int, statsEnabled :: Bool, newQueuesAllowed :: Bool, - basicAuthEnabled :: Bool, -- server is private if enabled - -- below is based on INFORMATION section of INI file - sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license - -- all below properties are optional, except entity name MUST be present if any entity country is present + basicAuthEnabled :: Bool -- server is private if enabled + } + +-- based on INFORMATION section of INI file +data ServerPublicInfo = ServerPublicInfo + { sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license conditions :: Maybe ServerConditions, operator :: Maybe Entity, website :: Maybe Text, - admin :: Maybe ServerContactAddress, - feedback :: Maybe ServerContactAddress, + adminContacts :: Maybe ServerContactAddress, + complaintsContacts :: Maybe ServerContactAddress, hosting :: Maybe Entity, serverCountry :: Maybe Text } diff --git a/simplexmq.cabal b/simplexmq.cabal index d579f3a43..23f1004c6 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplexmq -version: 5.6.2.1 +version: 5.8.0.10 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and @@ -28,6 +28,33 @@ extra-source-files: CHANGELOG.md cbits/sha512.h cbits/sntrup761.h + apps/smp-server/static/index.html + apps/smp-server/static/link.html + apps/smp-server/static/media/apk_icon.png + apps/smp-server/static/media/apple_store.svg + apps/smp-server/static/media/contact.js + apps/smp-server/static/media/contact_page_mobile.png + apps/smp-server/static/media/f_droid.svg + apps/smp-server/static/media/favicon.ico + apps/smp-server/static/media/GilroyBold.woff2 + apps/smp-server/static/media/GilroyLight.woff2 + apps/smp-server/static/media/GilroyMedium.woff2 + apps/smp-server/static/media/GilroyRegular.woff2 + apps/smp-server/static/media/GilroyRegularItalic.woff2 + apps/smp-server/static/media/google_play.svg + apps/smp-server/static/media/logo-dark.png + apps/smp-server/static/media/logo-light.png + apps/smp-server/static/media/logo-symbol-dark.svg + apps/smp-server/static/media/logo-symbol-light.svg + apps/smp-server/static/media/moon.svg + apps/smp-server/static/media/qrcode.js + apps/smp-server/static/media/script.js + apps/smp-server/static/media/style.css + apps/smp-server/static/media/sun.svg + apps/smp-server/static/media/swiper-bundle.min.css + apps/smp-server/static/media/swiper-bundle.min.js + apps/smp-server/static/media/tailwind.css + apps/smp-server/static/media/testflight.png flag swift description: Enable swift JSON format @@ -105,6 +132,7 @@ library Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem + Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays Simplex.Messaging.Agent.TRcvQueues Simplex.Messaging.Client Simplex.Messaging.Client.Agent @@ -139,14 +167,17 @@ library Simplex.Messaging.Server.Control Simplex.Messaging.Server.Env.STM Simplex.Messaging.Server.Expiration + Simplex.Messaging.Server.Information Simplex.Messaging.Server.Main Simplex.Messaging.Server.MsgStore Simplex.Messaging.Server.MsgStore.STM Simplex.Messaging.Server.QueueStore + Simplex.Messaging.Server.QueueStore.QueueInfo Simplex.Messaging.Server.QueueStore.STM Simplex.Messaging.Server.Stats Simplex.Messaging.Server.StoreLog Simplex.Messaging.ServiceScheme + Simplex.Messaging.Session Simplex.Messaging.TMap Simplex.Messaging.Transport Simplex.Messaging.Transport.Buffer @@ -173,7 +204,7 @@ library src default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 + 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 include-dirs: cbits c-sources: @@ -255,7 +286,7 @@ executable ntf-server apps/ntf-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + 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 build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -331,7 +362,7 @@ executable smp-agent apps/smp-agent default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + 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 build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -402,12 +433,15 @@ executable smp-agent executable smp-server main-is: Main.hs other-modules: + Static + Static.Embedded Paths_simplexmq hs-source-dirs: apps/smp-server + apps/smp-server/web default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + 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 build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -430,6 +464,7 @@ executable smp-server , deepseq ==1.4.* , direct-sqlcipher ==2.3.* , directory ==1.3.* + , file-embed , filepath ==1.4.* , hourglass ==0.2.* , http-types ==0.12.* @@ -458,6 +493,9 @@ executable smp-server , transformers ==0.6.* , unliftio ==0.2.* , unliftio-core ==0.2.* + , wai-app-static + , warp + , warp-tls , websockets ==0.12.* , yaml ==0.11.* , zstd ==0.1.3.* @@ -483,7 +521,7 @@ executable xftp apps/xftp default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + 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 build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -559,7 +597,7 @@ executable xftp-server apps/xftp-server default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts + 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 build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -657,6 +695,7 @@ test-suite simplexmq-test ServerTests SMPAgentClient SMPClient + SMPProxyTests Util XFTPAgent XFTPCLI @@ -667,7 +706,7 @@ test-suite simplexmq-test tests default-extensions: StrictData - ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1 + 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 build-depends: HUnit ==1.6.* , QuickCheck ==2.14.* diff --git a/src/Simplex/FileTransfer/Agent.hs b/src/Simplex/FileTransfer/Agent.hs index d04117942..c8030b206 100644 --- a/src/Simplex/FileTransfer/Agent.hs +++ b/src/Simplex/FileTransfer/Agent.hs @@ -112,8 +112,8 @@ closeXFTPAgent a = do where stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker) -xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AM RcvFileId -xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs = do +xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AM RcvFileId +xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs approvedRelays = do g <- asks random prefixPath <- lift $ getPrefixPath "rcv.xftp" createDirectory prefixPath @@ -124,7 +124,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi lift $ createEmptyFile =<< toFSFilePath relSavePath let saveFile = CryptoFile relSavePath cfArgs fId <- case redirect of - Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile + Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile approvedRelays Just _ -> do -- prepare description paths let relTmpPathRedirect = relPrefixPath "xftp.redirect-encrypted" @@ -134,7 +134,7 @@ xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redi cfArgsRedirect <- atomically $ CF.randomArgs g let saveFileRedirect = CryptoFile relSavePathRedirect $ Just cfArgsRedirect -- create download tasks - withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile + withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile approvedRelays forM_ chunks (downloadChunk c) pure fId @@ -176,12 +176,12 @@ runXFTPRcvWorker c srv Worker {doWork} = do runXFTPOperation :: AgentConfig -> AM () runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case - RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []} -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) "chunk has no replicas" - fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do + (RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _) -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (INTERNAL "chunk has no replicas") + (fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays) -> do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do - lift $ waitForUserNetwork c - downloadFileChunk fc replica + liftIO $ waitForUserNetwork c + downloadFileChunk fc replica approvedRelays `catchAgentError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e where retryLoop loop e replicaDelay = do @@ -191,9 +191,10 @@ runXFTPRcvWorker c srv Worker {doWork} = do withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay atomically $ assertAgentForeground c loop - retryDone e = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (show e) - downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> AM () - downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do + retryDone = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) + downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM () + downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do + unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwError $ XFTP "" XFTP.NOT_APPROVED fsFileTmpPath <- lift $ toFSFilePath fileTmpPath chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest) @@ -214,6 +215,10 @@ runXFTPRcvWorker c srv Worker {doWork} = do when complete . lift . void $ getXFTPRcvWorker True c Nothing where + ipAddressProtected' :: AM Bool + ipAddressProtected' = do + cfg <- liftIO $ getNetworkConfig' c + pure $ ipAddressProtected cfg srv receivedSize :: [RcvFileChunk] -> Int64 receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0 receivedChunkSize ch@RcvFileChunk {chunkSize = s} @@ -234,11 +239,11 @@ retryOnError name loop done e = do then loop else done -rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> AM () -rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath internalErrStr = do +rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> AgentErrorType -> AM () +rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath err = do lift $ forM_ tmpPath (removePath <=< toFSFilePath) - withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr - notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr + withStore' c $ \db -> updateRcvFileError db rcvFileId (show err) + notify c rcvFileEntityId $ RFERR err runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM () runXFTPRcvLocalWorker c Worker {doWork} = do @@ -252,7 +257,7 @@ runXFTPRcvLocalWorker c Worker {doWork} = do runXFTPOperation AgentConfig {rcvFilesTTL} = withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $ \f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} -> - decryptFile f `catchAgentError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show) + decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath decryptFile :: RcvFile -> AM () decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do let CryptoFile savePath cfArgs = saveFile @@ -262,9 +267,9 @@ runXFTPRcvLocalWorker c Worker {doWork} = do withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting chunkPaths <- getChunkPaths chunks encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths - when (FileSize encSize /= size) $ throwError $ XFTP XFTP.SIZE + when (FileSize encSize /= size) $ throwError $ XFTP "" XFTP.SIZE encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths - when (FileDigest encDigest /= digest) $ throwError $ XFTP XFTP.DIGEST + when (FileDigest encDigest /= digest) $ throwError $ XFTP "" XFTP.DIGEST let destFile = CryptoFile fsSavePath cfArgs void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile case redirect of @@ -281,10 +286,11 @@ runXFTPRcvLocalWorker c Worker {doWork} = do -- proceed with redirect yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath) next@FileDescription {chunks = nextChunks} <- case strDecode (LB.toStrict yaml) of - Left _ -> throwError . XFTP $ XFTP.REDIRECT "decode error" + -- TODO switch to another error constructor + Left _ -> throwError . XFTP "" $ XFTP.REDIRECT "decode error" Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest}) - | dstSize /= redirectSize -> throwError . XFTP $ XFTP.REDIRECT "size mismatch" - | dstDigest /= redirectDigest -> throwError . XFTP $ XFTP.REDIRECT "digest mismatch" + | dstSize /= redirectSize -> throwError . XFTP "" $ XFTP.REDIRECT "size mismatch" + | dstDigest /= redirectDigest -> throwError . XFTP "" $ XFTP.REDIRECT "digest mismatch" | otherwise -> pure fd -- register and download chunks from the actual file withStore c $ \db -> updateRcvFileRedirect db redirectDbId next @@ -424,7 +430,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do tryCreate = do usedSrvs <- newTVarIO ([] :: [XFTPServer]) withRetryInterval (riFast ri) $ \_ loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c createWithNextSrv usedSrvs `catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e where @@ -457,7 +463,7 @@ runXFTPSndWorker c srv Worker {doWork} = do fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c uploadFileChunk cfg fc replica `catchAgentError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e where @@ -624,7 +630,7 @@ runXFTPDelWorker c srv Worker {doWork} = do processDeletedReplica replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} = do let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c deleteChunkReplica `catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e where diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 642a3e73b..44d1b596b 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -14,6 +14,7 @@ module Simplex.FileTransfer.Client where import Control.Logger.Simple import Control.Monad import Control.Monad.Except +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first) import Data.ByteString.Builder (Builder, byteString) @@ -28,6 +29,7 @@ import qualified Data.X509.Validation as XV import qualified Network.HTTP.Types as N import qualified Network.HTTP2.Client as H import Simplex.FileTransfer.Protocol +import Simplex.FileTransfer.Server.Env (supportedXFTPhandshakes) import Simplex.FileTransfer.Transport import Simplex.Messaging.Client ( NetworkConfig (..), @@ -37,8 +39,8 @@ import Simplex.Messaging.Client defaultNetworkConfig, proxyUsername, transportClientConfig, + unexpectedResponse, ) -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding (smpDecode, smpEncode) @@ -50,26 +52,27 @@ import Simplex.Messaging.Protocol RecipientId, SenderId, ) -import Simplex.Messaging.Transport (HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), supportedParameters) +import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters) import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.Client import Simplex.Messaging.Transport.HTTP2.File -import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM) -import Simplex.Messaging.Version (compatibleVersion, pattern Compatible) +import Simplex.Messaging.Util (liftEitherWith, liftError', tshow, whenM) +import Simplex.Messaging.Version import UnliftIO import UnliftIO.Directory data XFTPClient = XFTPClient { http2Client :: HTTP2Client, transportSession :: TransportSession FileResponse, - thParams :: THandleParams XFTPVersion, + thParams :: THandleParams XFTPVersion 'TClient, config :: XFTPClientConfig } data XFTPClientConfig = XFTPClientConfig { xftpNetworkConfig :: NetworkConfig, - serverVRange :: VersionRangeXFTP + serverVRange :: VersionRangeXFTP, + clientALPN :: Maybe [ALPN] } data XFTPChunkBody = XFTPChunkBody @@ -91,62 +94,68 @@ defaultXFTPClientConfig :: XFTPClientConfig defaultXFTPClientConfig = XFTPClientConfig { xftpNetworkConfig = defaultNetworkConfig, - serverVRange = supportedFileServerVRange + serverVRange = supportedFileServerVRange, + clientALPN = Just supportedXFTPhandshakes } -getXFTPClient :: TVar ChaChaDRG -> TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) -getXFTPClient g transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do - let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = Just ["xftp/1"]} - http2Config = xftpHTTP2Config tcConfig config - username = proxyUsername transportSession +getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient) +getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do + let username = proxyUsername transportSession ProtocolServer _ host port keyHash = srv useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host + let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN} + http2Config = xftpHTTP2Config tcConfig config clientVar <- newTVarIO Nothing let usePort = if null port then "443" else port clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected let HTTP2Client {sessionId, sessionALPN} = http2Client - thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True} + v = VersionXFTP 1 + thServerVRange = versionToRange v + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True} logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN - thParams <- case sessionALPN of - Just "xftp/1" -> xftpClientHandshakeV1 g serverVRange keyHash http2Client thParams0 - Nothing -> pure thParams0 - _ -> throwError $ PCETransportError (TEHandshake VERSION) + thParams@THandleParams {thVersion} <- case sessionALPN of + Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0 + _ -> pure thParams0 + logDebug $ "Client negotiated protocol: " <> tshow thVersion let c = XFTPClient {http2Client, thParams, transportSession, config} atomically $ writeTVar clientVar $ Just c pure c -xftpClientHandshakeV1 :: TVar ChaChaDRG -> VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP -> ExceptT XFTPClientError IO THandleParamsXFTP -xftpClientHandshakeV1 g serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do - shs <- getServerHandshake - (v, sk) <- processServerHandshake shs - (k, pk) <- atomically $ C.generateKeyPair g - sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash, authPubKey = k} - pure thParams0 {thAuth = Just THandleAuth {peerPubKey = sk, privKey = pk}, thVersion = v} +xftpClientHandshakeV1 :: VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient) +xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do + shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake + (vr, sk) <- processServerHandshake shs + let v = maxVersion vr + sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash} + pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v, thServerVRange = vr} where + getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake getServerHandshake = do let helloReq = H.requestNoBody "POST" "/" [] HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <- - liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing - liftHS . smpDecode =<< liftHS (C.unPad shsBody) + liftError' xftpClientError $ sendRequest c helloReq Nothing + liftTransportErr (TEHandshake PARSE) . smpDecode =<< liftTransportErr TEBadBlock (C.unPad shsBody) + processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519) processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do - unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION - case xftpVersionRange `compatibleVersion` serverVRange of - Nothing -> throwError $ PCEResponseError HANDSHAKE - Just (Compatible v) -> - fmap (v,) . liftHS $ do + unless (sessionId == serverSessId) $ throwError $ PCETransportError TEBadSession + case xftpVersionRange `compatibleVRange` serverVRange of + Nothing -> throwError $ PCETransportError TEVersion + Just (Compatible vr) -> + fmap (vr,) . liftTransportErr (TEHandshake BAD_AUTH) $ do let (X.CertificateChain cert, exact) = serverAuth case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () _ -> throwError "bad certificate" pubKey <- maybe (throwError "bad server key type") (`C.verifyX509` exact) serverKey C.x509ToPublic (pubKey, []) >>= C.pubKey + sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO () sendClientHandshake chs = do - chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize + chs' <- liftTransportErr TELargeMsg $ C.pad (smpEncode chs) xftpBlockSize let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs' - HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c chsReq Nothing - unless (B.null bodyHead) $ throwError $ PCEResponseError HANDSHAKE - liftHS = liftEitherWith (const $ PCEResponseError HANDSHAKE) + HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' xftpClientError $ sendRequest c chsReq Nothing + unless (B.null bodyHead) $ throwError $ PCETransportError TEBadBlock + liftTransportErr e = liftEitherWith (const $ PCETransportError e) closeXFTPClient :: XFTPClient -> IO () closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client @@ -179,9 +188,11 @@ xftpClientError = \case sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body) sendXFTPCommand c@XFTPClient {thParams} pKey fId cmd chunkSpec_ = do + -- TODO random corrId + let corrIdUsedAsNonce = "" t <- liftEither . first PCETransportError $ - xftpEncodeAuthTransmission thParams pKey ("", fId, FileCmd (sFileParty @p) cmd) + xftpEncodeAuthTransmission thParams pKey (corrIdUsedAsNonce, fId, FileCmd (sFileParty @p) cmd) sendXFTPTransmission c t chunkSpec_ sendXFTPTransmission :: XFTPClient -> ByteString -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body) @@ -204,7 +215,7 @@ sendXFTPTransmission XFTPClient {config, thParams, http2Client} t chunkSpec_ = d forM_ chunkSpec_ $ \XFTPChunkSpec {filePath, chunkOffset, chunkSize} -> withFile filePath ReadMode $ \h -> do hSeek h AbsoluteSeek $ fromIntegral chunkOffset - hSendFile h send $ fromIntegral chunkSize + hSendFile h send chunkSize done createXFTPChunk :: @@ -217,13 +228,13 @@ createXFTPChunk :: createXFTPChunk c spKey file rcps auth_ = sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case (FRSndIds sId rIds, body) -> noFile body (sId, rIds) - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r addXFTPRecipients :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> NonEmpty C.APublicAuthKey -> ExceptT XFTPClientError IO (NonEmpty RecipientId) addXFTPRecipients c spKey fId rcps = sendXFTPCommand c spKey fId (FADD rcps) Nothing >>= \case (FRRcvIds rIds, body) -> noFile body rIds - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r uploadXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> XFTPChunkSpec -> ExceptT XFTPClientError IO () uploadXFTPChunk c spKey fId chunkSpec = @@ -239,14 +250,19 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec { let dhSecret = C.dh' sDhKey rpDhKey cbState <- liftEither . first PCECryptoError $ LC.cbInit dhSecret cbNonce let t = chunkTimeout config chunkSize - ExceptT (sequence <$> (t `timeout` download cbState)) >>= maybe (throwError PCEResponseTimeout) pure + ExceptT (sequence <$> (t `timeout` (download cbState `catches` errors))) >>= maybe (throwError PCEResponseTimeout) pure where + errors = + [ Handler $ \(_e :: H.HTTP2Error) -> pure $ Left PCENetworkError, + Handler $ \(e :: IOException) -> pure $ Left (PCEIOError e), + Handler $ \(_e :: SomeException) -> pure $ Left PCENetworkError + ] download cbState = runExceptT . withExceptT PCEResponseError $ receiveEncFile chunkPart cbState chunkSpec `catchError` \e -> whenM (doesFileExist filePath) (removeFile filePath) >> throwError e _ -> throwError $ PCEResponseError NO_FILE - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r xftpReqTimeout :: XFTPClientConfig -> Maybe Word32 -> Int xftpReqTimeout cfg@XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout}} chunkSize_ = @@ -270,12 +286,12 @@ pingXFTP c@XFTPClient {thParams} = do (r, _) <- sendXFTPTransmission c t Nothing case r of FRPong -> pure () - _ -> throwError $ PCEUnexpectedResponse $ bshow r + _ -> throwE $ unexpectedResponse r okResponse :: (FileResponse, HTTP2Body) -> ExceptT XFTPClientError IO () okResponse = \case (FROk, body) -> noFile body () - (r, _) -> throwError . PCEUnexpectedResponse $ bshow r + (r, _) -> throwE $ unexpectedResponse r -- TODO this currently does not check anything because response size is not set and bodyPart is always Just noFile :: HTTP2Body -> a -> ExceptT XFTPClientError IO a diff --git a/src/Simplex/FileTransfer/Client/Agent.hs b/src/Simplex/FileTransfer/Client/Agent.hs index f86de9afb..c17790c2d 100644 --- a/src/Simplex/FileTransfer/Client/Agent.hs +++ b/src/Simplex/FileTransfer/Client/Agent.hs @@ -11,7 +11,6 @@ import Control.Logger.Simple (logInfo) import Control.Monad import Control.Monad.Except import Control.Monad.Trans (lift) -import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first) import qualified Data.ByteString.Char8 as B import Data.Text (Text) @@ -19,7 +18,6 @@ import Data.Text.Encoding (decodeUtf8) import Simplex.FileTransfer.Client import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError) -import Simplex.Messaging.Client.Agent () import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtocolServer (..), XFTPServer) import Simplex.Messaging.TMap (TMap) @@ -61,15 +59,15 @@ newXFTPAgent config = do type ME a = ExceptT XFTPClientAgentError IO a -getXFTPServerClient :: TVar ChaChaDRG -> XFTPClientAgent -> XFTPServer -> ME XFTPClient -getXFTPServerClient g XFTPClientAgent {xftpClients, config} srv = do +getXFTPServerClient :: XFTPClientAgent -> XFTPServer -> ME XFTPClient +getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do atomically getClientVar >>= either newXFTPClient waitForXFTPClient where connectClient :: ME XFTPClient connectClient = ExceptT $ first (XFTPClientAgentError srv) - <$> getXFTPClient g (1, srv, Nothing) (xftpConfig config) clientDisconnected + <$> getXFTPClient (1, srv, Nothing) (xftpConfig config) clientDisconnected clientDisconnected :: XFTPClient -> IO () clientDisconnected _ = do diff --git a/src/Simplex/FileTransfer/Client/Main.hs b/src/Simplex/FileTransfer/Client/Main.hs index 3320e0a26..0acc6d3c9 100644 --- a/src/Simplex/FileTransfer/Client/Main.hs +++ b/src/Simplex/FileTransfer/Client/Main.hs @@ -332,10 +332,10 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re (sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g rKeys <- atomically $ L.fromList <$> replicateM numRecipients (C.generateAuthKeyPair C.SEd25519 g) digest <- liftIO $ getChunkDigest chunkSpec - let ch = FileInfo {sndKey, size = fromIntegral chunkSize, digest} - c <- withRetry retryCount $ getXFTPServerClient g a xftpServer + let ch = FileInfo {sndKey, size = chunkSize, digest} + c <- withRetry retryCount $ getXFTPServerClient a xftpServer (sndId, rIds) <- withRetry retryCount $ createXFTPChunk c spKey ch (L.map fst rKeys) auth - withReconnect g a xftpServer retryCount $ \c' -> uploadXFTPChunk c' spKey sndId chunkSpec + withReconnect a xftpServer retryCount $ \c' -> uploadXFTPChunk c' spKey sndId chunkSpec logInfo $ "uploaded chunk " <> tshow chunkNo uploaded <- atomically . stateTVar uploadedChunks $ \cs -> let cs' = fromIntegral chunkSize : cs in (sum cs', cs') @@ -344,7 +344,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re when verbose $ putStrLn "" let recipients = L.toList $ L.map ChunkReplicaId rIds `L.zip` L.map snd rKeys replicas = [SentFileChunkReplica {server = xftpServer, recipients}] - pure (chunkNo, SentFileChunk {chunkNo, sndId, sndPrivateKey = spKey, chunkSize = FileSize $ fromIntegral chunkSize, digest = FileDigest digest, replicas}) + pure (chunkNo, SentFileChunk {chunkNo, sndId, sndPrivateKey = spKey, chunkSize = FileSize chunkSize, digest = FileDigest digest, replicas}) getXFTPServer :: TVar StdGen -> NonEmpty XFTPServerWithAuth -> IO XFTPServerWithAuth getXFTPServer gen = \case srv :| [] -> pure srv @@ -445,7 +445,7 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, when (FileSize encSize /= size) $ throwError $ CLIError "File size mismatch" liftIO $ printNoNewLine "Decrypting file..." CryptoFile path _ <- withExceptT cliCryptoError $ decryptChunks encSize chunkPaths key nonce $ fmap CF.plain . getFilePath - forM_ chunks $ acknowledgeFileChunk g a + forM_ chunks $ acknowledgeFileChunk a whenM (doesPathExist encPath) $ removeDirectoryRecursive encPath liftIO $ do printNoNewLine $ "File downloaded: " <> path @@ -456,7 +456,7 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, logInfo $ "downloading chunk " <> tshow chunkNo <> " from " <> showServer server <> "..." chunkPath <- uniqueCombine encPath $ show chunkNo let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest) - withReconnect g a server retryCount $ \c -> downloadXFTPChunk g c replicaKey (unChunkReplicaId replicaId) chunkSpec + withReconnect a server retryCount $ \c -> downloadXFTPChunk g c replicaKey (unChunkReplicaId replicaId) chunkSpec logInfo $ "downloaded chunk " <> tshow chunkNo <> " to " <> T.pack chunkPath downloaded <- atomically . stateTVar downloadedChunks $ \cs -> let cs' = fromIntegral (unFileSize chunkSize) : cs in (sum cs', cs') @@ -472,12 +472,12 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, ifM (doesDirectoryExist path) (uniqueCombine path name) $ ifM (doesFileExist path) (throwError "File already exists") (pure path) _ -> (`uniqueCombine` name) . ( "Downloads") =<< getHomeDirectory - acknowledgeFileChunk :: TVar ChaChaDRG -> XFTPClientAgent -> FileChunk -> ExceptT CLIError IO () - acknowledgeFileChunk g a FileChunk {replicas = replica : _} = do + acknowledgeFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO () + acknowledgeFileChunk a FileChunk {replicas = replica : _} = do let FileChunkReplica {server, replicaId, replicaKey} = replica - c <- withRetry retryCount $ getXFTPServerClient g a server + c <- withRetry retryCount $ getXFTPServerClient a server withRetry retryCount $ ackXFTPChunk c replicaKey (unChunkReplicaId replicaId) - acknowledgeFileChunk _ _ _ = throwError $ CLIError "chunk has no replicas" + acknowledgeFileChunk _ _ = throwError $ CLIError "chunk has no replicas" printProgress :: String -> Int64 -> Int64 -> IO () printProgress s part total = printNoNewLine $ s <> " " <> show ((part * 100) `div` total) <> "%" @@ -501,8 +501,7 @@ cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do deleteFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO () deleteFileChunk a FileChunk {chunkNo, replicas = replica : _} = do let FileChunkReplica {server, replicaId, replicaKey} = replica - g <- liftIO C.newRandom - withReconnect g a server retryCount $ \c -> deleteXFTPChunk c replicaKey (unChunkReplicaId replicaId) + withReconnect a server retryCount $ \c -> deleteXFTPChunk c replicaKey (unChunkReplicaId replicaId) logInfo $ "deleted chunk " <> tshow chunkNo <> " from " <> showServer server deleteFileChunk _ _ = throwError $ CLIError "chunk has no replicas" @@ -564,15 +563,15 @@ prepareChunkSpecs filePath chunkSizes = reverse . snd $ foldl' addSpec (0, []) c where addSpec :: (Int64, [XFTPChunkSpec]) -> Word32 -> (Int64, [XFTPChunkSpec]) addSpec (chunkOffset, specs) sz = - let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = fromIntegral sz} + let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = sz} in (chunkOffset + fromIntegral sz, spec : specs) getEncPath :: MonadIO m => Maybe FilePath -> String -> m FilePath getEncPath path name = (`uniqueCombine` (name <> ".encrypted")) =<< maybe (liftIO getCanonicalTemporaryDirectory) pure path -withReconnect :: Show e => TVar ChaChaDRG -> XFTPClientAgent -> XFTPServer -> Int -> (XFTPClient -> ExceptT e IO a) -> ExceptT CLIError IO a -withReconnect g a srv n run = withRetry n $ do - c <- withRetry n $ getXFTPServerClient g a srv +withReconnect :: Show e => XFTPClientAgent -> XFTPServer -> Int -> (XFTPClient -> ExceptT e IO a) -> ExceptT CLIError IO a +withReconnect a srv n run = withRetry n $ do + c <- withRetry n $ getXFTPServerClient a srv withExceptT (CLIError . show) (run c) `catchError` \e -> do liftIO $ closeXFTPServerClient a srv throwError e diff --git a/src/Simplex/FileTransfer/Protocol.hs b/src/Simplex/FileTransfer/Protocol.hs index dcd9f2c52..c55b327f8 100644 --- a/src/Simplex/FileTransfer/Protocol.hs +++ b/src/Simplex/FileTransfer/Protocol.hs @@ -25,7 +25,7 @@ import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (isNothing) import Data.Type.Equality import Data.Word (Word32) -import Simplex.FileTransfer.Transport (VersionXFTP, XFTPErrorType (..), XFTPVersion, xftpClientHandshakeStub, pattern VersionXFTP) +import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPVersion, xftpClientHandshakeStub) import Simplex.Messaging.Client (authTransmission) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding @@ -39,8 +39,8 @@ import Simplex.Messaging.Protocol ProtocolErrorType (..), ProtocolMsgTag (..), ProtocolType (..), - RcvPublicDhKey, RcvPublicAuthKey, + RcvPublicDhKey, RecipientId, SenderId, SentRawTransmission, @@ -48,19 +48,17 @@ import Simplex.Messaging.Protocol SndPublicAuthKey, Transmission, TransmissionForAuth (..), - encodeTransmissionForAuth, + CorrId (..), encodeTransmission, + encodeTransmissionForAuth, messageTagP, tDecodeParseValidate, tEncodeBatch1, tParse, ) -import Simplex.Messaging.Transport (THandleParams (..), TransportError (..)) +import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Util ((<$?>)) -currentXFTPVersion :: VersionXFTP -currentXFTPVersion = VersionXFTP 1 - xftpBlockSize :: Int xftpBlockSize = 16384 @@ -328,12 +326,12 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of Just Refl -> Just c _ -> Nothing -xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString +xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg) - xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) corrId tForAuth + xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) (C.cbNonce $ bs corrId) tForAuth -xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> Transmission c -> Either TransportError ByteString +xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString xftpEncodeTransmission thParams (corrId, fId, msg) = do let t = encodeTransmission thParams (corrId, fId, msg) xftpEncodeBatch1 (Nothing, t) @@ -342,7 +340,7 @@ xftpEncodeTransmission thParams (corrId, fId, msg) = do xftpEncodeBatch1 :: SentRawTransmission -> Either TransportError ByteString xftpEncodeBatch1 t = first (const TELargeMsg) $ C.pad (tEncodeBatch1 t) xftpBlockSize -xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion -> ByteString -> Either XFTPErrorType (SignedTransmission e c) +xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> ByteString -> Either XFTPErrorType (SignedTransmission e c) xftpDecodeTransmission thParams t = do t' <- first (const BLOCK) $ C.unPad t case tParse thParams t' of diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index b426360ea..1ed4894ec 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} @@ -56,17 +57,20 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.Stats import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..)) +import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..)) import Simplex.Messaging.Transport.Buffer (trimCR) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize) import Simplex.Messaging.Transport.HTTP2.Server import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials) import Simplex.Messaging.Util -import Simplex.Messaging.Version (isCompatible) +import Simplex.Messaging.Version import System.Exit (exitFailure) import System.FilePath (()) import System.IO (hPrint, hPutStrLn, universalNewlineMode) +#ifdef slow_servers +import System.Random (getStdRandom, randomR) +#endif import UnliftIO import UnliftIO.Concurrent (threadDelay) import UnliftIO.Directory (doesFileExist, removeFile, renameFile) @@ -75,7 +79,7 @@ import qualified UnliftIO.Exception as E type M a = ReaderT XFTPEnv IO a data XFTPTransportRequest = XFTPTransportRequest - { thParams :: THandleParamsXFTP, + { thParams :: THandleParamsXFTP 'TServer, reqBody :: HTTP2Body, request :: H.Request, sendResponse :: H.Response -> IO () @@ -91,10 +95,10 @@ runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpSer data Handshake = HandshakeSent C.PrivateKeyX25519 - | HandshakeAccepted THandleAuth VersionXFTP + | HandshakeAccepted (THandleParams XFTPVersion 'TServer) xftpServer :: XFTPServerConfig -> TMVar Bool -> M () -xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do +xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do mapM_ (expireServerFiles Nothing) fileExpiration restoreServerStats raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer @@ -111,7 +115,9 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira let cleanup sessionId = atomically $ TM.delete sessionId sessions liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do reqBody <- getHTTP2Body r xftpBlockSize - let thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True} + let v = VersionXFTP 1 + thServerVRange = versionToRange v + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True} req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse} flip runReaderT env $ case sessionALPN of Nothing -> processRequest req0 @@ -120,13 +126,13 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira Nothing -> pure () -- handshake response sent Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here) _ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about - xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion)) - xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do + xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer)) + xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams0@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do s <- atomically $ TM.lookup sessionId sessions r <- runExceptT $ case s of Nothing -> processHello Just (HandshakeSent pk) -> processClientHandshake pk - Just (HandshakeAccepted auth v) -> pure $ Just thParams {thAuth = Just auth, thVersion = v} + Just (HandshakeAccepted thParams) -> pure $ Just thParams either sendError pure r where processHello = do @@ -134,22 +140,31 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira (k, pk) <- atomically . C.generateKeyPair =<< asks random atomically $ TM.insert sessionId (HandshakeSent pk) sessions let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k) - let hs = XFTPServerHandshake {xftpVersionRange = supportedFileServerVRange, sessionId, authPubKey} + let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey} shs <- encodeXftp hs +#ifdef slow_servers + lift randomDelay +#endif liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs pure Nothing - processClientHandshake privKey = do + processClientHandshake pk = do unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE body <- liftHS $ C.unPad bodyHead - XFTPClientHandshake {xftpVersion, keyHash, authPubKey} <- liftHS $ smpDecode body + XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ smpDecode body kh <- asks serverIdentity unless (keyHash == kh) $ throwError HANDSHAKE - unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE - let auth = THandleAuth {peerPubKey = authPubKey, privKey} - atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions - liftIO . sendResponse $ H.responseNoBody N.ok200 [] - pure Nothing - sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion)) + case compatibleVRange' xftpServerVRange v of + Just (Compatible vr) -> do + let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} + thParams = thParams0 {thAuth = Just auth, thVersion = v, thServerVRange = vr} + atomically $ TM.insert sessionId (HandshakeAccepted thParams) sessions +#ifdef slow_servers + lift randomDelay +#endif + liftIO . sendResponse $ H.responseNoBody N.ok200 [] + pure Nothing + Nothing -> throwError HANDSHAKE + sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer)) sendError err = do runExceptT (encodeXftp err) >>= \case Right bs -> liftIO . sendResponse $ H.responseBuilder N.ok200 [] bs @@ -310,6 +325,9 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea where sendXFTPResponse (corrId, fId, resp) serverFile_ = do let t_ = xftpEncodeTransmission thParams (corrId, fId, resp) +#ifdef slow_servers + randomDelay +#endif liftIO $ sendResponse $ H.responseStreaming N.ok200 [] $ streamBody t_ where streamBody t_ send done = do @@ -321,12 +339,21 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea send $ byteString t -- timeout sending file in the same way as receiving forM_ serverFile_ $ \ServerFile {filePath, fileSize, sbState} -> do - withFile filePath ReadMode $ \h -> sendEncFile h send sbState (fromIntegral fileSize) + withFile filePath ReadMode $ \h -> sendEncFile h send sbState fileSize done +#ifdef slow_servers +randomDelay :: M () +randomDelay = do + d <- asks $ responseDelay . config + when (d > 0) $ do + pc <- getStdRandom (randomR (-200, 200)) + threadDelay $ (d * (1000 + pc)) `div` 1000 +#endif + data VerificationResult = VRVerified XFTPRequest | VRFailed -verifyXFTPTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult +verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult verifyXFTPTransmission auth_ tAuth authorized fId cmd = case cmd of FileCmd SFSender (FNEW file rcps auth') -> pure $ XFTPReqNew file rcps auth' `verifyWith` sndKey file @@ -452,7 +479,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case pure $ FRErr e receiveChunk spec = do t <- asks $ fileTimeout . config - liftIO $ fromMaybe (Left TIMEOUT) <$> timeout t (runExceptT (receiveFile getBody spec) `catchAll_` pure (Left FILE_IO)) + liftIO $ fromMaybe (Left TIMEOUT) <$> timeout t (runExceptT $ receiveFile getBody spec) sendServerFile :: FileRec -> RcvPublicDhKey -> M (FileResponse, Maybe ServerFile) sendServerFile FileRec {senderId, filePath, fileInfo = FileInfo {size}} rDhKey = do readTVarIO filePath >>= \case diff --git a/src/Simplex/FileTransfer/Server/Env.hs b/src/Simplex/FileTransfer/Server/Env.hs index 789a3dc20..f8a6bc996 100644 --- a/src/Simplex/FileTransfer/Server/Env.hs +++ b/src/Simplex/FileTransfer/Server/Env.hs @@ -13,12 +13,9 @@ import Control.Logger.Simple import Control.Monad import Control.Monad.IO.Unlift import Crypto.Random -import Data.Default (def) import Data.Int (Int64) -import Data.List (find) import Data.List.NonEmpty (NonEmpty) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) import Data.Time.Clock (getCurrentTime) import Data.Word (Word32) import Data.X509.Validation (Fingerprint (..)) @@ -28,11 +25,12 @@ import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId) import Simplex.FileTransfer.Server.Stats import Simplex.FileTransfer.Server.Store import Simplex.FileTransfer.Server.StoreLog +import Simplex.FileTransfer.Transport (VersionRangeXFTP) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Transport (ALPN) -import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams) +import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadFingerprint, loadTLSServerParams) import Simplex.Messaging.Util (tshow) import System.IO (IOMode (..)) import UnliftIO.STM @@ -64,12 +62,15 @@ data XFTPServerConfig = XFTPServerConfig caCertificateFile :: FilePath, privateKeyFile :: FilePath, certificateFile :: FilePath, + -- | XFTP client-server protocol version range + xftpServerVRange :: VersionRangeXFTP, -- stats config - see SMP server config logStatsInterval :: Maybe Int64, logStatsStartTime :: Int64, serverStatsLogFile :: FilePath, serverStatsBackupFile :: Maybe FilePath, - transportConfig :: TransportServerConfig + transportConfig :: TransportServerConfig, + responseDelay :: Int } defaultInactiveClientExpiration :: ExpirationConfig @@ -103,7 +104,7 @@ supportedXFTPhandshakes :: [ALPN] supportedXFTPhandshakes = ["xftp/1"] newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv -newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile} = do +newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do random <- liftIO C.newRandom store <- atomically newFileStore storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile @@ -112,14 +113,7 @@ newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertifi forM_ fileSizeQuota $ \quota -> do logInfo $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used) when (quota < used) $ logInfo "WARNING: storage quota is less than used storage, no files can be uploaded!" - tlsServerParams' <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile - let tlsServerParams = - tlsServerParams' - { T.serverHooks = - def - { T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supportedXFTPhandshakes) - } - } + tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats} diff --git a/src/Simplex/FileTransfer/Server/Main.hs b/src/Simplex/FileTransfer/Server/Main.hs index 91ba17ff3..76b1f157a 100644 --- a/src/Simplex/FileTransfer/Server/Main.hs +++ b/src/Simplex/FileTransfer/Server/Main.hs @@ -7,19 +7,20 @@ module Simplex.FileTransfer.Server.Main where -import qualified Data.ByteString.Char8 as B import Data.Either (fromRight) import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Int (Int64) import Data.Maybe (fromMaybe) import qualified Data.Text as T +import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative import Simplex.FileTransfer.Chunks import Simplex.FileTransfer.Description (FileSize (..)) import Simplex.FileTransfer.Server (runXFTPServer) -import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration) +import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern XFTPServer) @@ -28,6 +29,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) @@ -68,7 +70,7 @@ xftpServerCLI cfgPath logPath = do fp <- createServerX509 cfgPath x509cfg let host = fromMaybe (if ip == "127.0.0.1" then "" else ip) fqdn srv = ProtoServerWithAuth (XFTPServer [THDomainName host] "" (C.KeyHash fp)) Nothing - writeFile iniFile $ iniFileContent host + T.writeFile iniFile $ iniFileContent host putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server." warnCAPrivateKeyFile cfgPath x509cfg printServiceInfo serverVersion srv @@ -82,7 +84,7 @@ xftpServerCLI cfgPath logPath = do \# Log is compacted on start (deleted objects are removed).\n" <> ("enable: " <> onOff enableStoreLog <> "\n\n") <> "# Expire files after the specified number of hours.\n" - <> ("expire_files_hours: " <> show defFileExpirationHours <> "\n\n") + <> ("expire_files_hours: " <> tshow defFileExpirationHours <> "\n\n") <> "log_stats: off\n\ \\n\ \[AUTH]\n\ @@ -101,20 +103,20 @@ xftpServerCLI cfgPath logPath = do \# control_port_user_password:\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" - <> ("host: " <> host <> "\n") - <> ("port: " <> defaultServerPort <> "\n") + <> ("host: " <> T.pack host <> "\n") + <> ("port: " <> T.pack defaultServerPort <> "\n") <> "log_tls_errors: off\n\ \# control_port: 5226\n\ \\n\ \[FILES]\n" - <> ("path: " <> filesPath <> "\n") - <> ("storage_quota: " <> B.unpack (strEncode fileSizeQuota) <> "\n") + <> ("path: " <> T.pack filesPath <> "\n") + <> ("storage_quota: " <> safeDecodeUtf8 (strEncode fileSizeQuota) <> "\n") <> "\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" - <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") - <> ("# check_interval: " <> show (checkInterval defaultInactiveClientExpiration) <> "\n") + <> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n") + <> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n") runServer ini = do hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering @@ -164,7 +166,7 @@ xftpServerCLI cfgPath logPath = do defaultFileExpiration { ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini }, - fileTimeout = 10 * 60 * 1000000, -- 10 mins to send 4mb chunk + fileTimeout = 5 * 60 * 1000000, -- 5 mins to send 4mb chunk inactiveClientExpiration = settingIsOn "INACTIVE_CLIENTS" "disconnect" ini $> ExpirationConfig @@ -174,14 +176,17 @@ xftpServerCLI cfgPath logPath = do caCertificateFile = c caCrtFile, privateKeyFile = c serverKeyFile, certificateFile = c serverCrtFile, + xftpServerVRange = supportedFileServerVRange, logStatsInterval = logStats $> 86400, -- seconds logStatsStartTime = 0, -- seconds from 00:00 UTC serverStatsLogFile = combine logPath "file-server-stats.daily.log", serverStatsBackupFile = logStats $> combine logPath "file-server-stats.log", transportConfig = defaultTransportServerConfig - { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini - } + { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, + alpn = Just supportedXFTPhandshakes + }, + responseDelay = 0 } data CliCommand diff --git a/src/Simplex/FileTransfer/Transport.hs b/src/Simplex/FileTransfer/Transport.hs index 49b809bf1..935fa1c42 100644 --- a/src/Simplex/FileTransfer/Transport.hs +++ b/src/Simplex/FileTransfer/Transport.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} @@ -9,6 +10,7 @@ module Simplex.FileTransfer.Transport ( supportedFileServerVRange, + authCmdsXFTPVersion, xftpClientHandshakeStub, XFTPClientHandshake (..), -- xftpClientHandshake, @@ -32,9 +34,11 @@ where import Control.Applicative ((<|>)) import qualified Control.Exception as E +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Control.Monad.Trans.Except import qualified Data.Aeson.TH as J import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first) @@ -43,17 +47,19 @@ import Data.ByteString.Builder (Builder, byteString) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Functor (($>)) import Data.Word (Word16, Word32) import qualified Data.X509 as X +import Network.HTTP2.Client (HTTP2Error) import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol (CommandError) -import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..)) +import Simplex.Messaging.Transport (SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.File -import Simplex.Messaging.Util (bshow) +import Simplex.Messaging.Util (bshow, tshow) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.IO (Handle, IOMode (..), withFile) @@ -76,18 +82,24 @@ type VersionRangeXFTP = VersionRange XFTPVersion pattern VersionXFTP :: Word16 -> VersionXFTP pattern VersionXFTP v = Version v -type THandleXFTP c = THandle XFTPVersion c -type THandleParamsXFTP = THandleParams XFTPVersion +type THandleXFTP c p = THandle XFTPVersion c p +type THandleParamsXFTP p = THandleParams XFTPVersion p initialXFTPVersion :: VersionXFTP initialXFTPVersion = VersionXFTP 1 -supportedFileServerVRange :: VersionRangeXFTP -supportedFileServerVRange = mkVersionRange initialXFTPVersion initialXFTPVersion +authCmdsXFTPVersion :: VersionXFTP +authCmdsXFTPVersion = VersionXFTP 2 --- XFTP protocol does not support handshake -xftpClientHandshakeStub :: c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c) -xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwError $ TEHandshake VERSION +currentXFTPVersion :: VersionXFTP +currentXFTPVersion = VersionXFTP 2 + +supportedFileServerVRange :: VersionRangeXFTP +supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion + +-- XFTP protocol does not use this handshake method +xftpClientHandshakeStub :: c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient) +xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwE TEVersion data XFTPServerHandshake = XFTPServerHandshake { xftpVersionRange :: VersionRangeXFTP, @@ -100,19 +112,16 @@ data XFTPClientHandshake = XFTPClientHandshake { -- | agreed XFTP server protocol version xftpVersion :: VersionXFTP, -- | server identity - CA certificate fingerprint - keyHash :: C.KeyHash, - -- | pub key to agree shared secret for entity ID encryption, shared secret for command authorization is agreed using per-queue keys. - authPubKey :: C.PublicKeyX25519 + keyHash :: C.KeyHash } instance Encoding XFTPClientHandshake where - smpEncode XFTPClientHandshake {xftpVersion, keyHash, authPubKey} = - smpEncode (xftpVersion, keyHash, authPubKey) + smpEncode XFTPClientHandshake {xftpVersion, keyHash} = + smpEncode (xftpVersion, keyHash) smpP = do (xftpVersion, keyHash) <- smpP - authPubKey <- smpP Tail _compat <- smpP - pure XFTPClientHandshake {xftpVersion, keyHash, authPubKey} + pure XFTPClientHandshake {xftpVersion, keyHash} instance Encoding XFTPServerHandshake where smpEncode XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey} = @@ -139,9 +148,14 @@ sendEncFile h send = go go sbState' $ sz - fromIntegral (B.length ch) receiveFile :: (Int -> IO ByteString) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO () -receiveFile getBody = receiveFile_ receive +receiveFile getBody chunk = ExceptT $ runExceptT (receiveFile_ receive chunk) `E.catches` handlers where receive h sz = hReceiveFile getBody h sz >>= \sz' -> pure $ if sz' == 0 then Right () else Left SIZE + handlers = + [ E.Handler $ \(e :: HTTP2Error) -> logWarn (err e) $> Left TIMEOUT, + E.Handler $ \(e :: E.SomeException) -> logError (err e) $> Left FILE_IO + ] + err e = "receiveFile error: " <> tshow e receiveEncFile :: (Int -> IO ByteString) -> LC.SbState -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO () receiveEncFile getBody = receiveFile_ . receive @@ -207,10 +221,12 @@ data XFTPErrorType HAS_FILE | -- | file IO error FILE_IO - | -- | file sending timeout + | -- | file sending or receiving timeout TIMEOUT | -- | bad redirect data REDIRECT {redirectError :: String} + | -- | cannot proceed with download from not approved relays without proxy + NOT_APPROVED | -- | internal server error INTERNAL | -- | used internally, never returned by the server (to be removed) @@ -243,6 +259,7 @@ instance Encoding XFTPErrorType where FILE_IO -> "FILE_IO" TIMEOUT -> "TIMEOUT" REDIRECT err -> "REDIRECT " <> smpEncode err + NOT_APPROVED -> "NOT_APPROVED" INTERNAL -> "INTERNAL" DUPLICATE_ -> "DUPLICATE_" @@ -262,6 +279,7 @@ instance Encoding XFTPErrorType where "FILE_IO" -> pure FILE_IO "TIMEOUT" -> pure TIMEOUT "REDIRECT" -> REDIRECT <$> _smpP + "NOT_APPROVED" -> pure NOT_APPROVED "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ _ -> fail "bad error type" diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 3f7ed0d90..8e9020b7d 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -55,6 +55,7 @@ module Simplex.Messaging.Agent deleteConnectionAsync, deleteConnectionsAsync, createConnection, + prepareConnectionToJoin, joinConnection, allowConnection, acceptContact, @@ -69,6 +70,7 @@ module Simplex.Messaging.Agent sendMessages, sendMessagesB, ackMessage, + getConnectionQueueInfo, switchConnection, abortConnectionSwitch, synchronizeRatchet, @@ -81,7 +83,6 @@ module Simplex.Messaging.Agent testProtocolServer, setNtfServers, setNetworkConfig, - getNetworkConfig, setUserNetworkInfo, reconnectAllServers, registerNtfToken, @@ -112,15 +113,17 @@ module Simplex.Messaging.Agent debugAgentLocks, getAgentStats, resetAgentStats, + getMsgCounts, getAgentSubscriptions, logConnection, ) where -import Control.Logger.Simple (logError, logInfo, showText) +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import Data.Bifunctor (bimap, first, second) @@ -137,6 +140,8 @@ import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) +import Data.Set (Set) +import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock @@ -149,7 +154,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Util (removePath) import Simplex.Messaging.Agent.Client import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Lock (withLock', withLock) +import Simplex.Messaging.Agent.Lock (withLock, withLock') import Simplex.Messaging.Agent.NtfSubSupervisor import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval @@ -157,10 +162,10 @@ import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Client (ProtocolClient (..), ServerTransmission) +import Simplex.Messaging.Client (ProtocolClient (..), SMPClientError, ServerTransmission (..), ServerTransmissionBatch, temporaryClientError, unexpectedResponse) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile, CryptoFileArgs) -import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOn, pattern PQEncOff, pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -168,8 +173,9 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..)) import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Parsers (parse) -import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (BrokerMsg, Cmd (..), EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SParty (..), SProtocolType (..), SndPublicAuthKey, SubscriptionMode (..), UserProtocol, VersionSMPC, XFTPServerWithAuth) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (SMPVersion, THandleParams (sessionId)) @@ -198,7 +204,7 @@ getSMPAgentClient_ clientId cfg initServers store backgroundMode = liftIO $ newSMPAgentEnv cfg store >>= runReaderT runAgent where runAgent = do - c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask + c@AgentClient {acThread} <- atomically . newAgentClient clientId initServers =<< ask t <- runAgentThreads c `forkFinally` const (liftIO $ disconnectAgentClient c) atomically . writeTVar acThread . Just =<< mkWeakThreadId t pure c @@ -239,7 +245,7 @@ createUser c = withAgentEnv c .: createUser' c {-# INLINE createUser #-} -- | Delete user record optionally deleting all user's connections on SMP servers -deleteUser :: AgentClient -> UserId -> Bool -> AE () +deleteUser :: AgentClient -> UserId -> Bool -> AE () deleteUser c = withAgentEnv c .: deleteUser' c {-# INLINE deleteUser #-} @@ -288,9 +294,18 @@ createConnection :: AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe createConnection c userId enableNtfs = withAgentEnv c .:: newConn c userId "" enableNtfs {-# INLINE createConnection #-} --- | Join SMP agent connection (JOIN command) -joinConnection :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AE ConnId -joinConnection c userId enableNtfs = withAgentEnv c .:: joinConn c userId "" enableNtfs +-- | Create SMP agent connection without queue (to be joined with joinConnection passing connection ID). +-- This method is required to prevent race condition when confirmation from peer is received before +-- the caller of joinConnection saves connection ID to the database. +-- Instead of it we could send confirmation asynchronously, but then it would be harder to report +-- "link deleted" (SMP AUTH) interactively, so this approach is simpler overall. +prepareConnectionToJoin :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> PQSupport -> AE ConnId +prepareConnectionToJoin c userId enableNtfs = withAgentEnv c .: newConnToJoin c userId "" enableNtfs + +-- | Join SMP agent connection (JOIN command). +joinConnection :: AgentClient -> UserId -> Maybe ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AE ConnId +joinConnection c userId Nothing enableNtfs = withAgentEnv c .:: joinConn c userId "" False enableNtfs +joinConnection c userId (Just connId) enableNtfs = withAgentEnv c .:: joinConn c userId connId True enableNtfs {-# INLINE joinConnection #-} -- | Allow connection to continue after CONF notification (LET command) @@ -344,18 +359,22 @@ sendMessage c = withAgentEnv c .:: sendMessage' c type MsgReq = (ConnId, PQEncryption, MsgFlags, MsgBody) -- | Send multiple messages to different connections (SEND command) -sendMessages :: AgentClient -> [MsgReq] -> IO [Either AgentErrorType (AgentMsgId, PQEncryption)] -sendMessages c = withAgentEnv' c . sendMessages' c +sendMessages :: AgentClient -> [MsgReq] -> AE [Either AgentErrorType (AgentMsgId, PQEncryption)] +sendMessages c = withAgentEnv c . sendMessages' c {-# INLINE sendMessages #-} -sendMessagesB :: Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> IO (t (Either AgentErrorType (AgentMsgId, PQEncryption))) -sendMessagesB c = withAgentEnv' c . sendMessagesB' c +sendMessagesB :: Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> AE (t (Either AgentErrorType (AgentMsgId, PQEncryption))) +sendMessagesB c = withAgentEnv c . sendMessagesB' c {-# INLINE sendMessagesB #-} ackMessage :: AgentClient -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AE () ackMessage c = withAgentEnv c .:. ackMessage' c {-# INLINE ackMessage #-} +getConnectionQueueInfo :: AgentClient -> ConnId -> AE QueueInfo +getConnectionQueueInfo c = withAgentEnv c . getConnectionQueueInfo' c +{-# INLINE getConnectionQueueInfo #-} + -- | Switch connection to the new receive queue switchConnection :: AgentClient -> ConnId -> AE ConnectionStats switchConnection c = withAgentEnv c . switchConnection' c @@ -413,25 +432,22 @@ setNetworkConfig c@AgentClient {useNetworkConfig} cfg' = do else True <$ (writeTVar useNetworkConfig $! (slowNetworkConfig cfg', cfg')) when changed $ reconnectAllServers c --- returns fast network config -getNetworkConfig :: AgentClient -> IO NetworkConfig -getNetworkConfig = fmap snd . readTVarIO . useNetworkConfig -{-# INLINE getNetworkConfig #-} - setUserNetworkInfo :: AgentClient -> UserNetworkInfo -> IO () -setUserNetworkInfo c@AgentClient {userNetworkState} UserNetworkInfo {networkType = nt'} = withAgentEnv' c $ do - d <- asks $ initialInterval . userNetworkInterval . config - ts <- liftIO getCurrentTime - atomically $ do - ns@UserNetworkState {networkType = nt} <- readTVar userNetworkState - when (nt' /= nt) $ - writeTVar userNetworkState $! case nt' of - UNNone -> ns {networkType = nt', offline = Just UNSOffline {offlineDelay = d, offlineFrom = ts}} - _ -> ns {networkType = nt', offline = Nothing} +setUserNetworkInfo c@AgentClient {userNetworkInfo, userNetworkUpdated} ni = withAgentEnv' c $ do + ts' <- liftIO getCurrentTime + i <- asks $ userOfflineDelay . config + -- if network offline event happens in less than `userOfflineDelay` after the previous event, it is ignored + atomically . whenM ((isOnline ni ||) <$> notRecentlyChanged ts' i) $ do + writeTVar userNetworkInfo ni + writeTVar userNetworkUpdated $ Just ts' + where + notRecentlyChanged ts' i = + maybe True (\ts -> diffUTCTime ts' ts > i) <$> readTVar userNetworkUpdated reconnectAllServers :: AgentClient -> IO () reconnectAllServers c = do reconnectServerClients c smpClients + reconnectServerClients c xftpClients reconnectServerClients c ntfClients -- | Register device notifications token @@ -470,8 +486,8 @@ xftpStartWorkers c = withAgentEnv c . startXFTPWorkers c {-# INLINE xftpStartWorkers #-} -- | Receive XFTP file -xftpReceiveFile :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AE RcvFileId -xftpReceiveFile c = withAgentEnv c .:. xftpReceiveFile' c +xftpReceiveFile :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> Bool -> AE RcvFileId +xftpReceiveFile c = withAgentEnv c .:: xftpReceiveFile' c {-# INLINE xftpReceiveFile #-} -- | Delete XFTP rcv file (deletes work files from file system and db records) @@ -541,6 +557,9 @@ resetAgentStats :: AgentClient -> IO () resetAgentStats = atomically . TM.clear . agentStats {-# INLINE resetAgentStats #-} +getMsgCounts :: AgentClient -> IO [(ConnId, (Int, Int))] -- (total, duplicates) +getMsgCounts c = readTVarIO (msgCounts c) >>= mapM (\(connId, cnt) -> (connId,) <$> readTVarIO cnt) . M.assocs + withAgentEnv' :: AgentClient -> AM' a -> IO a withAgentEnv' c = (`runReaderT` agentEnv c) {-# INLINE withAgentEnv' #-} @@ -552,7 +571,7 @@ withAgentEnv c a = ExceptT $ runExceptT a `runReaderT` agentEnv c logConnection :: AgentClient -> Bool -> IO () logConnection c connected = let event = if connected then "connected to" else "disconnected from" - in logInfo $ T.unwords ["client", showText (clientId c), event, "Agent"] + in logInfo $ T.unwords ["client", tshow (clientId c), event, "Agent"] -- | Runs an SMP agent instance that receives commands and sends responses via 'TBQueue's. runAgentClient :: AgentClient -> AM' () @@ -572,7 +591,7 @@ processCommand :: AgentClient -> (EntityId, APartyCmd 'Client) -> AM (EntityId, processCommand c (connId, APC e cmd) = second (APC e) <$> case cmd of NEW enableNtfs (ACM cMode) pqIK subMode -> second (INV . ACR cMode) <$> newConn c userId connId enableNtfs cMode Nothing pqIK subMode - JOIN enableNtfs (ACR _ cReq) pqEnc subMode connInfo -> (,OK) <$> joinConn c userId connId enableNtfs cReq connInfo pqEnc subMode + JOIN enableNtfs (ACR _ cReq) pqEnc subMode connInfo -> (,OK) <$> joinConn c userId connId False enableNtfs cReq connInfo pqEnc subMode LET confId ownCInfo -> allowConnection' c connId confId ownCInfo $> (connId, OK) ACPT invId pqEnc ownCInfo -> (,OK) <$> acceptContact' c connId True invId ownCInfo pqEnc SMSubscribe RJCT invId -> rejectContact' c connId invId $> (connId, OK) @@ -615,14 +634,14 @@ newConnAsync c userId corrId enableNtfs cMode pqInitKeys subMode = do newConnNoQueues :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> PQSupport -> AM ConnId newConnNoQueues c userId connId enableNtfs cMode pqSupport = do g <- asks random - connAgentVersion <- asks $ maxVersion . ($ pqSupport) . smpAgentVRange . config + connAgentVersion <- asks $ maxVersion . smpAgentVRange . config let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} withStore c $ \db -> createNewConn db g cData cMode joinConnAsync :: AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId joinConnAsync c userId corrId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup subMode = do withInvLock c (strEncode cReqUri) "joinConnAsync" $ do - lift (compatibleInvitationUri cReqUri pqSup) >>= \case + lift (compatibleInvitationUri cReqUri) >>= \case Just (_, Compatible (CR.E2ERatchetParams v _ _ _), Compatible connAgentVersion) -> do g <- asks random let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) @@ -632,14 +651,14 @@ joinConnAsync c userId corrId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup pure connId Nothing -> throwError $ AGENT A_VERSION joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _subMode _cInfo _pqEncryption = - throwError $ CMD PROHIBITED + throwE $ CMD PROHIBITED "joinConnAsync" allowConnectionAsync' :: AgentClient -> ACorrId -> ConnId -> ConfirmationId -> ConnInfo -> AM () allowConnectionAsync' c corrId connId confId ownConnInfo = withStore c (`getConn` connId) >>= \case SomeConn _ (RcvConnection _ RcvQueue {server}) -> enqueueCommand c corrId connId (Just server) $ AClientCommand $ APC SAEConn $ LET confId ownConnInfo - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "allowConnectionAsync" acceptContactAsync' :: AgentClient -> ACorrId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId acceptContactAsync' c corrId enableNtfs invId ownConnInfo pqSupport subMode = do @@ -650,7 +669,7 @@ acceptContactAsync' c corrId enableNtfs invId ownConnInfo pqSupport subMode = do joinConnAsync c userId corrId enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "acceptContactAsync" ackMessageAsync' :: AgentClient -> ACorrId -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> AM () ackMessageAsync' c corrId connId msgId rcptInfo_ = do @@ -659,14 +678,14 @@ ackMessageAsync' c corrId connId msgId rcptInfo_ = do SCDuplex -> enqueueAck SCRcv -> enqueueAck SCSnd -> throwError $ CONN SIMPLEX - SCContact -> throwError $ CMD PROHIBITED - SCNew -> throwError $ CMD PROHIBITED + SCContact -> throwE $ CMD PROHIBITED "ackMessageAsync: SCContact" + SCNew -> throwE $ CMD PROHIBITED "ackMessageAsync: SCNew" where enqueueAck :: AM () enqueueAck = do let mId = InternalId msgId RcvMsg {msgType} <- withStore c $ \db -> getRcvMsg db connId mId - when (isJust rcptInfo_ && msgType /= AM_A_MSG_) $ throwError $ CMD PROHIBITED + when (isJust rcptInfo_ && msgType /= AM_A_MSG_) $ throwE $ CMD PROHIBITED "ackMessageAsync: receipt not allowed" (RcvQueue {server}, _) <- withStore c $ \db -> setMsgUserAck db connId mId enqueueCommand c corrId connId (Just server) . AClientCommand $ APC SAEConn $ ACK msgId rcptInfo_ @@ -694,146 +713,164 @@ switchConnectionAsync' c corrId connId = withConnLock c connId "switchConnectionAsync" $ withStore c (`getConn` connId) >>= \case SomeConn _ (DuplexConnection cData rqs@(rq :| _rqs) sqs) - | isJust (switchingRQ rqs) -> throwError $ CMD PROHIBITED + | isJust (switchingRQ rqs) -> throwE $ CMD PROHIBITED "switchConnectionAsync: already switching" | otherwise -> do - when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED + when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "switchConnectionAsync: send prohibited" rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn SWCH let rqs' = updatedQs rq1 rqs pure . connectionStats $ DuplexConnection cData rqs' sqs - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "switchConnectionAsync: not duplex" newConn :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> AM (ConnId, ConnectionRequestUri c) newConn c userId connId enableNtfs cMode clientData pqInitKeys subMode = - getSMPServer c userId >>= newConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode + getSMPServer c userId >>= newConnSrv c userId connId False enableNtfs cMode clientData pqInitKeys subMode -newConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) -newConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv = do - connId' <- newConnNoQueues c userId connId enableNtfs cMode (CR.connPQEncryption pqInitKeys) +newConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) +newConnSrv c userId connId hasNewConn enableNtfs cMode clientData pqInitKeys subMode srv = do + connId' <- + if hasNewConn + then pure connId + else newConnNoQueues c userId connId enableNtfs cMode (CR.connPQEncryption pqInitKeys) newRcvConnSrv c userId connId' enableNtfs cMode clientData pqInitKeys subMode srv newRcvConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> CR.InitialKeys -> SubscriptionMode -> SMPServerWithAuth -> AM (ConnId, ConnectionRequestUri c) newRcvConnSrv c userId connId enableNtfs cMode clientData pqInitKeys subMode srv = do case (cMode, pqInitKeys) of - (SCMContact, CR.IKUsePQ) -> throwError $ CMD PROHIBITED + (SCMContact, CR.IKUsePQ) -> throwE $ CMD PROHIBITED "newRcvConnSrv" _ -> pure () AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config - (rq, qUri) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv smpClientVRange subMode `catchAgentError` \e -> liftIO (print e) >> throwError e rq' <- withStore c $ \db -> updateNewConnRcv db connId rq - liftIO $ case subMode of - SMOnlyCreate -> pure () - SMSubscribe -> addSubscription c rq' + lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId when enableNtfs $ do ns <- asks ntfSupervisor atomically $ sendNtfSubCommand ns (connId, NSCCreate) - let pqEnc = CR.connPQEncryption pqInitKeys - crData = ConnReqUriData SSSimplex (smpAgentVRange pqEnc) [qUri] clientData - e2eVRange = e2eEncryptVRange pqEnc + let crData = ConnReqUriData SSSimplex smpAgentVRange [qUri] clientData case cMode of SCMContact -> pure (connId, CRContactUri crData) SCMInvitation -> do g <- asks random - (pk1, pk2, pKem, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eVRange) (CR.initialPQEncryption pqInitKeys) + (pk1, pk2, pKem, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) (CR.initialPQEncryption pqInitKeys) withStore' c $ \db -> createRatchetX3dhKeys db connId pk1 pk2 pKem - pure (connId, CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eVRange) + pure (connId, CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eEncryptVRange) -joinConn :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId -joinConn c userId connId enableNtfs cReq cInfo pqSupport subMode = do +newConnToJoin :: forall c. AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> PQSupport -> AM ConnId +newConnToJoin c userId connId enableNtfs cReq pqSup = case cReq of + CRInvitationUri {} -> + lift (compatibleInvitationUri cReq) >>= \case + Just (_, (Compatible (CR.E2ERatchetParams v _ _ _)), aVersion) -> create aVersion (Just v) + Nothing -> throwError $ AGENT A_VERSION + CRContactUri {} -> + lift (compatibleContactUri cReq) >>= \case + Just (_, aVersion) -> create aVersion Nothing + Nothing -> throwError $ AGENT A_VERSION + where + create :: Compatible VersionSMPA -> Maybe CR.VersionE2E -> AM ConnId + create (Compatible connAgentVersion) e2eV_ = do + g <- asks random + let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion e2eV_ + cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} + withStore c $ \db -> createNewConn db g cData SCMInvitation + +joinConn :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId +joinConn c userId connId hasNewConn enableNtfs cReq cInfo pqSupport subMode = do srv <- case cReq of CRInvitationUri ConnReqUriData {crSmpQueues = q :| _} _ -> getNextServer c userId [qServer q] _ -> getSMPServer c userId - joinConnSrv c userId connId enableNtfs cReq cInfo pqSupport subMode srv + joinConnSrv c userId connId hasNewConn enableNtfs cReq cInfo pqSupport subMode srv -startJoinInvitation :: UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM (Compatible VersionSMPA, ConnData, NewSndQueue, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) +startJoinInvitation :: UserId -> ConnId -> Bool -> ConnectionRequestUri 'CMInvitation -> PQSupport -> AM (ConnData, NewSndQueue, CR.Ratchet 'C.X448, CR.SndE2ERatchetParams 'C.X448) startJoinInvitation userId connId enableNtfs cReqUri pqSup = - lift (compatibleInvitationUri cReqUri pqSup) >>= \case - Just (qInfo, (Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_)), aVersion@(Compatible connAgentVersion)) -> do + lift (compatibleInvitationUri cReqUri) >>= \case + Just (qInfo, (Compatible e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_)), Compatible connAgentVersion) -> do g <- asks random let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) (pk1, pk2, pKem, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ v kem_ pqSupport) (_, rcDHRs) <- atomically $ C.generateKeyPair g rcParams <- liftEitherWith cryptoError $ CR.pqX3dhSnd pk1 pk2 pKem e2eRcvParams - maxSupported <- asks $ maxVersion . ($ pqSup) . e2eEncryptVRange . config + maxSupported <- asks $ maxVersion . e2eEncryptVRange . config let rcVs = CR.RatchetVersions {current = v, maxSupported} rc = CR.initSndRatchet rcVs rcDHRr rcDHRs rcParams q <- lift $ newSndQueue userId "" qInfo let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} - pure (aVersion, cData, q, rc, e2eSndParams) + pure (cData, q, rc, e2eSndParams) Nothing -> throwError $ AGENT A_VERSION connRequestPQSupport :: AgentClient -> PQSupport -> ConnectionRequestUri c -> IO (Maybe (VersionSMPA, PQSupport)) connRequestPQSupport c pqSup cReq = withAgentEnv' c $ case cReq of - CRInvitationUri {} -> invPQSupported <$$> compatibleInvitationUri cReq pqSup + CRInvitationUri {} -> invPQSupported <$$> compatibleInvitationUri cReq where invPQSupported (_, Compatible (CR.E2ERatchetParams e2eV _ _ _), Compatible agentV) = (agentV, pqSup `CR.pqSupportAnd` versionPQSupport_ agentV (Just e2eV)) - CRContactUri {} -> ctPQSupported <$$> compatibleContactUri cReq pqSup + CRContactUri {} -> ctPQSupported <$$> compatibleContactUri cReq where ctPQSupported (_, Compatible agentV) = (agentV, pqSup `CR.pqSupportAnd` versionPQSupport_ agentV Nothing) -compatibleInvitationUri :: ConnectionRequestUri 'CMInvitation -> PQSupport -> AM' (Maybe (Compatible SMPQueueInfo, Compatible (CR.RcvE2ERatchetParams 'C.X448), Compatible VersionSMPA)) -compatibleInvitationUri (CRInvitationUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)} e2eRcvParamsUri) pqSup = do +compatibleInvitationUri :: ConnectionRequestUri 'CMInvitation -> AM' (Maybe (Compatible SMPQueueInfo, Compatible (CR.RcvE2ERatchetParams 'C.X448), Compatible VersionSMPA)) +compatibleInvitationUri (CRInvitationUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)} e2eRcvParamsUri) = do AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config pure $ (,,) <$> (qUri `compatibleVersion` smpClientVRange) - <*> (e2eRcvParamsUri `compatibleVersion` e2eEncryptVRange pqSup) - <*> (crAgentVRange `compatibleVersion` smpAgentVRange pqSup) + <*> (e2eRcvParamsUri `compatibleVersion` e2eEncryptVRange) + <*> (crAgentVRange `compatibleVersion` smpAgentVRange) -compatibleContactUri :: ConnectionRequestUri 'CMContact -> PQSupport -> AM' (Maybe (Compatible SMPQueueInfo, Compatible VersionSMPA)) -compatibleContactUri (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)}) pqSup = do +compatibleContactUri :: ConnectionRequestUri 'CMContact -> AM' (Maybe (Compatible SMPQueueInfo, Compatible VersionSMPA)) +compatibleContactUri (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)}) = do AgentConfig {smpClientVRange, smpAgentVRange} <- asks config pure $ (,) - <$> (qUri `compatibleVersion` smpClientVRange) - <*> (crAgentVRange `compatibleVersion` smpAgentVRange pqSup) + <$> (qUri `compatibleVersion` smpClientVRange) + <*> (crAgentVRange `compatibleVersion` smpAgentVRange) versionPQSupport_ :: VersionSMPA -> Maybe CR.VersionE2E -> PQSupport versionPQSupport_ agentV e2eV_ = PQSupport $ agentV >= pqdrSMPAgentVersion && maybe True (>= CR.pqRatchetE2EEncryptVersion) e2eV_ {-# INLINE versionPQSupport_ #-} -joinConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM ConnId -joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSup subMode srv = +joinConnSrv :: AgentClient -> UserId -> ConnId -> Bool -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM ConnId +joinConnSrv c userId connId hasNewConn enableNtfs inv@CRInvitationUri {} cInfo pqSup subMode srv = withInvLock c (strEncode inv) "joinConnSrv" $ do - (aVersion, cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSup + (cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSup g <- asks random (connId', sq) <- withStore c $ \db -> runExceptT $ do - r@(connId', _) <- ExceptT $ createSndConn db g cData q + r@(connId', _) <- + if hasNewConn + then (connId,) <$> ExceptT (updateNewConnSnd db connId q) + else ExceptT $ createSndConn db g cData q liftIO $ createRatchet db connId' rc pure r let cData' = (cData :: ConnData) {connId = connId'} - tryError (confirmQueue aVersion c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case + tryError (confirmQueue c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case Right _ -> pure connId' Left e -> do -- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md void $ withStore' c $ \db -> deleteConn db Nothing connId' throwError e -joinConnSrv c userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode srv = - lift (compatibleContactUri cReqUri pqSup) >>= \case +joinConnSrv c userId connId hasNewConn enableNtfs cReqUri@CRContactUri {} cInfo pqSup subMode srv = + lift (compatibleContactUri cReqUri) >>= \case Just (qInfo, vrsn) -> do - (connId', cReq) <- newConnSrv c userId connId enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv - sendInvitation c userId qInfo vrsn cReq cInfo + (connId', cReq) <- newConnSrv c userId connId hasNewConn enableNtfs SCMInvitation Nothing (CR.IKNoPQ pqSup) subMode srv + void $ sendInvitation c userId qInfo vrsn cReq cInfo pure connId' Nothing -> throwError $ AGENT A_VERSION joinConnSrvAsync :: AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> SMPServerWithAuth -> AM () joinConnSrvAsync c userId connId enableNtfs inv@CRInvitationUri {} cInfo pqSupport subMode srv = do - (_aVersion, cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSupport + (cData, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv pqSupport q' <- withStore c $ \db -> runExceptT $ do liftIO $ createRatchet db connId rc ExceptT $ updateNewConnSnd db connId q confirmQueueAsync c cData q' srv cInfo (Just e2eSndParams) subMode joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode _pqSupport _srv = do - throwError $ CMD PROHIBITED + throwE $ CMD PROHIBITED "joinConnSrvAsync" createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do - (rq, qUri) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode + (rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) subMode let qInfo = toVersionT qUri smpClientVersion rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq - liftIO $ case subMode of - SMOnlyCreate -> pure () - SMSubscribe -> addSubscription c rq' + lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId when enableNtfs $ do ns <- asks ntfSupervisor atomically $ sendNtfSubCommand ns (connId, NSCCreate) @@ -851,7 +888,7 @@ allowConnection' c connId confId ownConnInfo = withConnLock c connId "allowConne liftIO $ setRcvQueueConfirmedE2E db rq dhSecret $ min v v' pure senderKey enqueueCommand c "" connId (Just server) . AInternalCommand $ ICAllowSecure rcvId senderKey - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "allowConnection" -- | Accept contact (ACPT command) in Reader monad acceptContact' :: AgentClient -> ConnId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId @@ -860,10 +897,10 @@ acceptContact' c connId enableNtfs invId ownConnInfo pqSupport subMode = withCon withStore c (`getConn` contactConnId) >>= \case SomeConn _ (ContactConnection ConnData {userId} _) -> do withStore' c $ \db -> acceptInvitation db invId ownConnInfo - joinConn c userId connId enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do + joinConn c userId connId False enableNtfs connReq ownConnInfo pqSupport subMode `catchAgentError` \err -> do withStore' c (`unacceptInvitation` invId) throwError err - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "acceptContact" -- | Reject contact (RJCT command) in Reader monad rejectContact' :: AgentClient -> ConnId -> InvitationId -> AM () @@ -893,7 +930,7 @@ subscribeConnections' c connIds = do (subRs, rcvQs) = M.mapEither rcvQueueOrResult cs mapM_ (mapM_ (\(cData, sqs) -> mapM_ (lift . resumeMsgDelivery c cData) sqs) . sndQueue) cs mapM_ (resumeConnCmds c) $ M.keys cs - rcvRs <- lift $ connResults <$> subscribeQueues c (concat $ M.elems rcvQs) + rcvRs <- lift $ connResults . fst <$> subscribeQueues c (concat $ M.elems rcvQs) ns <- asks ntfSupervisor tkn <- readTVarIO (ntfTkn ns) when (instantNotifications tkn) . void . lift . forkIO . void . runExceptT $ sendNtfCreate ns rcvRs conns @@ -963,14 +1000,14 @@ resubscribeConnections' c connIds = do getConnectionMessage' :: AgentClient -> ConnId -> AM (Maybe SMPMsgMeta) getConnectionMessage' c connId = do - whenM (atomically $ hasActiveSubscription c connId) . throwError $ CMD PROHIBITED + whenM (atomically $ hasActiveSubscription c connId) . throwE $ CMD PROHIBITED "getConnectionMessage: subscribed" SomeConn _ conn <- withStore c (`getConn` connId) case conn of DuplexConnection _ (rq :| _) _ -> getQueueMessage c rq RcvConnection _ rq -> getQueueMessage c rq ContactConnection _ rq -> getQueueMessage c rq SndConnection _ _ -> throwError $ CONN SIMPLEX - NewConnection _ -> throwError $ CMD PROHIBITED + NewConnection _ -> throwE $ CMD PROHIBITED "getConnectionMessage: NewConnection" getNotificationMessage' :: AgentClient -> C.CbNonce -> ByteString -> AM (NotificationInfo, [SMPMsgMeta]) getNotificationMessage' c nonce encNtfInfo = do @@ -982,7 +1019,7 @@ getNotificationMessage' c nonce encNtfInfo = do ntfMsgMeta <- (eitherToMaybe . smpDecode <$> agentCbDecrypt rcvNtfDhSecret nmsgNonce encNMsgMeta) `catchAgentError` \_ -> pure Nothing maxMsgs <- asks $ ntfMaxMessages . config (NotificationInfo {ntfConnId, ntfTs, ntfMsgMeta},) <$> getNtfMessages ntfConnId ntfMsgMeta maxMsgs - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "getNotificationMessage" where getNtfMessages ntfConnId nMeta = getMsg where @@ -999,16 +1036,27 @@ getNotificationMessage' c nonce encNtfInfo = do -- | Send message to the connection (SEND command) in Reader monad sendMessage' :: AgentClient -> ConnId -> PQEncryption -> MsgFlags -> MsgBody -> AM (AgentMsgId, PQEncryption) -sendMessage' c connId pqEnc msgFlags msg = ExceptT $ runIdentity <$> sendMessagesB' c (Identity (Right (connId, pqEnc, msgFlags, msg))) +sendMessage' c connId pqEnc msgFlags msg = ExceptT $ runIdentity <$> sendMessagesB_ c (Identity (Right (connId, pqEnc, msgFlags, msg))) (S.singleton connId) {-# INLINE sendMessage' #-} -- | Send multiple messages to different connections (SEND command) in Reader monad -sendMessages' :: AgentClient -> [MsgReq] -> AM' [Either AgentErrorType (AgentMsgId, PQEncryption)] +sendMessages' :: AgentClient -> [MsgReq] -> AM [Either AgentErrorType (AgentMsgId, PQEncryption)] sendMessages' c = sendMessagesB' c . map Right {-# INLINE sendMessages' #-} -sendMessagesB' :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> AM' (t (Either AgentErrorType (AgentMsgId, PQEncryption))) -sendMessagesB' c reqs = withConnLocks c connIds "sendMessages" $ do +sendMessagesB' :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> AM (t (Either AgentErrorType (AgentMsgId, PQEncryption))) +sendMessagesB' c reqs = do + connIds <- liftEither $ foldl' addConnId (Right S.empty) reqs + lift $ sendMessagesB_ c reqs connIds + where + addConnId s@(Right s') (Right (connId, _, _, _)) + | B.null connId = s + | connId `S.notMember` s' = Right $ S.insert connId s' + | otherwise = Left $ INTERNAL "sendMessages: duplicate connection ID" + addConnId s _ = s + +sendMessagesB_ :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType MsgReq) -> Set ConnId -> AM' (t (Either AgentErrorType (AgentMsgId, PQEncryption))) +sendMessagesB_ c reqs connIds = withConnLocks c connIds "sendMessages" $ do reqs' <- withStoreBatch c (\db -> fmap (bindRight $ \req@(connId, _, _, _) -> bimap storeError (req,) <$> getConn db connId) reqs) let (toEnable, reqs'') = mapAccumL prepareConn [] reqs' void $ withStoreBatch' c $ \db -> map (\connId -> setConnPQSupport db connId PQSupportOn) toEnable @@ -1023,14 +1071,13 @@ sendMessagesB' c reqs = withConnLocks c connIds "sendMessages" $ do where prepareMsg :: ConnData -> NonEmpty SndQueue -> ([ConnId], Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) prepareMsg cData@ConnData {connId, pqSupport} sqs - | ratchetSyncSendProhibited cData = (acc, Left $ CMD PROHIBITED) + | ratchetSyncSendProhibited cData = (acc, Left $ CMD PROHIBITED "sendMessagesB: send prohibited") -- connection is only updated if PQ encryption was disabled, and now it has to be enabled. -- support for PQ encryption (small message envelopes) will not be disabled when message is sent. | pqEnc == PQEncOn && pqSupport == PQSupportOff = let cData' = cData {pqSupport = PQSupportOn} :: ConnData in (connId : acc, Right (cData', sqs, Just pqEnc, msgFlags, A_MSG msg)) | otherwise = (acc, Right (cData, sqs, Just pqEnc, msgFlags, A_MSG msg)) - connIds = map (\(connId, _, _, _) -> connId) $ rights $ toList reqs -- / async command processing v v v @@ -1086,7 +1133,7 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do withStore c (`getConn` connId) >>= \case SomeConn _ conn@(DuplexConnection _ (replaced :| _rqs) _) -> switchDuplexConnection c conn replaced >>= notify . SWITCH QDRcv SPStarted - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "SWCH: not duplex" DEL -> withServer' . tryCommand $ deleteConnection' c connId >> notify OK _ -> notify $ ERR $ INTERNAL $ "unsupported async command " <> show (aCommandTag cmd) AInternalCommand cmd -> case cmd of @@ -1206,7 +1253,7 @@ enqueueMessage c cData sq msgFlags aMessage = {-# INLINE enqueueMessage #-} -- this function is used only for sending messages in batch, it returns the list of successes to enqueue additional deliveries -enqueueMessageB :: forall t. (Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) +enqueueMessageB :: forall t. Traversable t => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage)) -> AM' (t (Either AgentErrorType ((AgentMsgId, PQEncryption), Maybe (ConnData, [SndQueue], AgentMsgId)))) enqueueMessageB c reqs = do cfg <- asks config reqMids <- withStoreBatch c $ \db -> fmap (bindRight $ storeSentMsg db cfg) reqs @@ -1216,7 +1263,7 @@ enqueueMessageB c reqs = do pure $ Right ((msgId, pqSecr), if null sqs' then Nothing else Just (cData, sqs', msgId)) where storeSentMsg :: DB.Connection -> AgentConfig -> (ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage) -> IO (Either AgentErrorType ((ConnData, NonEmpty SndQueue, Maybe PQEncryption, MsgFlags, AMessage), InternalId, PQEncryption)) - storeSentMsg db cfg req@(cData@ConnData {connId, pqSupport}, sq :| _, pqEnc_, msgFlags, aMessage) = fmap (first storeError) $ runExceptT $ do + storeSentMsg db cfg req@(cData@ConnData {connId}, sq :| _, pqEnc_, msgFlags, aMessage) = fmap (first storeError) $ runExceptT $ do let AgentConfig {smpAgentVRange, e2eEncryptVRange} = cfg internalTs <- liftIO getCurrentTime (internalId, internalSndId, prevMsgHash) <- liftIO $ updateSndIds db connId @@ -1224,10 +1271,9 @@ enqueueMessageB c reqs = do agentMsg = AgentMessage privHeader aMessage agentMsgStr = smpEncode agentMsg internalHash = C.sha256Hash agentMsgStr - currentE2EVersion = maxVersion $ e2eEncryptVRange PQSupportOff - (encAgentMessage, pqEnc) <- agentRatchetEncrypt db cData agentMsgStr e2eEncUserMsgLength pqEnc_ currentE2EVersion - -- agent version range is determined by the connection suppport of PQ encryption, that is may be enabled when message is sent - let agentVersion = maxVersion $ smpAgentVRange pqSupport + currentE2EVersion = maxVersion e2eEncryptVRange + (encAgentMessage, pqEnc) <- agentRatchetEncrypt db cData agentMsgStr e2eEncAgentMsgLength pqEnc_ currentE2EVersion + let agentVersion = maxVersion smpAgentVRange msgBody = smpEncode $ AgentMsgEnvelope {agentVersion, encAgentMessage} msgType = agentMessageType agentMsg msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgFlags, msgBody, pqEncryption = pqEnc, internalHash, prevMsgHash} @@ -1239,7 +1285,7 @@ enqueueSavedMessage :: AgentClient -> ConnData -> AgentMsgId -> SndQueue -> AM' enqueueSavedMessage c cData msgId sq = enqueueSavedMessageB c $ Identity (cData, [sq], msgId) {-# INLINE enqueueSavedMessage #-} -enqueueSavedMessageB :: (Foldable t) => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () +enqueueSavedMessageB :: Foldable t => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> AM' () enqueueSavedMessageB c reqs = do -- saving to the database is in the start to avoid race conditions when delivery is read from queue before it is saved void $ withStoreBatch' c $ \db -> concatMap (storeDeliveries db) reqs @@ -1283,7 +1329,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork let mId = unId msgId ri' = maybe id updateRetryInterval2 msgRetryState ri withRetryLock2 ri' qLock $ \riState loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c resp <- tryError $ case msgType of AM_CONN_INFO -> sendConfirmation c sq msgBody AM_CONN_INFO_REPLY -> sendConfirmation c sq msgBody @@ -1292,13 +1338,17 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork Left e -> do let err = if msgType == AM_A_MSG_ then MERR mId e else ERR e case e of - SMP SMP.QUOTA -> case msgType of + SMP _ SMP.QUOTA -> case msgType of AM_CONN_INFO -> connError msgId NOT_AVAILABLE AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE _ -> do expireTs <- addUTCTime (-quotaExceededTimeout) <$> liftIO getCurrentTime - if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RISlow - SMP SMP.AUTH -> case msgType of + if internalTs < expireTs + then notifyDelMsgs msgId e expireTs + else do + notify $ MWARN (unId msgId) e + retrySndMsg RISlow + SMP _ SMP.AUTH -> case msgType of AM_CONN_INFO -> connError msgId NOT_AVAILABLE AM_CONN_INFO_REPLY -> connError msgId NOT_AVAILABLE AM_RATCHET_INFO -> connError msgId NOT_AVAILABLE @@ -1324,13 +1374,17 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork | temporaryOrHostError e -> do let msgTimeout = if msgType == AM_HELLO_ then helloTimeout else messageTimeout expireTs <- addUTCTime (-msgTimeout) <$> liftIO getCurrentTime - if internalTs < expireTs then notifyDelMsgs msgId e expireTs else retrySndMsg RIFast + if internalTs < expireTs + then notifyDelMsgs msgId e expireTs + else do + when (serverHostError e) $ notify $ MWARN (unId msgId) e + retrySndMsg RIFast | otherwise -> notifyDel msgId err where retrySndMsg riMode = do withStore' c $ \db -> updatePendingMsgRIState db connId msgId riState retrySndOp c $ loop riMode - Right () -> do + Right proxySrv_ -> do case msgType of AM_CONN_INFO -> setConfirmed AM_CONN_INFO_REPLY -> setConfirmed @@ -1352,7 +1406,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq (Worker {doWork when (status == Active) $ notify $ CON pqEncryption -- this branch should never be reached as receive queue is created before the confirmation, _ -> logError "HELLO sent without receive queue" - AM_A_MSG_ -> notify $ SENT mId + AM_A_MSG_ -> notify $ SENT mId proxySrv_ AM_A_RCVD_ -> pure () AM_QCONT_ -> pure () AM_QADD_ -> pure () @@ -1429,8 +1483,8 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do DuplexConnection {} -> ack >> sendRcpt conn >> del RcvConnection {} -> ack >> del SndConnection {} -> throwError $ CONN SIMPLEX - ContactConnection {} -> throwError $ CMD PROHIBITED - NewConnection _ -> throwError $ CMD PROHIBITED + ContactConnection {} -> throwE $ CMD PROHIBITED "ackMessage: ContactConnection" + NewConnection _ -> throwE $ CMD PROHIBITED "ackMessage: NewConnection" where ack :: AM () ack = do @@ -1444,7 +1498,7 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do msg@RcvMsg {msgType, msgReceipt} <- withStore c $ \db -> getRcvMsg db connId $ InternalId msgId case rcptInfo_ of Just rcptInfo -> do - unless (msgType == AM_A_MSG_) $ throwError (CMD PROHIBITED) + unless (msgType == AM_A_MSG_) . throwE $ CMD PROHIBITED "ackMessage: receipt not allowed" when (connAgentVersion >= deliveryRcptsSMPAgentVersion) $ do let RcvMsg {msgMeta = MsgMeta {sndMsgId}, internalHash} = msg rcpt = A_RCVD [AMessageReceipt {agentMsgId = sndMsgId, msgHash = internalHash, rcptInfo}] @@ -1455,17 +1509,27 @@ ackMessage' c connId msgId rcptInfo_ = withConnLock c connId "ackMessage" $ do withStore' c $ \db -> deleteDeliveredSndMsg db connId $ InternalId sndMsgId _ -> pure () +getConnectionQueueInfo' :: AgentClient -> ConnId -> AM QueueInfo +getConnectionQueueInfo' c connId = do + SomeConn _ conn <- withStore c (`getConn` connId) + case conn of + DuplexConnection _ (rq :| _) _ -> getQueueInfo c rq + RcvConnection _ rq -> getQueueInfo c rq + ContactConnection _ rq -> getQueueInfo c rq + SndConnection {} -> throwE $ CONN SIMPLEX + NewConnection _ -> throwE $ CMD PROHIBITED "getConnectionQueueInfo': NewConnection" + switchConnection' :: AgentClient -> ConnId -> AM ConnectionStats switchConnection' c connId = withConnLock c connId "switchConnection" $ withStore c (`getConn` connId) >>= \case SomeConn _ conn@(DuplexConnection cData rqs@(rq :| _rqs) _) - | isJust (switchingRQ rqs) -> throwError $ CMD PROHIBITED + | isJust (switchingRQ rqs) -> throwE $ CMD PROHIBITED "switchConnection: already switching" | otherwise -> do - when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED + when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "switchConnection: send prohibited" rq' <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted switchDuplexConnection c conn rq' - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "switchConnection: not duplex" switchDuplexConnection :: AgentClient -> Connection 'CDuplex -> RcvQueue -> AM ConnectionStats switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs sqs) rq@RcvQueue {server, dbQueueId = DBQueueId dbQueueId, sndId} = do @@ -1474,10 +1538,10 @@ switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs s -- try to get the server that is different from all queues, or at least from the primary rcv queue srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs) srv' <- if srv == server then getNextServer c userId [server] else pure srvAuth - (q, qUri) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe + (q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SMSubscribe let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId} rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq' - liftIO $ addSubscription c rq'' + lift $ addNewQueueSubscription c rq'' tSess sessId void . enqueueMessages c cData sqs SMP.noMsgFlags $ QADD [(qUri, Just (server, sndId))] rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSendingQADD let rqs' = updatedQs rq1 rqs <> [rq''] @@ -1490,7 +1554,7 @@ abortConnectionSwitch' c connId = SomeConn _ (DuplexConnection cData rqs sqs) -> case switchingRQ rqs of Just rq | canAbortRcvSwitch rq -> do - when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED + when (ratchetSyncSendProhibited cData) $ throwE $ CMD PROHIBITED "abortConnectionSwitch: send prohibited" -- multiple queues to which the connections switches were possible when repeating switch was allowed let (delRqs, keepRqs) = L.partition ((Just (dbQId rq) ==) . dbReplaceQId) rqs case L.nonEmpty keepRqs of @@ -1503,9 +1567,9 @@ abortConnectionSwitch' c connId = conn' = DuplexConnection cData rqs'' sqs pure $ connectionStats conn' _ -> throwError $ INTERNAL "won't delete all rcv queues in connection" - | otherwise -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD PROHIBITED + | otherwise -> throwE $ CMD PROHIBITED "abortConnectionSwitch: no rcv queues left" + _ -> throwE $ CMD PROHIBITED "abortConnectionSwitch: not allowed" + _ -> throwE $ CMD PROHIBITED "abortConnectionSwitch: not duplex" synchronizeRatchet' :: AgentClient -> ConnId -> PQSupport -> Bool -> AM ConnectionStats synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchronizeRatchet" $ do @@ -1517,7 +1581,7 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni let cData' = cData {pqSupport = pqSupport'} :: ConnData AgentConfig {e2eEncryptVRange} <- asks config g <- asks random - (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion $ e2eEncryptVRange pqSupport') pqSupport' + (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqSupport' enqueueRatchetKeyMsgs c cData' sqs e2eParams withStore' c $ \db -> do setConnRatchetSync db connId RSStarted @@ -1525,13 +1589,13 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni let cData'' = cData' {ratchetSyncState = RSStarted} :: ConnData conn' = DuplexConnection cData'' rqs sqs pure $ connectionStats conn' - | otherwise -> throwError $ CMD PROHIBITED - _ -> throwError $ CMD PROHIBITED + | otherwise -> throwE $ CMD PROHIBITED "synchronizeRatchet: not allowed" + _ -> throwE $ CMD PROHIBITED "synchronizeRatchet: not duplex" ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM () ackQueueMessage c rq srvMsgId = sendAck c rq srvMsgId `catchAgentError` \case - SMP SMP.NO_MSG -> pure () + SMP _ SMP.NO_MSG -> pure () e -> throwError e -- | Suspend SMP agent connection (OFF command) in Reader monad @@ -1543,7 +1607,7 @@ suspendConnection' c connId = withConnLock c connId "suspendConnection" $ do RcvConnection _ rq -> suspendQueue c rq ContactConnection _ rq -> suspendQueue c rq SndConnection _ _ -> throwError $ CONN SIMPLEX - NewConnection _ -> throwError $ CMD PROHIBITED + NewConnection _ -> throwE $ CMD PROHIBITED "suspendConnection" -- | Delete SMP agent connection (DEL command) in Reader monad -- unlike deleteConnectionAsync, this function does not mark connection as deleted in case of deletion failure @@ -1781,7 +1845,7 @@ registerNtfToken' c suppliedDeviceToken suppliedNtfMode = withStore' c (`createNtfToken` tkn) registerToken tkn pure NTRegistered - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "createToken" registerToken :: NtfToken -> AM () registerToken tkn@NtfToken {ntfPubKey, ntfDhKeys = (pubDhKey, privDhKey)} = do (tknId, srvPubDhKey) <- agentNtfRegisterToken c tkn ntfPubKey pubDhKey @@ -1794,7 +1858,7 @@ verifyNtfToken' :: AgentClient -> DeviceToken -> C.CbNonce -> ByteString -> AM ( verifyNtfToken' c deviceToken nonce code = withStore' c getSavedNtfToken >>= \case Just tkn@NtfToken {deviceToken = savedDeviceToken, ntfTokenId = Just tknId, ntfDhSecret = Just dhSecret, ntfMode} -> do - when (deviceToken /= savedDeviceToken) . throwError $ CMD PROHIBITED + when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "verifyNtfToken: different token" code' <- liftEither . bimap cryptoError NtfRegCode $ C.cbDecrypt dhSecret nonce code toStatus <- withToken c tkn (Just (NTConfirmed, NTAVerify code')) (NTActive, Just NTACheck) $ @@ -1803,36 +1867,36 @@ verifyNtfToken' c deviceToken nonce code = cron <- asks $ ntfCron . config agentNtfEnableCron c tknId tkn cron when (ntfMode == NMInstant) $ initializeNtfSubs c - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "verifyNtfToken: no token" checkNtfToken' :: AgentClient -> DeviceToken -> AM NtfTknStatus checkNtfToken' c deviceToken = withStore' c getSavedNtfToken >>= \case Just tkn@NtfToken {deviceToken = savedDeviceToken, ntfTokenId = Just tknId} -> do - when (deviceToken /= savedDeviceToken) . throwError $ CMD PROHIBITED + when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "checkNtfToken: different token" agentNtfCheckToken c tknId tkn - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "checkNtfToken: no token" deleteNtfToken' :: AgentClient -> DeviceToken -> AM () deleteNtfToken' c deviceToken = withStore' c getSavedNtfToken >>= \case Just tkn@NtfToken {deviceToken = savedDeviceToken} -> do - when (deviceToken /= savedDeviceToken) . throwError $ CMD PROHIBITED + when (deviceToken /= savedDeviceToken) . throwE $ CMD PROHIBITED "deleteNtfToken: different token" deleteToken_ c tkn deleteNtfSubs c NSCSmpDelete - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "deleteNtfToken: no token" getNtfToken' :: AgentClient -> AM (DeviceToken, NtfTknStatus, NotificationsMode, NtfServer) getNtfToken' c = withStore' c getSavedNtfToken >>= \case Just NtfToken {deviceToken, ntfTknStatus, ntfMode, ntfServer} -> pure (deviceToken, ntfTknStatus, ntfMode, ntfServer) - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "getNtfToken" getNtfTokenData' :: AgentClient -> AM NtfToken getNtfTokenData' c = withStore' c getSavedNtfToken >>= \case Just tkn -> pure tkn - _ -> throwError $ CMD PROHIBITED + _ -> throwE $ CMD PROHIBITED "getNtfTokenData" -- | Set connection notifications, in Reader monad toggleConnectionNtfs' :: AgentClient -> ConnId -> Bool -> AM () @@ -1861,7 +1925,7 @@ deleteToken_ c tkn@NtfToken {ntfTokenId, ntfTknStatus} = do withStore' c $ \db -> updateNtfToken db tkn ntfTknStatus ntfTknAction atomically $ nsUpdateToken ns tkn {ntfTknStatus, ntfTknAction} agentNtfDeleteToken c tknId tkn `catchAgentError` \case - NTF AUTH -> pure () + NTF _ AUTH -> pure () e -> throwError e withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns @@ -1878,7 +1942,7 @@ withToken c tkn@NtfToken {deviceToken, ntfMode} from_ (toStatus, toAction_) f = let updatedToken = tkn {ntfTknStatus = toStatus, ntfTknAction = toAction_} atomically $ nsUpdateToken ns updatedToken pure toStatus - Left e@(NTF AUTH) -> do + Left e@(NTF _ AUTH) -> do withStore' c $ \db -> removeNtfToken db tkn atomically $ nsRemoveNtfToken ns void $ registerNtfToken' c deviceToken ntfMode @@ -1964,9 +2028,7 @@ subscriber :: AgentClient -> AM' () subscriber c@AgentClient {msgQ} = forever $ do t <- atomically $ readTBQueue msgQ agentOperationBracket c AORcvNetwork waitUntilActive $ - runExceptT (processSMPTransmission c t) >>= \case - Left e -> liftIO $ print e - Right _ -> return () + processSMPTransmissions c t cleanupManager :: AgentClient -> AM' () cleanupManager c@AgentClient {subQ} = do @@ -2040,19 +2102,66 @@ cleanupManager c@AgentClient {subQ} = do data ACKd = ACKd | ACKPending --- | make sure to ACK or throw in each message processing branch --- it cannot be finally, unfortunately, as sometimes it needs to be ACK+DEL -processSMPTransmission :: AgentClient -> ServerTransmission SMPVersion BrokerMsg -> AM () -processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, sessId, rId, cmd) = do - (rq, SomeConn _ conn) <- withStore c (\db -> getRcvConn db srv rId) - processSMP rq conn $ toConnData conn +-- | Make sure to ACK or throw in each message processing branch +-- It cannot be finally, as sometimes it needs to be ACK+DEL, +-- and sometimes ACK has to be sent from the consumer. +processSMPTransmissions :: AgentClient -> ServerTransmissionBatch SMPVersion ErrorType BrokerMsg -> AM' () +processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts) = do + upConnIds <- newTVarIO [] + forM_ ts $ \(entId, t) -> case t of + STEvent msgOrErr -> + withRcvConn entId $ \rq@RcvQueue {connId} conn -> case msgOrErr of + Right msg -> processSMP rq conn (toConnData conn) msg + Left e -> lift $ notifyErr connId e + STResponse (Cmd SRecipient cmd) respOrErr -> + withRcvConn entId $ \rq conn -> case cmd of + SMP.SUB -> case respOrErr of + Right SMP.OK -> processSubOk rq upConnIds + Right msg@SMP.MSG {} -> do + processSubOk rq upConnIds -- the connection is UP even when processing this particular message fails + processSMP rq conn (toConnData conn) msg + Right r -> processSubErr rq $ unexpectedResponse r + Left e -> unless (temporaryClientError e) $ processSubErr rq e -- timeout/network was already reported + SMP.ACK _ -> case respOrErr of + Right msg@SMP.MSG {} -> processSMP rq conn (toConnData conn) msg + _ -> pure () -- TODO process OK response to ACK + _ -> pure () -- TODO process expired response to DEL + STResponse {} -> pure () -- TODO process expired responses to sent messages + STUnexpectedError e -> do + logServer "<--" c srv entId $ "error: " <> bshow e + notifyErr "" e + connIds <- readTVarIO upConnIds + unless (null connIds) $ notify' "" $ UP srv connIds where - processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> AM () + withRcvConn :: SMP.RecipientId -> (forall c. RcvQueue -> Connection c -> AM ()) -> AM' () + withRcvConn rId a = do + tryAgentError' (withStore c $ \db -> getRcvConn db srv rId) >>= \case + Left e -> notify' "" (ERR e) + Right (rq@RcvQueue {connId}, SomeConn _ conn) -> + tryAgentError' (a rq conn) >>= \case + Left e -> notify' connId (ERR e) + Right () -> pure () + processSubOk :: RcvQueue -> TVar [ConnId] -> AM () + processSubOk rq@RcvQueue {connId} upConnIds = + atomically . whenM (isPendingSub connId) $ do + addSubscription c rq + modifyTVar' upConnIds (connId :) + processSubErr :: RcvQueue -> SMPClientError -> AM () + processSubErr rq@RcvQueue {connId} e = do + atomically . whenM (isPendingSub connId) $ failSubscription c rq e + lift $ notifyErr connId e + isPendingSub connId = (&&) <$> hasPendingSubscription c connId <*> activeClientSession c tSess sessId + notify' :: forall e m. (AEntityI e, MonadIO m) => ConnId -> ACommand 'Agent e -> m () + notify' connId msg = atomically $ writeTBQueue subQ ("", connId, APC (sAEntity @e) msg) + notifyErr :: ConnId -> SMPClientError -> AM' () + notifyErr connId = notify' connId . ERR . protocolClientError SMP (B.unpack $ strEncode srv) + processSMP :: forall c. RcvQueue -> Connection c -> ConnData -> BrokerMsg -> AM () processSMP - rq@RcvQueue {e2ePrivKey, e2eDhSecret, status} + rq@RcvQueue {rcvId = rId, e2ePrivKey, e2eDhSecret, status} conn - cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} = - withConnLock c connId "processSMP" $ case cmd of + cData@ConnData {userId, connId, connAgentVersion, ratchetSyncState = rss} + smpMsg = + withConnLock c connId "processSMP" $ case smpMsg of SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} -> void . handleNotifyAck $ do msg' <- decryptSMPMessage rq msg @@ -2079,7 +2188,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, smpConfirmation srvMsgId conn senderKey e2ePubKey e2eEncryption_ encConnInfo phVer agentVersion >> ack (SMP.PHEmpty, AgentInvitation {connReq, connInfo}) -> smpInvitation srvMsgId conn connReq connInfo >> ack - _ -> prohibited >> ack + _ -> prohibited "handshake: incorrect state" >> ack (Just e2eDh, Nothing) -> do decryptClientMessage e2eDh clientMsg >>= \case (SMP.PHEmpty, AgentRatchetKey {agentVersion, e2eEncryption}) -> do @@ -2102,7 +2211,8 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, _ -> pure () let encryptedMsgHash = C.sha256Hash encAgentMessage g <- asks random - tryError (agentClientMsg g encryptedMsgHash) >>= \case + atomically updateTotalMsgCount + tryAgentError (agentClientMsg g encryptedMsgHash) >>= \case Right (Just (msgId, msgMeta, aMessage, rcPrev)) -> do conn'' <- resetRatchetSync case aMessage of @@ -2133,12 +2243,13 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, withStore' c $ \db -> setConnRatchetSync db connId RSOk pure conn'' | otherwise = pure conn' - Right _ -> prohibited >> ack + Right Nothing -> prohibited "msg: bad agent msg" >> ack Left e@(AGENT A_DUPLICATE) -> do + atomically updateDupMsgCount withStore' c (\db -> getLastMsg db connId srvMsgId) >>= \case Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck} | userAck -> ackDel internalId - | otherwise -> do + | otherwise -> liftEither (parse smpP (AGENT A_MESSAGE) agentMsgBody) >>= \case AgentMessage _ (A_MSG body) -> do logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId @@ -2165,6 +2276,20 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, checkDuplicateHash e encryptedMsgHash = unlessM (withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash) $ throwError e + updateTotalMsgCount :: STM () + updateTotalMsgCount = + TM.lookup connId (msgCounts c) >>= \case + Just v -> modifyTVar' v $ first (+ 1) + Nothing -> addMsgCount 0 + updateDupMsgCount :: STM () + updateDupMsgCount = + TM.lookup connId (msgCounts c) >>= \case + Just v -> modifyTVar' v $ second (+ 1) + Nothing -> addMsgCount 1 + addMsgCount :: Int -> STM () + addMsgCount duplicate = do + counts <- newTVar (1, duplicate) + TM.insert connId counts (msgCounts c) agentClientMsg :: TVar ChaChaDRG -> ByteString -> AM (Maybe (InternalId, MsgMeta, AMessage, CR.RatchetX448)) agentClientMsg g encryptedMsgHash = withStore c $ \db -> runExceptT $ do rc <- ExceptT $ getRatchet db connId -- ratchet state pre-decryption - required for processing EREADY @@ -2183,11 +2308,11 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, liftIO $ createRcvMsg db connId rq rcvMsg pure $ Just (internalId, msgMeta, aMessage, rc) _ -> pure Nothing - _ -> prohibited >> ack - _ -> prohibited >> ack + _ -> prohibited "msg: bad client msg" >> ack + _ -> prohibited "msg: no keys" >> ack updateConnVersion :: Connection c -> ConnData -> VersionSMPA -> AM (Connection c) - updateConnVersion conn' cData'@ConnData {pqSupport} msgAgentVersion = do - aVRange <- asks $ ($ pqSupport) . smpAgentVRange . config + updateConnVersion conn' cData' msgAgentVersion = do + aVRange <- asks $ smpAgentVRange . config let msgAVRange = fromMaybe (versionToRange msgAgentVersion) $ safeVersionRange (minVersion aVRange) msgAgentVersion case msgAVRange `compatibleVersion` aVRange of Just (Compatible av) @@ -2204,34 +2329,35 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, handleNotifyAck :: AM ACKd -> AM ACKd handleNotifyAck m = m `catchAgentError` \e -> notify (ERR e) >> ack SMP.END -> - atomically (TM.lookup tSess smpClients $>>= (tryReadTMVar . sessionVar) >>= processEND) - >>= logServer "<--" c srv rId + atomically (TM.lookup tSess (smpClients c) $>>= (tryReadTMVar . sessionVar) >>= processEND) + >>= notifyEnd where processEND = \case Just (Right clnt) - | sessId == sessionId (thParams clnt) -> do - removeSubscription c connId - notify' END - pure "END" - | otherwise -> ignored - _ -> ignored - ignored = pure "END from disconnected client - ignored" - _ -> do - logServer "<--" c srv rId $ "unexpected: " <> bshow cmd - notify . ERR $ BROKER (B.unpack $ strEncode srv) UNEXPECTED + | sessId == sessionId (thParams $ connectedClient clnt) -> + removeSubscription c connId $> True + _ -> pure False + notifyEnd removed + | removed = notify END >> logServer "<--" c srv rId "END" + | otherwise = logServer "<--" c srv rId "END from disconnected client - ignored" + SMP.ERR e -> notify $ ERR $ SMP (B.unpack $ strEncode srv) e + r -> unexpected r where - notify :: forall e m. MonadIO m => AEntityI e => ACommand 'Agent e -> m () - notify = atomically . notify' + notify :: forall e m. (AEntityI e, MonadIO m) => ACommand 'Agent e -> m () + notify = notify' connId - notify' :: forall e. AEntityI e => ACommand 'Agent e -> STM () - notify' msg = writeTBQueue subQ ("", connId, APC (sAEntity @e) msg) - - prohibited :: AM () - prohibited = notify . ERR $ AGENT A_PROHIBITED + prohibited :: String -> AM () + prohibited = notify . ERR . AGENT . A_PROHIBITED enqueueCmd :: InternalCommand -> AM () enqueueCmd = enqueueCommand c "" connId (Just srv) . AInternalCommand + unexpected :: BrokerMsg -> AM () + unexpected r = do + logServer "<--" c srv rId $ "unexpected: " <> bshow r + -- TODO add extended information about transmission type once UNEXPECTED has string + notify . ERR $ BROKER (B.unpack $ strEncode srv) $ UNEXPECTED (take 32 $ show r) + decryptClientMessage :: C.DhSecretX25519 -> SMP.ClientMsgEnvelope -> AM (SMP.PrivHeader, AgentMsgEnvelope) decryptClientMessage e2eDh SMP.ClientMsgEnvelope {cmNonce, cmEncBody} = do clientMsg <- agentCbDecrypt e2eDh cmNonce cmEncBody @@ -2253,19 +2379,17 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId AgentConfig {smpClientVRange, smpAgentVRange, e2eEncryptVRange} <- asks config let ConnData {pqSupport} = toConnData conn' - aVRange = smpAgentVRange pqSupport - e2eVRange = e2eEncryptVRange pqSupport unless - (agentVersion `isCompatible` aVRange && smpClientVersion `isCompatible` smpClientVRange) + (agentVersion `isCompatible` smpAgentVRange && smpClientVersion `isCompatible` smpClientVRange) (throwError $ AGENT A_VERSION) case status of New -> case (conn', e2eEncryption) of -- party initiating connection (RcvConnection _ _, Just (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _))) -> do - unless (e2eVersion `isCompatible` e2eVRange) (throwError $ AGENT A_VERSION) + unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwError $ AGENT A_VERSION) (pk1, rcDHRs, pKem) <- withStore c (`getRatchetX3dhKeys` connId) rcParams <- liftError cryptoError $ CR.pqX3dhRcv pk1 rcDHRs pKem e2eSndParams - let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eVRange} + let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eEncryptVRange} pqSupport' = pqSupport `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just e2eVersion) rc = CR.initRcvRatchet rcVs rcDHRs rcParams pqSupport' g <- asks random @@ -2275,7 +2399,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, parseMessage agentMsgBody >>= \case AgentConnInfoReply smpQueues connInfo -> processConf connInfo SMPConfirmation {senderKey, e2ePubKey, connInfo, smpReplyQueues = L.toList smpQueues, smpClientVersion} - _ -> prohibited -- including AgentConnInfo, that is prohibited here in v2 + _ -> prohibited "conf: not AgentConnInfoReply" -- including AgentConnInfo, that is prohibited here in v2 where processConf connInfo senderConf = do let newConfirmation = NewConfirmation {connId, senderConf, ratchetState = rc'} @@ -2285,7 +2409,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, createConfirmation db g newConfirmation let srvs = map qServer $ smpReplyQueues senderConf notify $ CONF confId pqSupport' srvs connInfo - _ -> prohibited + _ -> prohibited "conf: decrypt error or skipped" -- party accepting connection (DuplexConnection _ (RcvQueue {smpClientVersion = v'} :| _) _, Nothing) -> do g <- asks random @@ -2295,15 +2419,15 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, let dhSecret = C.dh' e2ePubKey e2ePrivKey withStore' c $ \db -> setRcvQueueConfirmedE2E db rq dhSecret $ min v' smpClientVersion enqueueCmd $ ICDuplexSecure rId senderKey - _ -> prohibited - _ -> prohibited - _ -> prohibited + _ -> prohibited "conf: not AgentConnInfo" + _ -> prohibited "conf: incorrect state" + _ -> prohibited "conf: status /= new" helloMsg :: SMP.MsgId -> MsgMeta -> Connection c -> AM () helloMsg srvMsgId MsgMeta {pqEncryption} conn' = do logServer "<--" c srv rId $ "MSG :" <> logSecret srvMsgId case status of - Active -> prohibited + Active -> prohibited "hello: active" _ -> case conn' of DuplexConnection _ _ (sq@SndQueue {status = sndStatus} :| _) @@ -2345,7 +2469,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, let sndMsgId = InternalSndId agentMsgId SndMsg {internalId = InternalId msgId, msgType, internalHash, msgReceipt} <- withStore c $ \db -> getSndMsgViaRcpt db connId sndMsgId if msgType /= AM_A_MSG_ - then notify (ERR $ AGENT A_PROHIBITED) $> Nothing -- unexpected message type for receipt + then prohibited "receipt: not a msg" $> Nothing else case msgReceipt of Just MsgReceipt {msgRcptStatus = MROk} -> pure Nothing -- already notified with MROk status _ -> do @@ -2447,13 +2571,13 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, ContactConnection {} -> do -- show connection request even if invitaion via contact address is not compatible. -- in case invitation not compatible, assume there is no PQ encryption support. - pqSupport <- lift $ maybe PQSupportOff pqSupported <$> compatibleInvitationUri connReq PQSupportOn + pqSupport <- lift $ maybe PQSupportOff pqSupported <$> compatibleInvitationUri connReq g <- asks random let newInv = NewInvitation {contactConnId = connId, connReq, recipientConnInfo = cInfo} invId <- withStore c $ \db -> createInvitation db g newInv let srvs = L.map qServer $ crSmpQueues crData notify $ REQ invId pqSupport srvs cInfo - _ -> prohibited + _ -> prohibited "inv: sent to message conn" where pqSupported (_, Compatible (CR.E2ERatchetParams v _ _ _), Compatible agentVersion) = PQSupportOn `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just v) @@ -2467,10 +2591,9 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), _v, newRatchetKey e2eOtherPartyParams@(CR.E2ERatchetParams e2eVersion k1Rcv k2Rcv _) conn'@(DuplexConnection cData'@ConnData {lastExternalSndId, pqSupport} _ sqs) = unlessM ratchetExists $ do AgentConfig {e2eEncryptVRange} <- asks config - let connE2EVRange = e2eEncryptVRange pqSupport - unless (e2eVersion `isCompatible` connE2EVRange) (throwError $ AGENT A_VERSION) + unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwError $ AGENT A_VERSION) keys <- getSendRatchetKeys - let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion connE2EVRange} + let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eEncryptVRange} initRatchet rcVs keys notifyAgreed where @@ -2568,21 +2691,20 @@ confirmQueueAsync c cData sq srv connInfo e2eEncryption_ subMode = do storeConfirmation c cData sq e2eEncryption_ =<< mkAgentConfirmation c cData sq srv connInfo subMode lift $ submitPendingMsg c cData sq -confirmQueue :: Compatible VersionSMPA -> AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () -confirmQueue (Compatible agentVersion) c cData@ConnData {connId, pqSupport} sq srv connInfo e2eEncryption_ subMode = do +confirmQueue :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> SubscriptionMode -> AM () +confirmQueue c cData@ConnData {connId, connAgentVersion, pqSupport} sq srv connInfo e2eEncryption_ subMode = do msg <- mkConfirmation =<< mkAgentConfirmation c cData sq srv connInfo subMode - sendConfirmation c sq msg + void $ sendConfirmation c sq msg withStore' c $ \db -> setSndQueueStatus db sq Confirmed where mkConfirmation :: AgentMessage -> AM MsgBody mkConfirmation aMessage = do - -- the version to be used when PQSupport is disabled - currentE2EVersion <- asks $ maxVersion . ($ PQSupportOff) . e2eEncryptVRange . config + currentE2EVersion <- asks $ maxVersion . e2eEncryptVRange . config withStore c $ \db -> runExceptT $ do void . liftIO $ updateSndIds db connId let pqEnc = CR.pqSupportToEnc pqSupport (encConnInfo, _) <- agentRatchetEncrypt db cData (smpEncode aMessage) e2eEncConnInfoLength (Just pqEnc) currentE2EVersion - pure . smpEncode $ AgentConfirmation {agentVersion, e2eEncryption_, encConnInfo} + pure . smpEncode $ AgentConfirmation {agentVersion = connAgentVersion, e2eEncryption_, encConnInfo} mkAgentConfirmation :: AgentClient -> ConnData -> SndQueue -> SMPServerWithAuth -> ConnInfo -> SubscriptionMode -> AM AgentMessage mkAgentConfirmation c cData sq srv connInfo subMode = do @@ -2596,8 +2718,7 @@ enqueueConfirmation c cData sq connInfo e2eEncryption_ = do storeConfirmation :: AgentClient -> ConnData -> SndQueue -> Maybe (CR.SndE2ERatchetParams 'C.X448) -> AgentMessage -> AM () storeConfirmation c cData@ConnData {connId, pqSupport, connAgentVersion = v} sq e2eEncryption_ agentMsg = do - -- the version to be used when PQSupport is disabled - currentE2EVersion <- asks $ maxVersion . ($ PQSupportOff) . e2eEncryptVRange . config + currentE2EVersion <- asks $ maxVersion . e2eEncryptVRange . config withStore c $ \db -> runExceptT $ do internalTs <- liftIO getCurrentTime (internalId, internalSndId, prevMsgHash) <- liftIO $ updateSndIds db connId @@ -2617,8 +2738,8 @@ enqueueRatchetKeyMsgs c cData (sq :| sqs) e2eEncryption = do mapM_ (lift . enqueueSavedMessage c cData msgId) $ filter isActiveSndQ sqs enqueueRatchetKey :: AgentClient -> ConnData -> SndQueue -> CR.RcvE2ERatchetParams 'C.X448 -> AM AgentMsgId -enqueueRatchetKey c cData@ConnData {connId, pqSupport} sq e2eEncryption = do - aVRange <- asks $ ($ pqSupport) . smpAgentVRange . config +enqueueRatchetKey c cData@ConnData {connId} sq e2eEncryption = do + aVRange <- asks $ smpAgentVRange . config msgId <- storeRatchetKey $ maxVersion aVRange lift $ submitPendingMsg c cData sq pure $ unId msgId diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 3091fba87..e18888244 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -28,6 +28,7 @@ module Simplex.Messaging.Agent.Client withConnLocks, withInvLock, withLockMap, + ipAddressProtected, closeAgentClient, closeProtocolServerClients, reconnectServerClients, @@ -41,17 +42,21 @@ module Simplex.Messaging.Agent.Client getQueueMessage, decryptSMPMessage, addSubscription, + failSubscription, + addNewQueueSubscription, getSubscriptions, sendConfirmation, sendInvitation, temporaryAgentError, temporaryOrHostError, + serverHostError, secureQueue, enableQueueNotifications, enableQueuesNtfs, disableQueueNotifications, disableQueuesNtfs, sendAgentMessage, + getQueueInfo, agentNtfRegisterToken, agentNtfVerifyToken, agentNtfCheckToken, @@ -77,11 +82,14 @@ module Simplex.Messaging.Agent.Client logSecret, removeSubscription, hasActiveSubscription, + hasPendingSubscription, hasGetLock, + activeClientSession, agentClientStore, agentDRG, getAgentSubscriptions, slowNetworkConfig, + protocolClientError, Worker (..), SessionVar (..), SubscriptionsInfo (..), @@ -103,9 +111,10 @@ module Simplex.Messaging.Agent.Client waitUntilActive, UserNetworkInfo (..), UserNetworkType (..), - UserNetworkState (..), - UNSOffline (..), + getNetworkConfig', waitForUserNetwork, + isNetworkOnline, + isOnline, throwWhenInactive, throwWhenNoDelivery, beginAgentOperation, @@ -129,9 +138,13 @@ module Simplex.Messaging.Agent.Client getAgentWorkersDetails, AgentWorkersSummary (..), getAgentWorkersSummary, + AgentQueuesInfo (..), + getAgentQueuesInfo, SMPTransportSession, NtfTransportSession, XFTPTransportSession, + ProxiedRelay (..), + SMPConnectedClient (..), ) where @@ -145,6 +158,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson as J import qualified Data.Aeson.TH as J @@ -152,10 +166,8 @@ import Data.Bifunctor (bimap, first, second) import Data.ByteString.Base64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import Data.Composition ((.:.)) -import Data.Either (lefts, partitionEithers) +import Data.Either (partitionEithers) import Data.Functor (($>)) -import Data.Int (Int64) import Data.List (deleteFirstsBy, foldl', partition, (\\)) import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L @@ -166,15 +178,17 @@ import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import Data.Text.Encoding -import Data.Time (UTCTime, defaultTimeLocale, diffUTCTime, formatTime, getCurrentTime) +import Data.Time (UTCTime, addUTCTime, defaultTimeLocale, formatTime, getCurrentTime) import Data.Time.Clock.System (getSystemTime) import Data.Word (Word16) +import qualified Database.SQLite.Simple as SQL import Network.Socket (HostName) import Simplex.FileTransfer.Client (XFTPChunkSpec (..), XFTPClient, XFTPClientConfig (..), XFTPClientError) import qualified Simplex.FileTransfer.Client as X import Simplex.FileTransfer.Description (ChunkReplicaId (..), FileDigest (..), kb) import Simplex.FileTransfer.Protocol (FileInfo (..), FileResponse) import Simplex.FileTransfer.Transport (XFTPErrorType (DIGEST), XFTPRcvChunkSpec (..), XFTPVersion) +import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (DeletedSndChunkReplica (..), NewSndChunkReplica (..), RcvFileChunkReplica (..), SndFileChunk (..), SndFileChunkReplica (..)) import Simplex.FileTransfer.Util (uniqueCombine) import Simplex.Messaging.Agent.Env.SQLite @@ -187,7 +201,6 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Agent.TRcvQueues (TRcvQueues (getRcvQueues)) import qualified Simplex.Messaging.Agent.TRcvQueues as RQ import Simplex.Messaging.Client -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -195,7 +208,7 @@ import Simplex.Messaging.Notifications.Client import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Transport (NTFVersion) import Simplex.Messaging.Notifications.Types -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse, sumTypeJSON) import Simplex.Messaging.Protocol ( AProtocolType (..), BrokerMsg, @@ -227,10 +240,12 @@ import Simplex.Messaging.Protocol sameSrvAddr', ) import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Server.QueueStore.QueueInfo +import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (SMPVersion) -import Simplex.Messaging.Transport.Client (TransportHost) +import Simplex.Messaging.Transport (SMPVersion, SessionId, THandleParams (sessionId), TransportError (..)) +import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version import System.Mem.Weak (Weak) @@ -241,12 +256,7 @@ import UnliftIO.Directory (doesFileExist, getTemporaryDirectory, removeFile) import qualified UnliftIO.Exception as E import UnliftIO.STM -data SessionVar a = SessionVar - { sessionVar :: TMVar a, - sessionVarId :: Int - } - -type ClientVar msg = SessionVar (Either AgentErrorType (Client msg)) +type ClientVar msg = SessionVar (Either (AgentErrorType, Maybe UTCTime) (Client msg)) type SMPClientVar = ClientVar SMP.BrokerMsg @@ -265,15 +275,20 @@ data AgentClient = AgentClient active :: TVar Bool, rcvQ :: TBQueue (ATransmission 'Client), subQ :: TBQueue (ATransmission 'Agent), - msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg), + msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg), smpServers :: TMap UserId (NonEmpty SMPServerWithAuth), smpClients :: TMap SMPTransportSession SMPClientVar, + -- smpProxiedRelays: + -- SMPTransportSession defines connection from proxy to relay, + -- SMPServerWithAuth defines client connected to SMP proxy (with the same userId and entityId in TransportSession) + smpProxiedRelays :: TMap SMPTransportSession SMPServerWithAuth, ntfServers :: TVar [NtfServer], ntfClients :: TMap NtfTransportSession NtfClientVar, xftpServers :: TMap UserId (NonEmpty XFTPServerWithAuth), xftpClients :: TMap XFTPTransportSession XFTPClientVar, useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), -- (slow, fast) networks - userNetworkState :: TVar UserNetworkState, + userNetworkInfo :: TVar UserNetworkInfo, + userNetworkUpdated :: TVar (Maybe UTCTime), subscrConns :: TVar (Set ConnId), activeSubs :: TRcvQueues, pendingSubs :: TRcvQueues, @@ -298,10 +313,18 @@ data AgentClient = AgentClient -- smpSubWorkers for SMP servers sessions smpSubWorkers :: TMap SMPTransportSession (SessionVar (Async ())), agentStats :: TMap AgentStatsKey (TVar Int), + msgCounts :: TMap ConnId (TVar (Int, Int)), -- (total, duplicates) clientId :: Int, agentEnv :: Env } +data SMPConnectedClient = SMPConnectedClient + { connectedClient :: SMPClient, + proxiedRelays :: TMap SMPServer ProxiedRelayVar + } + +type ProxiedRelayVar = SessionVar (Either AgentErrorType ProxiedRelay) + getAgentWorker :: (Ord k, Show k) => String -> Bool -> AgentClient -> k -> TMap k Worker -> (Worker -> AM ()) -> AM' Worker getAgentWorker = getAgentWorker' id pure {-# INLINE getAgentWorker #-} @@ -405,26 +428,25 @@ data AgentStatsKey = AgentStatsKey deriving (Eq, Ord, Show) data UserNetworkInfo = UserNetworkInfo - { networkType :: UserNetworkType + { networkType :: UserNetworkType, + online :: Bool } deriving (Show) +isNetworkOnline :: AgentClient -> STM Bool +isNetworkOnline c = isOnline <$> readTVar (userNetworkInfo c) + +isOnline :: UserNetworkInfo -> Bool +isOnline UserNetworkInfo {networkType, online} = networkType /= UNNone && online + data UserNetworkType = UNNone | UNCellular | UNWifi | UNEthernet | UNOther deriving (Eq, Show) -data UserNetworkState = UserNetworkState - { networkType :: UserNetworkType, - offline :: Maybe UNSOffline - } - deriving (Show) - -data UNSOffline = UNSOffline {offlineDelay :: Int64, offlineFrom :: UTCTime} - deriving (Show) - -- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's. newAgentClient :: Int -> InitialAgentServers -> Env -> STM AgentClient newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = do - let qSize = tbqSize $ config agentEnv + let cfg = config agentEnv + qSize = tbqSize cfg acThread <- newTVar Nothing active <- newTVar True rcvQ <- newTBQueue qSize @@ -432,12 +454,14 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = msgQ <- newTBQueue qSize smpServers <- newTVar smp smpClients <- TM.empty + smpProxiedRelays <- TM.empty ntfServers <- newTVar ntf ntfClients <- TM.empty xftpServers <- newTVar xftp xftpClients <- TM.empty useNetworkConfig <- newTVar (slowNetworkConfig netCfg, netCfg) - userNetworkState <- newTVar $ UserNetworkState UNOther Nothing + userNetworkInfo <- newTVar $ UserNetworkInfo UNOther True + userNetworkUpdated <- newTVar Nothing subscrConns <- newTVar S.empty activeSubs <- RQ.empty pendingSubs <- RQ.empty @@ -458,6 +482,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = deleteLock <- createLock smpSubWorkers <- TM.empty agentStats <- TM.empty + msgCounts <- TM.empty return AgentClient { acThread, @@ -467,12 +492,14 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = msgQ, smpServers, smpClients, + smpProxiedRelays, ntfServers, ntfClients, xftpServers, xftpClients, useNetworkConfig, - userNetworkState, + userNetworkInfo, + userNetworkUpdated, subscrConns, activeSubs, pendingSubs, @@ -493,6 +520,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = deleteLock, smpSubWorkers, agentStats, + msgCounts, clientId, agentEnv } @@ -515,15 +543,19 @@ agentDRG AgentClient {agentEnv = Env {random}} = random class (Encoding err, Show err) => ProtocolServerClient v err msg | msg -> v, msg -> err where type Client msg = c | c -> msg getProtocolServerClient :: AgentClient -> TransportSession msg -> AM (Client msg) - clientProtocolError :: err -> AgentErrorType - closeProtocolServerClient :: Client msg -> IO () - clientServer :: Client msg -> String - clientTransportHost :: Client msg -> TransportHost - clientSessionTs :: Client msg -> UTCTime + type ProtoClient msg = c | c -> msg + protocolClient :: Client msg -> ProtoClient msg + clientProtocolError :: HostName -> err -> AgentErrorType + closeProtocolServerClient :: ProtoClient msg -> IO () + clientServer :: ProtoClient msg -> String + clientTransportHost :: ProtoClient msg -> TransportHost + clientSessionTs :: ProtoClient msg -> UTCTime instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where - type Client BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg + type Client BrokerMsg = SMPConnectedClient getProtocolServerClient = getSMPServerClient + type ProtoClient BrokerMsg = ProtocolClient SMPVersion ErrorType BrokerMsg + protocolClient = connectedClient clientProtocolError = SMP closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer @@ -533,6 +565,8 @@ instance ProtocolServerClient SMPVersion ErrorType BrokerMsg where instance ProtocolServerClient NTFVersion ErrorType NtfResponse where type Client NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse getProtocolServerClient = getNtfServerClient + type ProtoClient NtfResponse = ProtocolClient NTFVersion ErrorType NtfResponse + protocolClient = id clientProtocolError = NTF closeProtocolServerClient = closeProtocolClient clientServer = protocolClientServer @@ -542,82 +576,145 @@ instance ProtocolServerClient NTFVersion ErrorType NtfResponse where instance ProtocolServerClient XFTPVersion XFTPErrorType FileResponse where type Client FileResponse = XFTPClient getProtocolServerClient = getXFTPServerClient + type ProtoClient FileResponse = XFTPClient + protocolClient = id clientProtocolError = XFTP closeProtocolServerClient = X.closeXFTPClient clientServer = X.xftpClientServer clientTransportHost = X.xftpTransportHost clientSessionTs = X.xftpSessionTs -getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPClient -getSMPServerClient c@AgentClient {active, smpClients, msgQ} tSess@(userId, srv, _) = do +getSMPServerClient :: AgentClient -> SMPTransportSession -> AM SMPConnectedClient +getSMPServerClient c@AgentClient {active, smpClients, workerSeq} tSess = do unlessM (readTVarIO active) . throwError $ INACTIVE - atomically (getTSessVar c tSess smpClients) - >>= either newClient (waitForProtocolClient c tSess) + ts <- liftIO getCurrentTime + atomically (getSessVar workerSeq tSess smpClients ts) + >>= either newClient (waitForProtocolClient c tSess smpClients) where - -- we resubscribe only on newClient error, but not on waitForProtocolClient error, - -- as the large number of delivery workers waiting for the client TMVar - -- make it expensive to check for pending subscriptions. - newClient v = - newProtocolClient c tSess smpClients connectClient v - `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e - connectClient :: SMPClientVar -> AM SMPClient - connectClient v = do + newClient v = do + prs <- atomically TM.empty + smpConnectClient c tSess prs v + +getSMPProxyClient :: AgentClient -> SMPTransportSession -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) +getSMPProxyClient c@AgentClient {active, smpClients, smpProxiedRelays, workerSeq} destSess@(userId, destSrv, qId) = do + unlessM (readTVarIO active) . throwError $ INACTIVE + proxySrv <- getNextServer c userId [destSrv] + ts <- liftIO getCurrentTime + atomically (getClientVar proxySrv ts) >>= \(tSess, auth, v) -> + either (newProxyClient tSess auth ts) (waitForProxyClient tSess auth) v + where + getClientVar :: SMPServerWithAuth -> UTCTime -> STM (SMPTransportSession, Maybe SMP.BasicAuth, Either SMPClientVar SMPClientVar) + getClientVar proxySrv ts = do + ProtoServerWithAuth srv auth <- TM.lookup destSess smpProxiedRelays >>= maybe (TM.insert destSess proxySrv smpProxiedRelays $> proxySrv) pure + let tSess = (userId, srv, qId) + (tSess,auth,) <$> getSessVar workerSeq tSess smpClients ts + newProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> UTCTime -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) + newProxyClient tSess auth ts v = do + (prs, rv) <- atomically $ do + prs <- TM.empty + -- we do not need to check if it is a new proxied relay session, + -- as the client is just created and there are no sessions yet + (prs,) . either id id <$> getSessVar workerSeq destSrv prs ts + clnt <- smpConnectClient c tSess prs v + (clnt,) <$> newProxiedRelay clnt auth rv + waitForProxyClient :: SMPTransportSession -> Maybe SMP.BasicAuth -> SMPClientVar -> AM (SMPConnectedClient, Either AgentErrorType ProxiedRelay) + waitForProxyClient tSess auth v = do + clnt@(SMPConnectedClient _ prs) <- waitForProtocolClient c tSess smpClients v + ts <- liftIO getCurrentTime + sess <- + atomically (getSessVar workerSeq destSrv prs ts) + >>= either (newProxiedRelay clnt auth) (waitForProxiedRelay tSess) + pure (clnt, sess) + newProxiedRelay :: SMPConnectedClient -> Maybe SMP.BasicAuth -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) + newProxiedRelay clnt@(SMPConnectedClient smp prs) proxyAuth rv = + tryAgentError (liftClient SMP (clientServer smp) $ connectSMPProxiedRelay smp destSrv proxyAuth) >>= \case + Right sess -> do + atomically $ putTMVar (sessionVar rv) (Right sess) + liftIO $ incClientStat c userId clnt "PROXY" "OK" + pure $ Right sess + Left e -> do + liftIO $ incClientStat c userId clnt "PROXY" $ strEncode e + atomically $ do + unless (serverHostError e) $ do + removeSessVar rv destSrv prs + TM.delete destSess smpProxiedRelays + putTMVar (sessionVar rv) (Left e) + pure $ Left e + waitForProxiedRelay :: SMPTransportSession -> ProxiedRelayVar -> AM (Either AgentErrorType ProxiedRelay) + waitForProxiedRelay (_, srv, _) rv = do + NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c + sess_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar rv) + pure $ case sess_ of + Just (Right sess) -> Right sess + Just (Left e) -> Left e + Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT + +smpConnectClient :: AgentClient -> SMPTransportSession -> TMap SMPServer ProxiedRelayVar -> SMPClientVar -> AM SMPConnectedClient +smpConnectClient c@AgentClient {smpClients, msgQ} tSess@(_, srv, _) prs v = + newProtocolClient c tSess smpClients connectClient v + `catchAgentError` \e -> lift (resubscribeSMPSession c tSess) >> throwError e + where + connectClient :: SMPClientVar -> AM SMPConnectedClient + connectClient v' = do cfg <- lift $ getClientConfig c smpCfg g <- asks random env <- ask - liftError' (protocolClientError SMP $ B.unpack $ strEncode srv) $ - getProtocolClient g tSess cfg (Just msgQ) $ - clientDisconnected env v + liftError (protocolClientError SMP $ B.unpack $ strEncode srv) $ do + smp <- ExceptT $ getProtocolClient g tSess cfg (Just msgQ) $ smpClientDisconnected c tSess env v' prs + pure SMPConnectedClient {connectedClient = smp, proxiedRelays = prs} - clientDisconnected :: Env -> SMPClientVar -> SMPClient -> IO () - clientDisconnected env v client = do - removeClientAndSubs >>= serverDown - logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv +smpClientDisconnected :: AgentClient -> SMPTransportSession -> Env -> SMPClientVar -> TMap SMPServer ProxiedRelayVar -> SMPClient -> IO () +smpClientDisconnected c@AgentClient {active, smpClients, smpProxiedRelays} tSess@(userId, srv, qId) env v prs client = do + removeClientAndSubs >>= serverDown + logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv + where + -- we make active subscriptions pending only if the client for tSess was current (in the map) and active, + -- because we can have a race condition when a new current client could have already + -- made subscriptions active, and the old client would be processing diconnection later. + removeClientAndSubs :: IO ([RcvQueue], [ConnId]) + removeClientAndSubs = atomically $ ifM currentActiveClient removeSubs $ pure ([], []) where - -- we make active subscriptions pending only if the client for tSess was current (in the map) and active, - -- because we can have a race condition when a new current client could have already - -- made subscriptions active, and the old client would be processing diconnection later. - removeClientAndSubs :: IO ([RcvQueue], [ConnId]) - removeClientAndSubs = atomically $ ifM currentActiveClient removeSubs $ pure ([], []) - where - currentActiveClient = (&&) <$> removeTSessVar' v tSess smpClients <*> readTVar active - removeSubs = do - (qs, cs) <- RQ.getDelSessQueues tSess $ activeSubs c - RQ.batchAddQueues (pendingSubs c) qs - pure (qs, cs) + currentActiveClient = (&&) <$> removeSessVar' v tSess smpClients <*> readTVar active + removeSubs = do + (qs, cs) <- RQ.getDelSessQueues tSess $ activeSubs c + RQ.batchAddQueues (pendingSubs c) qs + -- this removes proxied relays that this client created sessions to + destSrvs <- M.keys <$> readTVar prs + forM_ destSrvs $ \destSrv -> TM.delete (userId, destSrv, qId) smpProxiedRelays + pure (qs, cs) - serverDown :: ([RcvQueue], [ConnId]) -> IO () - serverDown (qs, conns) = whenM (readTVarIO active) $ do - incClientStat c userId client "DISCONNECT" "" - notifySub "" $ hostEvent DISCONNECT client - unless (null conns) $ notifySub "" $ DOWN srv conns - unless (null qs) $ do - atomically $ mapM_ (releaseGetLock c) qs - runReaderT (resubscribeSMPSession c tSess) env + serverDown :: ([RcvQueue], [ConnId]) -> IO () + serverDown (qs, conns) = whenM (readTVarIO active) $ do + incClientStat' c userId client "DISCONNECT" "" + notifySub "" $ hostEvent' DISCONNECT client + unless (null conns) $ notifySub "" $ DOWN srv conns + unless (null qs) $ do + atomically $ mapM_ (releaseGetLock c) qs + runReaderT (resubscribeSMPSession c tSess) env - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () - notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) + notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () + notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) resubscribeSMPSession :: AgentClient -> SMPTransportSession -> AM' () -resubscribeSMPSession c@AgentClient {smpSubWorkers} tSess = - atomically getWorkerVar >>= mapM_ (either newSubWorker (\_ -> pure ())) +resubscribeSMPSession c@AgentClient {smpSubWorkers, workerSeq} tSess = do + ts <- liftIO getCurrentTime + atomically (getWorkerVar ts) >>= mapM_ (either newSubWorker (\_ -> pure ())) where - getWorkerVar = + getWorkerVar ts = ifM (null <$> getPending) (pure Nothing) -- prevent race with cleanup and adding pending queues in another call - (Just <$> getTSessVar c tSess smpSubWorkers) + (Just <$> getSessVar workerSeq tSess smpSubWorkers ts) newSubWorker v = do a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v) atomically $ putTMVar (sessionVar v) a runSubWorker = do ri <- asks $ reconnectInterval . config - timeoutCounts <- newTVarIO 0 withRetryInterval ri $ \_ loop -> do pending <- atomically getPending forM_ (L.nonEmpty pending) $ \qs -> do - waitForUserNetwork c - void . tryAgentError' $ reconnectSMPClient timeoutCounts c tSess qs + liftIO $ waitForUserNetwork c + reconnectSMPClient c tSess qs loop getPending = RQ.getSessQueues tSess $ pendingSubs c cleanup :: SessionVar (Async ()) -> STM () @@ -625,46 +722,38 @@ resubscribeSMPSession c@AgentClient {smpSubWorkers} tSess = -- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar. -- Not waiting may result in terminated worker remaining in the map. whenM (isEmptyTMVar $ sessionVar v) retry - removeTSessVar v tSess smpSubWorkers + removeSessVar v tSess smpSubWorkers -reconnectSMPClient :: TVar Int -> AgentClient -> SMPTransportSession -> NonEmpty RcvQueue -> AM () -reconnectSMPClient tc c tSess@(_, srv, _) qs = do - NetworkConfig {tcpTimeout} <- atomically $ getNetworkConfig c - -- this allows 3x of timeout per batch of subscription (90 queues per batch empirically) - let t = (length qs `div` 90 + 1) * tcpTimeout * 3 - ExceptT (sequence <$> (t `timeout` runExceptT resubscribe)) >>= \case - Just _ -> atomically $ writeTVar tc 0 - Nothing -> do - tc' <- atomically $ stateTVar tc $ \i -> (i + 1, i + 1) - maxTC <- asks $ maxSubscriptionTimeouts . config - let err = if tc' >= maxTC then CRITICAL True else INTERNAL - msg = show tc' <> " consecutive subscription timeouts: " <> show (length qs) <> " queues, transport session: " <> show tSess - atomically $ writeTBQueue (subQ c) ("", "", APC SAEConn $ ERR $ err msg) +reconnectSMPClient :: AgentClient -> SMPTransportSession -> NonEmpty RcvQueue -> AM' () +reconnectSMPClient c tSess@(_, srv, _) qs = handleNotify $ do + cs <- readTVarIO $ RQ.getConnections $ activeSubs c + (rs, sessId_) <- subscribeQueues c $ L.toList qs + let (errs, okConns) = partitionEithers $ map (\(RcvQueue {connId}, r) -> bimap (connId,) (const connId) r) rs + conns = filter (`M.notMember` cs) okConns + unless (null conns) $ notifySub "" $ UP srv conns + let (tempErrs, finalErrs) = partition (temporaryAgentError . snd) errs + mapM_ (\(connId, e) -> notifySub connId $ ERR e) finalErrs + forM_ (listToMaybe tempErrs) $ \(connId, e) -> do + when (null okConns && M.null cs && null finalErrs) . liftIO $ + forM_ sessId_ $ \sessId -> do + -- We only close the client session that was used to subscribe. + v_ <- atomically $ ifM (activeClientSession c tSess sessId) (TM.lookupDelete tSess $ smpClients c) (pure Nothing) + mapM_ (closeClient_ c) v_ + notifySub connId $ ERR e where - resubscribe :: AM () - resubscribe = do - cs <- readTVarIO $ RQ.getConnections $ activeSubs c - rs <- lift . subscribeQueues c $ L.toList qs - let (errs, okConns) = partitionEithers $ map (\(RcvQueue {connId}, r) -> bimap (connId,) (const connId) r) rs - liftIO $ do - let conns = filter (`M.notMember` cs) okConns - unless (null conns) $ notifySub "" $ UP srv conns - let (tempErrs, finalErrs) = partition (temporaryAgentError . snd) errs - liftIO $ mapM_ (\(connId, e) -> notifySub connId $ ERR e) finalErrs - forM_ (listToMaybe tempErrs) $ \(_, err) -> do - when (null okConns && M.null cs && null finalErrs) . liftIO $ - closeClient c smpClients tSess - throwError err - notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> IO () + handleNotify :: AM' () -> AM' () + handleNotify = E.handleAny $ notifySub "" . ERR . INTERNAL . show + notifySub :: forall e. AEntityI e => ConnId -> ACommand 'Agent e -> AM' () notifySub connId cmd = atomically $ writeTBQueue (subQ c) ("", connId, APC (sAEntity @e) cmd) getNtfServerClient :: AgentClient -> NtfTransportSession -> AM NtfClient -getNtfServerClient c@AgentClient {active, ntfClients} tSess@(userId, srv, _) = do +getNtfServerClient c@AgentClient {active, ntfClients, workerSeq} tSess@(userId, srv, _) = do unlessM (readTVarIO active) . throwError $ INACTIVE - atomically (getTSessVar c tSess ntfClients) + ts <- liftIO getCurrentTime + atomically (getSessVar workerSeq tSess ntfClients ts) >>= either (newProtocolClient c tSess ntfClients connectClient) - (waitForProtocolClient c tSess) + (waitForProtocolClient c tSess ntfClients) where connectClient :: NtfClientVar -> AM NtfClient connectClient v = do @@ -676,64 +765,55 @@ getNtfServerClient c@AgentClient {active, ntfClients} tSess@(userId, srv, _) = d clientDisconnected :: NtfClientVar -> NtfClient -> IO () clientDisconnected v client = do - atomically $ removeTSessVar v tSess ntfClients + atomically $ removeSessVar v tSess ntfClients incClientStat c userId client "DISCONNECT" "" atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv getXFTPServerClient :: AgentClient -> XFTPTransportSession -> AM XFTPClient -getXFTPServerClient c@AgentClient {active, xftpClients} tSess@(userId, srv, _) = do +getXFTPServerClient c@AgentClient {active, xftpClients, workerSeq} tSess@(userId, srv, _) = do unlessM (readTVarIO active) . throwError $ INACTIVE - atomically (getTSessVar c tSess xftpClients) + ts <- liftIO getCurrentTime + atomically (getSessVar workerSeq tSess xftpClients ts) >>= either (newProtocolClient c tSess xftpClients connectClient) - (waitForProtocolClient c tSess) + (waitForProtocolClient c tSess xftpClients) where connectClient :: XFTPClientVar -> AM XFTPClient connectClient v = do cfg <- asks $ xftpCfg . config - g <- asks random xftpNetworkConfig <- atomically $ getNetworkConfig c liftError' (protocolClientError XFTP $ B.unpack $ strEncode srv) $ - X.getXFTPClient g tSess cfg {xftpNetworkConfig} $ + X.getXFTPClient tSess cfg {xftpNetworkConfig} $ clientDisconnected v clientDisconnected :: XFTPClientVar -> XFTPClient -> IO () clientDisconnected v client = do - atomically $ removeTSessVar v tSess xftpClients + atomically $ removeSessVar v tSess xftpClients incClientStat c userId client "DISCONNECT" "" atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ hostEvent DISCONNECT client) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv -getTSessVar :: forall a s. AgentClient -> TransportSession s -> TMap (TransportSession s) (SessionVar a) -> STM (Either (SessionVar a) (SessionVar a)) -getTSessVar c tSess vs = maybe (Left <$> newSessionVar) (pure . Right) =<< TM.lookup tSess vs - where - newSessionVar :: STM (SessionVar a) - newSessionVar = do - sessionVar <- newEmptyTMVar - sessionVarId <- stateTVar (workerSeq c) $ \next -> (next, next + 1) - let v = SessionVar {sessionVar, sessionVarId} - TM.insert tSess v vs - pure v - -removeTSessVar :: SessionVar a -> TransportSession msg -> TMap (TransportSession msg) (SessionVar a) -> STM () -removeTSessVar = void .:. removeTSessVar' -{-# INLINE removeTSessVar #-} - -removeTSessVar' :: SessionVar a -> TransportSession msg -> TMap (TransportSession msg) (SessionVar a) -> STM Bool -removeTSessVar' v tSess vs = - TM.lookup tSess vs >>= \case - Just v' | sessionVarId v == sessionVarId v' -> TM.delete tSess vs $> True - _ -> pure False - -waitForProtocolClient :: ProtocolTypeI (ProtoType msg) => AgentClient -> TransportSession msg -> ClientVar msg -> AM (Client msg) -waitForProtocolClient c (_, srv, _) v = do +waitForProtocolClient :: + (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => + AgentClient -> + TransportSession msg -> + TMap (TransportSession msg) (ClientVar msg) -> + ClientVar msg -> + AM (Client msg) +waitForProtocolClient c tSess@(_, srv, _) clients v = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c client_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) - liftEither $ case client_ of - Just (Right smpClient) -> Right smpClient - Just (Left e) -> Left e - Nothing -> Left $ BROKER (B.unpack $ strEncode srv) TIMEOUT + case client_ of + Just (Right smpClient) -> pure smpClient + Just (Left (e, ts_)) -> case ts_ of + Nothing -> throwE e + Just ts -> + ifM + ((ts <) <$> liftIO getCurrentTime) + (atomically (removeSessVar v tSess clients) >> getProtocolServerClient c tSess) + (throwE e) + Nothing -> throwE $ BROKER (B.unpack $ strEncode srv) TIMEOUT -- clientConnected arg is only passed for SMP server newProtocolClient :: @@ -755,13 +835,22 @@ newProtocolClient c tSess@(userId, srv, entityId_) clients connectClient v = pure client Left e -> do liftIO $ incServerStat c userId srv "CLIENT" $ strEncode e - atomically $ do - removeTSessVar v tSess clients - putTMVar (sessionVar v) (Left e) - throwError e -- signal error to caller + ei <- asks $ persistErrorInterval . config + if ei == 0 + then atomically $ do + removeSessVar v tSess clients + putTMVar (sessionVar v) (Left (e, Nothing)) + else do + ts <- addUTCTime ei <$> liftIO getCurrentTime + atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) + throwE e -- signal error to caller hostEvent :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> Client msg -> ACommand 'Agent 'AENone -hostEvent event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost +hostEvent event = hostEvent' event . protocolClient +{-# INLINE hostEvent #-} + +hostEvent' :: forall v err msg. (ProtocolTypeI (ProtoType msg), ProtocolServerClient v err msg) => (AProtocolType -> TransportHost -> ACommand 'Agent 'AENone) -> ProtoClient msg -> ACommand 'Agent 'AENone +hostEvent' event = event (AProtocolType $ protocolTypeI @(ProtoType msg)) . clientTransportHost getClientConfig :: AgentClient -> (AgentConfig -> ProtocolClientConfig v) -> AM' (ProtocolClientConfig v) getClientConfig c cfgSel = do @@ -772,42 +861,22 @@ getClientConfig c cfgSel = do getNetworkConfig :: AgentClient -> STM NetworkConfig getNetworkConfig c = do (slowCfg, fastCfg) <- readTVar (useNetworkConfig c) - UserNetworkState {networkType} <- readTVar (userNetworkState c) + UserNetworkInfo {networkType} <- readTVar $ userNetworkInfo c pure $ case networkType of UNCellular -> slowCfg UNNone -> slowCfg _ -> fastCfg -waitForUserNetwork :: AgentClient -> AM' () -waitForUserNetwork AgentClient {userNetworkState} = - (offline <$> readTVarIO userNetworkState) >>= mapM_ waitWhileOffline - where - waitWhileOffline UNSOffline {offlineDelay = d} = - unlessM (liftIO $ waitOnline d False) $ do -- network delay reached, increase delay - ts' <- liftIO getCurrentTime - ni <- asks $ userNetworkInterval . config - atomically $ do - ns@UserNetworkState {offline} <- readTVar userNetworkState - forM_ offline $ \UNSOffline {offlineDelay = d', offlineFrom = ts} -> - -- Using `min` to avoid multiple updates in a short period of time - -- and to reset `offlineDelay` if network went `on` and `off` again. - writeTVar userNetworkState $! - let d'' = nextRetryDelay (diffToMicroseconds $ diffUTCTime ts' ts) (min d d') ni - in ns {offline = Just UNSOffline {offlineDelay = d'', offlineFrom = ts}} - waitOnline :: Int64 -> Bool -> IO Bool - waitOnline t online' - | t <= 0 = pure online' - | otherwise = - registerDelay (fromIntegral maxWait) - >>= atomically . onlineOrDelay - >>= waitOnline (t - maxWait) - where - maxWait = min t $ fromIntegral (maxBound :: Int) - onlineOrDelay delay = do - online <- isNothing . offline <$> readTVar userNetworkState - expired <- readTVar delay - unless (online || expired) retry - pure online +-- returns fast network config +getNetworkConfig' :: AgentClient -> IO NetworkConfig +getNetworkConfig' = fmap snd . readTVarIO . useNetworkConfig +{-# INLINE getNetworkConfig' #-} + +waitForUserNetwork :: AgentClient -> IO () +waitForUserNetwork c = + unlessM (atomically $ isNetworkOnline c) $ do + delay <- registerDelay $ userNetworkInterval $ config $ agentEnv c + atomically $ unlessM (isNetworkOnline c) $ unlessM (readTVar delay) retry closeAgentClient :: AgentClient -> IO () closeAgentClient c = do @@ -815,6 +884,7 @@ closeAgentClient c = do closeProtocolServerClients c smpClients closeProtocolServerClients c ntfClients closeProtocolServerClients c xftpClients + atomically $ writeTVar (smpProxiedRelays c) M.empty atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect clearWorkers smpDeliveryWorkers >>= mapM_ (cancelWorker . fst) clearWorkers asyncCmdWorkers >>= mapM_ cancelWorker @@ -865,9 +935,10 @@ closeClient c clientSel tSess = closeClient_ :: ProtocolServerClient v err msg => AgentClient -> ClientVar msg -> IO () closeClient_ c v = do NetworkConfig {tcpConnectTimeout} <- atomically $ getNetworkConfig c - E.handle (\BlockedIndefinitelyOnSTM -> pure ()) $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) >>= \case - Just (Right client) -> closeProtocolServerClient client `catchAll_` pure () - _ -> pure () + E.handle (\BlockedIndefinitelyOnSTM -> pure ()) $ + tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) >>= \case + Just (Right client) -> closeProtocolServerClient (protocolClient client) `catchAll_` pure () + _ -> pure () closeXFTPServerClient :: AgentClient -> UserId -> XFTPServer -> FileDigest -> IO () closeXFTPServerClient c userId server (FileDigest chunkDigest) = @@ -890,15 +961,15 @@ withInvLock' :: AgentClient -> ByteString -> String -> AM' a -> AM' a withInvLock' AgentClient {invLocks} = withLockMap invLocks {-# INLINE withInvLock' #-} -withConnLocks :: AgentClient -> [ConnId] -> String -> AM' a -> AM' a -withConnLocks AgentClient {connLocks} = withLocksMap_ connLocks . filter (not . B.null) +withConnLocks :: AgentClient -> Set ConnId -> String -> AM' a -> AM' a +withConnLocks AgentClient {connLocks} = withLocksMap_ connLocks {-# INLINE withConnLocks #-} withLockMap :: (Ord k, MonadUnliftIO m) => TMap k Lock -> k -> String -> m a -> m a withLockMap = withGetLock . getMapLock {-# INLINE withLockMap #-} -withLocksMap_ :: (Ord k, MonadUnliftIO m) => TMap k Lock -> [k] -> String -> m a -> m a +withLocksMap_ :: (Ord k, MonadUnliftIO m) => TMap k Lock -> Set k -> String -> m a -> m a withLocksMap_ = withGetLocks . getMapLock {-# INLINE withLocksMap_ #-} @@ -919,6 +990,25 @@ withClient_ c tSess@(userId, srv, _) statCmd action = do stat cl $ strEncode e throwError e +withProxySession :: AgentClient -> SMPTransportSession -> SMP.SenderId -> ByteString -> ((SMPConnectedClient, ProxiedRelay) -> AM a) -> AM a +withProxySession c destSess@(userId, destSrv, _) entId cmdStr action = do + (cl, sess_) <- getSMPProxyClient c destSess + logServer ("--> " <> proxySrv cl <> " >") c destSrv entId cmdStr + case sess_ of + Right sess -> do + r <- (action (cl, sess) <* stat cl "OK") `catchAgentError` logServerError cl + logServer ("<-- " <> proxySrv cl <> " <") c destSrv entId "OK" + pure r + Left e -> logServerError cl e + where + stat cl = liftIO . incClientStat c userId cl cmdStr + proxySrv = showServer . protocolClientServer' . protocolClient + logServerError :: SMPConnectedClient -> AgentErrorType -> AM a + logServerError cl e = do + logServer ("<-- " <> proxySrv cl <> " <") c destSrv "" $ strEncode e + stat cl $ strEncode e + throwError e + withLogClient_ :: ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> AM a) -> AM a withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do logServer "-->" c srv entId cmdStr @@ -927,22 +1017,82 @@ withLogClient_ c tSess@(_, srv, _) entId cmdStr action = do return res withClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer client) $ action client +withClient c tSess statKey action = withClient_ c tSess statKey $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withClient #-} withLogClient :: forall v err msg a. ProtocolServerClient v err msg => AgentClient -> TransportSession msg -> EntityId -> ByteString -> (Client msg -> ExceptT (ProtocolClientError err) IO a) -> AM a -withLogClient c tSess entId cmdStr action = withLogClient_ c tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer client) $ action client +withLogClient c tSess entId cmdStr action = withLogClient_ c tSess entId cmdStr $ \client -> liftClient (clientProtocolError @v @err @msg) (clientServer $ protocolClient client) $ action client {-# INLINE withLogClient #-} withSMPClient :: SMPQueueRec q => AgentClient -> q -> ByteString -> (SMPClient -> ExceptT SMPClientError IO a) -> AM a withSMPClient c q cmdStr action = do tSess <- liftIO $ mkSMPTransportSession c q - withLogClient c tSess (queueId q) cmdStr action + withLogClient c tSess (queueId q) cmdStr $ action . connectedClient -withSMPClient_ :: SMPQueueRec q => AgentClient -> q -> ByteString -> (SMPClient -> AM a) -> AM a -withSMPClient_ c q cmdStr action = do - tSess <- liftIO $ mkSMPTransportSession c q - withLogClient_ c tSess (queueId q) cmdStr action +sendOrProxySMPMessage :: AgentClient -> UserId -> SMPServer -> ByteString -> Maybe SMP.SndPrivateAuthKey -> SMP.SenderId -> MsgFlags -> SMP.MsgBody -> AM (Maybe SMPServer) +sendOrProxySMPMessage c userId destSrv cmdStr spKey_ senderId msgFlags msg = do + sess <- liftIO $ mkTransportSession c userId destSrv senderId + ifM (atomically shouldUseProxy) (sendViaProxy sess) (sendDirectly sess $> Nothing) + where + shouldUseProxy = do + cfg <- getNetworkConfig c + case smpProxyMode cfg of + SPMAlways -> pure True + SPMUnknown -> unknownServer + SPMUnprotected + | ipAddressProtected cfg destSrv -> pure False + | otherwise -> unknownServer + SPMNever -> pure False + directAllowed = do + cfg <- getNetworkConfig c + pure $ case smpProxyFallback cfg of + SPFAllow -> True + SPFAllowProtected -> ipAddressProtected cfg destSrv + SPFProhibit -> False + unknownServer = maybe True (all ((destSrv /=) . protoServer)) <$> TM.lookup userId (userServers c) + sendViaProxy destSess@(_, _, qId) = do + r <- tryAgentError . withProxySession c destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess) -> do + liftClient SMP (clientServer smp) (proxySMPMessage smp proxySess spKey_ senderId msgFlags msg) >>= \case + Right () -> pure . Just $ protocolClientServer' smp + Left proxyErr -> do + case proxyErr of + (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> atomically deleteRelaySession + _ -> pure () + throwE + PROXY + { proxyServer = protocolClientServer smp, + relayServer = B.unpack $ strEncode destSrv, + proxyErr + } + where + -- checks that the current proxied relay session is the same one that was used to send the message and removes it + deleteRelaySession = + ( TM.lookup destSess (smpProxiedRelays c) + $>>= \(ProtoServerWithAuth srv _) -> tryReadSessVar (userId, srv, qId) (smpClients c) + ) + >>= \case + Just (Right (SMPConnectedClient smp' prs)) + | sameClient smp' -> + tryReadSessVar destSrv prs >>= \case + Just (Right proxySess') | sameProxiedRelay proxySess' -> TM.delete destSrv prs + _ -> pure () + _ -> pure () + sameClient smp' = sessionId (thParams smp) == sessionId (thParams smp') + sameProxiedRelay proxySess' = prSessionId proxySess == prSessionId proxySess' + case r of + Right r' -> pure r' + Left e + | serverHostError e -> ifM (atomically directAllowed) (sendDirectly destSess $> Nothing) (throwE e) + | otherwise -> throwE e + sendDirectly tSess = + withLogClient_ c tSess senderId ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> + liftClient SMP (clientServer smp) $ sendSMPMessage smp spKey_ senderId msgFlags msg + +ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool +ipAddressProtected NetworkConfig {socksProxy, hostMode} (ProtocolServer _ hosts _ _) = do + isJust socksProxy || (hostMode == HMOnion && any isOnionHost hosts) + where + isOnionHost = \case THOnionHost _ -> True; _ -> False withNtfClient :: AgentClient -> NtfServer -> EntityId -> ByteString -> (NtfClient -> ExceptT NtfClientError IO a) -> AM a withNtfClient c srv = withLogClient c (0, srv, Nothing) @@ -958,15 +1108,15 @@ withXFTPClient c (userId, srv, entityId) cmdStr action = do tSess <- liftIO $ mkTransportSession c userId srv entityId withLogClient c tSess entityId cmdStr action -liftClient :: (Show err, Encoding err) => (err -> AgentErrorType) -> HostName -> ExceptT (ProtocolClientError err) IO a -> AM a +liftClient :: (Show err, Encoding err) => (HostName -> err -> AgentErrorType) -> HostName -> ExceptT (ProtocolClientError err) IO a -> AM a liftClient protocolError_ = liftError . protocolClientError protocolError_ {-# INLINE liftClient #-} -protocolClientError :: (Show err, Encoding err) => (err -> AgentErrorType) -> HostName -> ProtocolClientError err -> AgentErrorType +protocolClientError :: (Show err, Encoding err) => (HostName -> err -> AgentErrorType) -> HostName -> ProtocolClientError err -> AgentErrorType protocolClientError protocolError_ host = \case - PCEProtocolError e -> protocolError_ e + PCEProtocolError e -> protocolError_ host e PCEResponseError e -> BROKER host $ RESPONSE $ B.unpack $ smpEncode e - PCEUnexpectedResponse _ -> BROKER host UNEXPECTED + PCEUnexpectedResponse e -> BROKER host $ UNEXPECTED $ B.unpack e PCEResponseTimeout -> BROKER host TIMEOUT PCENetworkError -> BROKER host NETWORK PCEIncompatibleHost -> BROKER host HOST @@ -1013,7 +1163,6 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do liftError (testErr TSSecureQueue) $ secureSMPQueue smp rpKey rcvId sKey liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp rpKey rcvId ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient smp - incClientStat c userId smp "SMP_TEST" "OK" pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok Left e -> pure (Just $ testErr TSConnect e) where @@ -1031,7 +1180,7 @@ runXFTPServerTest c userId (ProtoServerWithAuth srv auth) = do rcvPath <- getTempFilePath workDir liftIO $ do let tSess = (userId, srv, Nothing) - X.getXFTPClient g tSess cfg {xftpNetworkConfig} (\_ -> pure ()) >>= \case + X.getXFTPClient tSess cfg {xftpNetworkConfig} (\_ -> pure ()) >>= \case Right xftp -> withTestChunk filePath $ do (sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g (rcvKey, rpKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g @@ -1043,7 +1192,7 @@ runXFTPServerTest c userId (ProtoServerWithAuth srv auth) = do liftError (testErr TSUploadFile) $ X.uploadXFTPChunk xftp spKey sId chunkSpec liftError (testErr TSDownloadFile) $ X.downloadXFTPChunk g xftp rpKey rId $ XFTPRcvChunkSpec rcvPath chSize digest rcvDigest <- liftIO $ C.sha256Hash <$> B.readFile rcvPath - unless (digest == rcvDigest) $ throwError $ ProtocolTestFailure TSCompareFile $ XFTP DIGEST + unless (digest == rcvDigest) $ throwError $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST liftError (testErr TSDeleteFile) $ X.deleteXFTPChunk xftp spKey sId ok <- tcpTimeout xftpNetworkConfig `timeout` X.closeXFTPClient xftp incClientStat c userId xftp "XFTP_TEST" "OK" @@ -1118,7 +1267,7 @@ getSessionMode :: AgentClient -> IO TransportSessionMode getSessionMode = atomically . fmap sessionMode . getNetworkConfig {-# INLINE getSessionMode #-} -newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri) +newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId) newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do C.AuthAlg a <- asks (rcvAuthAlg . config) g <- asks random @@ -1127,8 +1276,9 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do (e2eDhKey, e2ePrivKey) <- atomically $ C.generateKeyPair g logServer "-->" c srv "" "NEW" tSess <- liftIO $ mkTransportSession c userId srv connId - QIK {rcvId, sndId, rcvPublicDhKey} <- - withClient c tSess "NEW" $ \smp -> createSMPQueue smp rKeys dhKey auth subMode + (sessId, QIK {rcvId, sndId, rcvPublicDhKey}) <- + withClient c tSess "NEW" $ \(SMPConnectedClient smp _) -> + (sessionId $ thParams smp,) <$> createSMPQueue smp rKeys dhKey auth subMode liftIO . logServer "<--" c srv "" $ B.unwords ["IDS", logSecret rcvId, logSecret sndId] let rq = RcvQueue @@ -1150,34 +1300,51 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode = do clientNtfCreds = Nothing, deleteErrors = 0 } - pure (rq, SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey) + qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey + pure (rq, qUri, tSess, sessId) -processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> IO (Either SMPClientError ()) -processSubResult c rq r = do - case r of - Left e -> - unless (temporaryClientError e) . atomically $ do - RQ.deleteQueue rq (pendingSubs c) - TM.insert (RQ.qKey rq) e (removedSubs c) - _ -> addSubscription c rq - pure r +processSubResult :: AgentClient -> RcvQueue -> Either SMPClientError () -> STM () +processSubResult c rq@RcvQueue {connId} = \case + Left e -> + unless (temporaryClientError e) $ + failSubscription c rq e + Right () -> + whenM (hasPendingSubscription c connId) $ + addSubscription c rq temporaryAgentError :: AgentErrorType -> Bool temporaryAgentError = \case - BROKER _ NETWORK -> True - BROKER _ TIMEOUT -> True + BROKER _ e -> tempBrokerError e + SMP _ (SMP.PROXY (SMP.BROKER e)) -> tempBrokerError e + XFTP _ XFTP.TIMEOUT -> True + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> tempBrokerError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY SMP.NO_SESSION)) -> True INACTIVE -> True _ -> False -{-# INLINE temporaryAgentError #-} + where + tempBrokerError = \case + NETWORK -> True + TIMEOUT -> True + _ -> False temporaryOrHostError :: AgentErrorType -> Bool -temporaryOrHostError = \case - BROKER _ HOST -> True - e -> temporaryAgentError e +temporaryOrHostError e = temporaryAgentError e || serverHostError e {-# INLINE temporaryOrHostError #-} +serverHostError :: AgentErrorType -> Bool +serverHostError = \case + BROKER _ e -> brokerHostError e + SMP _ (SMP.PROXY (SMP.BROKER e)) -> brokerHostError e + PROXY _ _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerHostError e + _ -> False + where + brokerHostError = \case + HOST -> True + SMP.TRANSPORT TEVersion -> True + _ -> False + -- | Subscribe to queues. The list of results can have a different order. -subscribeQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] +subscribeQueues :: AgentClient -> [RcvQueue] -> AM' ([(RcvQueue, Either AgentErrorType ())], Maybe SessionId) subscribeQueues c qs = do (errs, qs') <- partitionEithers <$> mapM checkQueue qs atomically $ do @@ -1185,20 +1352,43 @@ subscribeQueues c qs = do RQ.batchAddQueues (pendingSubs c) qs' env <- ask -- only "checked" queues are subscribed - (errs <>) <$> sendTSessionBatches "SUB" 90 id (subscribeQueues_ env) c qs' + session <- newTVarIO Nothing + rs <- sendTSessionBatches "SUB" 90 id (subscribeQueues_ env session) c qs' + (errs <> rs,) <$> readTVarIO session where checkQueue rq = do prohibited <- atomically $ hasGetLock c rq - pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED) else Right rq - subscribeQueues_ :: Env -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) - subscribeQueues_ env smp qs' = do + pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED "subscribeQueues") else Right rq + subscribeQueues_ :: Env -> TVar (Maybe SessionId) -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ()) + subscribeQueues_ env session smp qs' = do rs <- sendBatch subscribeSMPQueues smp qs' - mapM_ (uncurry $ processSubResult c) rs - when (any temporaryClientError . lefts . map snd $ L.toList rs) $ - runReaderT (resubscribeSMPSession c $ transportSession' smp) env - pure rs + active <- + atomically $ + ifM + (activeClientSession c tSess sessId) + (writeTVar session (Just sessId) >> processSubResults rs $> True) + (pure False) + if active + then when (hasTempErrors rs) resubscribe $> rs + else do + logWarn "subcription batch result for replaced SMP client, resubscribing" + resubscribe $> L.map (second $ \_ -> Left PCENetworkError) rs + where + tSess = transportSession' smp + sessId = sessionId $ thParams smp + hasTempErrors = any (either temporaryClientError (const False) . snd) + processSubResults :: NonEmpty (RcvQueue, Either SMPClientError ()) -> STM () + processSubResults = mapM_ $ uncurry $ processSubResult c + resubscribe = resubscribeSMPSession c tSess `runReaderT` env -type BatchResponses e r = (NonEmpty (RcvQueue, Either e r)) +activeClientSession :: AgentClient -> SMPTransportSession -> SessionId -> STM Bool +activeClientSession c tSess sessId = sameSess <$> tryReadSessVar tSess (smpClients c) + where + sameSess = \case + Just (Right (SMPConnectedClient smp _)) -> sessId == sessionId (thParams smp) + _ -> False + +type BatchResponses e r = NonEmpty (RcvQueue, Either e r) -- statBatchSize is not used to batch the commands, only for traffic statistics sendTSessionBatches :: forall q r. ByteString -> Int -> (q -> RcvQueue) -> (SMPClient -> NonEmpty q -> IO (BatchResponses SMPClientError r)) -> AgentClient -> [q] -> AM' [(RcvQueue, Either AgentErrorType r)] @@ -1217,7 +1407,7 @@ sendTSessionBatches statCmd statBatchSize toRQ action c qs = sendClientBatch (tSess@(userId, srv, _), qs') = tryAgentError' (getSMPServerClient c tSess) >>= \case Left e -> pure $ L.map ((,Left e) . toRQ) qs' - Right smp -> liftIO $ do + Right (SMPConnectedClient smp _) -> liftIO $ do logServer "-->" c srv (bshow (length qs') <> " queues") statCmd rs <- L.map agentError <$> action smp qs' statBatch @@ -1233,16 +1423,40 @@ sendBatch smpCmdFunc smp qs = L.zip qs <$> smpCmdFunc smp (L.map queueCreds qs) where queueCreds RcvQueue {rcvPrivateKey, rcvId} = (rcvPrivateKey, rcvId) -addSubscription :: AgentClient -> RcvQueue -> IO () -addSubscription c rq@RcvQueue {connId} = atomically $ do +addSubscription :: AgentClient -> RcvQueue -> STM () +addSubscription c rq@RcvQueue {connId} = do modifyTVar' (subscrConns c) $ S.insert connId RQ.addQueue rq $ activeSubs c RQ.deleteQueue rq $ pendingSubs c +failSubscription :: AgentClient -> RcvQueue -> SMPClientError -> STM () +failSubscription c rq e = do + RQ.deleteQueue rq (pendingSubs c) + TM.insert (RQ.qKey rq) e (removedSubs c) + +addPendingSubscription :: AgentClient -> RcvQueue -> STM () +addPendingSubscription c rq@RcvQueue {connId} = do + modifyTVar' (subscrConns c) $ S.insert connId + RQ.addQueue rq $ pendingSubs c + +addNewQueueSubscription :: AgentClient -> RcvQueue -> SMPTransportSession -> SessionId -> AM' () +addNewQueueSubscription c rq tSess sessId = do + same <- + atomically $ + ifM + (activeClientSession c tSess sessId) + (True <$ addSubscription c rq) + (False <$ addPendingSubscription c rq) + unless same $ resubscribeSMPSession c tSess + hasActiveSubscription :: AgentClient -> ConnId -> STM Bool hasActiveSubscription c connId = RQ.hasConn connId $ activeSubs c {-# INLINE hasActiveSubscription #-} +hasPendingSubscription :: AgentClient -> ConnId -> STM Bool +hasPendingSubscription c connId = RQ.hasConn connId $ pendingSubs c +{-# INLINE hasPendingSubscription #-} + removeSubscription :: AgentClient -> ConnId -> STM () removeSubscription c connId = do modifyTVar' (subscrConns c) $ S.delete connId @@ -1267,20 +1481,17 @@ logSecret :: ByteString -> ByteString logSecret bs = encode $ B.take 3 bs {-# INLINE logSecret #-} -sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM () -sendConfirmation c sq@SndQueue {sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = - withSMPClient_ c sq "SEND " $ \smp -> do - let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation - msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg - liftClient SMP (clientServer smp) $ sendSMPMessage smp Nothing sndId (SMP.MsgFlags {notification = True}) msg +sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM (Maybe SMPServer) +sendConfirmation c sq@SndQueue {userId, server, sndId, sndPublicKey = Just sndPublicKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do + let clientMsg = SMP.ClientMessage (SMP.PHConfirmation sndPublicKey) agentConfirmation + msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg + sendOrProxySMPMessage c userId server "" Nothing sndId (MsgFlags {notification = True}) msg sendConfirmation _ _ _ = throwError $ INTERNAL "sendConfirmation called without snd_queue public key(s) in the database" -sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM () +sendInvitation :: AgentClient -> UserId -> Compatible SMPQueueInfo -> Compatible VersionSMPA -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> AM (Maybe SMPServer) sendInvitation c userId (Compatible (SMPQueueInfo v SMPQueueAddress {smpServer, senderId, dhPublicKey})) (Compatible agentVersion) connReq connInfo = do - tSess <- liftIO $ mkTransportSession c userId smpServer senderId - withLogClient_ c tSess senderId "SEND " $ \smp -> do - msg <- mkInvitation - liftClient SMP (clientServer smp) $ sendSMPMessage smp Nothing senderId MsgFlags {notification = True} msg + msg <- mkInvitation + sendOrProxySMPMessage c userId smpServer "" Nothing senderId (MsgFlags {notification = True}) msg where mkInvitation :: AM ByteString -- this is only encrypted with per-queue E2E, not with double ratchet @@ -1364,12 +1575,16 @@ deleteQueue c rq@RcvQueue {rcvId, rcvPrivateKey} = do deleteQueues :: AgentClient -> [RcvQueue] -> AM' [(RcvQueue, Either AgentErrorType ())] deleteQueues = sendTSessionBatches "DEL" 90 id $ sendBatch deleteSMPQueues -sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM () -sendAgentMessage c sq@SndQueue {sndId, sndPrivateKey} msgFlags agentMsg = - withSMPClient_ c sq "SEND " $ \smp -> do - let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg - msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg - liftClient SMP (clientServer smp) $ sendSMPMessage smp (Just sndPrivateKey) sndId msgFlags msg +sendAgentMessage :: AgentClient -> SndQueue -> MsgFlags -> ByteString -> AM (Maybe SMPServer) +sendAgentMessage c sq@SndQueue {userId, server, sndId, sndPrivateKey} msgFlags agentMsg = do + let clientMsg = SMP.ClientMessage SMP.PHEmpty agentMsg + msg <- agentCbEncrypt sq Nothing $ smpEncode clientMsg + sendOrProxySMPMessage c userId server "" (Just sndPrivateKey) sndId msgFlags msg + +getQueueInfo :: AgentClient -> RcvQueue -> AM QueueInfo +getQueueInfo c rq@RcvQueue {rcvId, rcvPrivateKey} = + withSMPClient c rq "QUE" $ \smp -> + getSMPQueueInfo smp rcvPrivateKey rcvId agentNtfRegisterToken :: AgentClient -> NtfToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> AM (NtfTokenId, C.PublicKeyX25519) agentNtfRegisterToken c NtfToken {deviceToken, ntfServer, ntfPrivKey} ntfPubKey pubDhKey = @@ -1416,7 +1631,7 @@ agentXFTPNewChunk :: AgentClient -> SndFileChunk -> Int -> XFTPServerWithAuth -> agentXFTPNewChunk c SndFileChunk {userId, chunkSpec = XFTPChunkSpec {chunkSize}, digest = FileDigest chunkDigest} n (ProtoServerWithAuth srv auth) = do rKeys <- xftpRcvKeys n (sndKey, replicaKey) <- atomically . C.generateAuthKeyPair C.SEd25519 =<< asks random - let fileInfo = FileInfo {sndKey, size = fromIntegral chunkSize, digest = chunkDigest} + let fileInfo = FileInfo {sndKey, size = chunkSize, digest = chunkDigest} logServer "-->" c srv "" "FNEW" tSess <- liftIO $ mkTransportSession c userId srv chunkDigest (sndId, rIds) <- withClient c tSess "FNEW" $ \xftp -> X.createXFTPChunk xftp replicaKey fileInfo (L.map fst rKeys) auth @@ -1479,7 +1694,7 @@ agentCbDecrypt dhSecret nonce msg = cryptoError :: C.CryptoError -> AgentErrorType cryptoError = \case - C.CryptoLargeMsgError -> CMD LARGE + C.CryptoLargeMsgError -> CMD LARGE "CryptoLargeMsgError" C.CryptoHeaderError _ -> AGENT A_MESSAGE -- parsing error C.CERatchetDuplicateMessage -> AGENT A_DUPLICATE C.AESDecryptError -> c DECRYPT_AES @@ -1590,10 +1805,16 @@ withStore :: AgentClient -> (DB.Connection -> IO (Either StoreError a)) -> AM a withStore c action = do st <- asks store withExceptT storeError . ExceptT . liftIO . agentOperationBracket c AODatabase (\_ -> pure ()) $ - withTransaction st action `E.catch` handleInternal "" + withTransaction st action `E.catches` handleDBErrors where - handleInternal :: String -> E.SomeException -> IO (Either StoreError a) - handleInternal ctxStr e = pure . Left . SEInternal . B.pack $ show e <> ctxStr + handleDBErrors :: [E.Handler IO (Either StoreError a)] + handleDBErrors = + [ E.Handler $ \(e :: SQL.SQLError) -> + let se = SQL.sqlError e + busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked + in pure . Left . (if busy then SEDatabaseBusy else SEInternal) $ bshow se, + E.Handler $ \(E.SomeException e) -> pure . Left $ SEInternal $ bshow e + ] withStoreBatch :: Traversable t => AgentClient -> (DB.Connection -> t (IO (Either AgentErrorType a))) -> AM' (t (Either AgentErrorType a)) withStoreBatch c actions = do @@ -1616,11 +1837,12 @@ storeError = \case SEConnDuplicate -> CONN DUPLICATE SEBadConnType CRcv -> CONN SIMPLEX SEBadConnType CSnd -> CONN SIMPLEX - SEInvitationNotFound -> CMD PROHIBITED + SEInvitationNotFound -> CMD PROHIBITED "SEInvitationNotFound" -- this error is never reported as store error, -- it is used to wrap agent operations when "transaction-like" store access is needed -- NOTE: network IO should NOT be used inside AgentStoreMonad SEAgentError e -> e + SEDatabaseBusy e -> CRITICAL True $ B.unpack e e -> INTERNAL $ show e incStat :: AgentClient -> Int -> AgentStatsKey -> STM () @@ -1630,9 +1852,13 @@ incStat AgentClient {agentStats} n k = do _ -> newTVar n >>= \v -> TM.insert k v agentStats incClientStat :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> ByteString -> ByteString -> IO () -incClientStat c userId pc = incClientStatN c userId pc 1 +incClientStat c userId = incClientStat' c userId . protocolClient {-# INLINE incClientStat #-} +incClientStat' :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> ByteString -> ByteString -> IO () +incClientStat' c userId pc = incClientStatN c userId pc 1 +{-# INLINE incClientStat' #-} + incServerStat :: AgentClient -> UserId -> ProtocolServer p -> ByteString -> ByteString -> IO () incServerStat c userId ProtocolServer {host} cmd res = do threadDelay 100000 @@ -1640,7 +1866,7 @@ incServerStat c userId ProtocolServer {host} cmd res = do where statsKey = AgentStatsKey {userId, host = strEncode $ L.head host, clientTs = "", cmd, res} -incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> Client msg -> Int -> ByteString -> ByteString -> IO () +incClientStatN :: ProtocolServerClient v err msg => AgentClient -> UserId -> ProtoClient msg -> Int -> ByteString -> ByteString -> IO () incClientStatN c userId pc n cmd res = do atomically $ incStat c n statsKey where @@ -1833,6 +2059,39 @@ getAgentWorkersSummary AgentClient {smpClients, ntfClients, xftpClients, smpDeli (pure WorkersSummary {numActive, numIdle = numIdle + 1, totalRestarts = totalRestarts + restartCount}) (pure WorkersSummary {numActive = numActive + 1, numIdle, totalRestarts = totalRestarts + restartCount}) +data AgentQueuesInfo = AgentQueuesInfo + { msgQInfo :: TBQueueInfo, + subQInfo :: TBQueueInfo, + smpClientsQueues :: Map Text (Int, UTCTime, ClientInfo) + } + deriving (Show) + +data ClientInfo + = ClientInfoQueues {sndQInfo :: TBQueueInfo, rcvQInfo :: TBQueueInfo} + | ClientInfoError {clientError :: (AgentErrorType, Maybe UTCTime)} + | ClientInfoConnecting + deriving (Show) + +getAgentQueuesInfo :: AgentClient -> IO AgentQueuesInfo +getAgentQueuesInfo AgentClient {msgQ, subQ, smpClients} = do + msgQInfo <- atomically $ getTBQueueInfo msgQ + subQInfo <- atomically $ getTBQueueInfo subQ + smpClientsMap <- readTVarIO smpClients + let smpClientsMap' = M.mapKeys (decodeLatin1 . strEncode) smpClientsMap + smpClientsQueues <- mapM getClientQueuesInfo smpClientsMap' + pure AgentQueuesInfo {msgQInfo, subQInfo, smpClientsQueues} + where + getClientQueuesInfo :: SMPClientVar -> IO (Int, UTCTime, ClientInfo) + getClientQueuesInfo SessionVar {sessionVar, sessionVarId, sessionVarTs} = do + clientInfo <- + atomically (tryReadTMVar sessionVar) >>= \case + Just (Right c) -> do + (sndQInfo, rcvQInfo) <- getProtocolClientQueuesInfo $ protocolClient c + pure ClientInfoQueues {sndQInfo, rcvQInfo} + Just (Left e) -> pure $ ClientInfoError e + Nothing -> pure ClientInfoConnecting + pure (sessionVarId, sessionVarTs, clientInfo) + $(J.deriveJSON defaultJSON ''AgentLocks) $(J.deriveJSON (enumJSON $ dropPrefix "TS") ''ProtocolTestStep) @@ -1851,6 +2110,10 @@ $(J.deriveJSON defaultJSON {J.fieldLabelModifier = takeWhile (/= '_')} ''AgentWo $(J.deriveJSON defaultJSON ''AgentWorkersSummary) +$(J.deriveJSON (sumTypeJSON $ dropPrefix "ClientInfo") ''ClientInfo) + +$(J.deriveJSON defaultJSON ''AgentQueuesInfo) + $(J.deriveJSON (enumJSON $ dropPrefix "UN") ''UserNetworkType) $(J.deriveJSON defaultJSON ''UserNetworkInfo) diff --git a/src/Simplex/Messaging/Agent/Env/SQLite.hs b/src/Simplex/Messaging/Agent/Env/SQLite.hs index 02a28ba95..63aa652ac 100644 --- a/src/Simplex/Messaging/Agent/Env/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Env/SQLite.hs @@ -54,9 +54,8 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (PQSupport, VersionRangeE2E, supportedE2EEncryptVRange) +import Simplex.Messaging.Crypto.Ratchet (VersionRangeE2E, supportedE2EEncryptVRange) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Notifications.Transport (NTFVersion) import Simplex.Messaging.Notifications.Types @@ -92,16 +91,17 @@ data AgentConfig = AgentConfig xftpCfg :: XFTPClientConfig, reconnectInterval :: RetryInterval, messageRetryInterval :: RetryInterval2, - userNetworkInterval :: RetryInterval, + userNetworkInterval :: Int, + userOfflineDelay :: NominalDiffTime, messageTimeout :: NominalDiffTime, connDeleteDeliveryTimeout :: NominalDiffTime, helloTimeout :: NominalDiffTime, quotaExceededTimeout :: NominalDiffTime, + persistErrorInterval :: NominalDiffTime, initialCleanupDelay :: Int64, cleanupInterval :: Int64, cleanupStepInterval :: Int, maxWorkerRestartsPerMin :: Int, - maxSubscriptionTimeouts :: Int, storedMsgDataTTL :: NominalDiffTime, rcvFilesTTL :: NominalDiffTime, sndFilesTTL :: NominalDiffTime, @@ -117,8 +117,8 @@ data AgentConfig = AgentConfig caCertificateFile :: FilePath, privateKeyFile :: FilePath, certificateFile :: FilePath, - e2eEncryptVRange :: PQSupport -> VersionRangeE2E, - smpAgentVRange :: PQSupport -> VersionRangeSMPA, + e2eEncryptVRange :: VersionRangeE2E, + smpAgentVRange :: VersionRangeSMPA, smpClientVRange :: VersionRangeSMPC } @@ -147,14 +147,6 @@ defaultMessageRetryInterval = } } -defaultUserNetworkInterval :: RetryInterval -defaultUserNetworkInterval = - RetryInterval - { initialInterval = 1200_000000, -- 20 minutes - increaseAfter = 0, - maxInterval = 7200_000000 -- 2 hours - } - defaultAgentConfig :: AgentConfig defaultAgentConfig = AgentConfig @@ -170,18 +162,17 @@ defaultAgentConfig = xftpCfg = defaultXFTPClientConfig, reconnectInterval = defaultReconnectInterval, messageRetryInterval = defaultMessageRetryInterval, - userNetworkInterval = defaultUserNetworkInterval, + userNetworkInterval = 1800_000000, -- 30 minutes, should be less than Int32 max value + userOfflineDelay = 2, -- if network offline event happens in less than 2 seconds after it was set online, it is ignored messageTimeout = 2 * nominalDay, connDeleteDeliveryTimeout = 2 * nominalDay, helloTimeout = 2 * nominalDay, quotaExceededTimeout = 7 * nominalDay, + persistErrorInterval = 3, -- seconds initialCleanupDelay = 30 * 1000000, -- 30 seconds cleanupInterval = 30 * 60 * 1000000, -- 30 minutes cleanupStepInterval = 200000, -- 200ms maxWorkerRestartsPerMin = 5, - -- 3 consecutive subscription timeouts will result in alert to the user - -- this is a fallback, as the timeout set to 3x of expected timeout, to avoid potential locking. - maxSubscriptionTimeouts = 3, storedMsgDataTTL = 21 * nominalDay, rcvFilesTTL = 2 * nominalDay, sndFilesTTL = nominalDay, diff --git a/src/Simplex/Messaging/Agent/Lock.hs b/src/Simplex/Messaging/Agent/Lock.hs index c0647b844..69b8169e2 100644 --- a/src/Simplex/Messaging/Agent/Lock.hs +++ b/src/Simplex/Messaging/Agent/Lock.hs @@ -12,6 +12,8 @@ import Control.Monad (void) import Control.Monad.Except (ExceptT (..), runExceptT) import Control.Monad.IO.Unlift import Data.Functor (($>)) +import Data.Set (Set) +import qualified Data.Set as S import UnliftIO.Async (forConcurrently) import qualified UnliftIO.Exception as E import UnliftIO.STM @@ -39,13 +41,11 @@ withGetLock getLock key name a = (atomically . takeTMVar) (const a) -withGetLocks :: MonadUnliftIO m => (k -> STM Lock) -> [k] -> String -> m a -> m a +withGetLocks :: MonadUnliftIO m => (k -> STM Lock) -> Set k -> String -> m a -> m a withGetLocks getLock keys name = E.bracket holdLocks releaseLocks . const where - holdLocks = forConcurrently keys $ \key -> atomically $ getPutLock getLock key name - -- only this withGetLocks would be holding the locks, - -- so it's safe to combine all lock releases into one transaction - releaseLocks = atomically . mapM_ takeTMVar + holdLocks = forConcurrently (S.toList keys) $ \key -> atomically $ getPutLock getLock key name + releaseLocks = mapM_ (atomically . takeTMVar) -- getLock and putTMVar can be in one transaction on the assumption that getLock doesn't write in case the lock already exists, -- and in case it is created and added to some shared resource (we use TMap) it also helps avoid contention for the newly created lock. diff --git a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs index ae0066328..1e2c7cb00 100644 --- a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs +++ b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs @@ -33,7 +33,6 @@ import Simplex.Messaging.Agent.Protocol (ACommand (..), APartyCmd (..), AgentErr import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.SQLite -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol (NtfSubStatus (..), NtfTknStatus (..), SMPQueueNtf (..)) import Simplex.Messaging.Notifications.Types @@ -161,7 +160,7 @@ runNtfWorker c srv Worker {doWork} = do logInfo $ "runNtfWorker, nextSub " <> tshow nextSub ri <- asks $ reconnectInterval . config withRetryInterval ri $ \_ loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c processSub nextSub `catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show) processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM () @@ -245,7 +244,7 @@ runNtfSMPWorker c srv Worker {doWork} = do logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub ri <- asks $ reconnectInterval . config withRetryInterval ri $ \_ loop -> do - lift $ waitForUserNetwork c + liftIO $ waitForUserNetwork c processSub nextSub `catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show) processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM () diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 4ee8d373f..e44c2c0f7 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -44,7 +44,7 @@ module Simplex.Messaging.Agent.Protocol currentSMPAgentVersion, supportedSMPAgentVRange, e2eEncConnInfoLength, - e2eEncUserMsgLength, + e2eEncAgentMsgLength, -- * SMP agent protocol types ConnInfo, @@ -108,6 +108,7 @@ module Simplex.Messaging.Agent.Protocol CRClientData, ServiceScheme, simplexChat, + connReqUriP', AgentErrorType (..), CommandErrorType (..), ConnectionErrorType (..), @@ -157,8 +158,8 @@ where import Control.Applicative (optional, (<|>)) import Control.Monad (unless) -import Control.Monad.Except (runExceptT, throwError) import Control.Monad.IO.Class +import Control.Monad.Trans.Except import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.TH as J import Data.Attoparsec.ByteString.Char8 (Parser) @@ -189,23 +190,25 @@ import Simplex.FileTransfer.Description import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Transport (XFTPErrorType) import Simplex.Messaging.Agent.QueryString +import Simplex.Messaging.Client (ProxyClientError) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet ( InitialKeys (..), PQEncryption (..), - pattern PQEncOff, PQSupport, - pattern PQSupportOn, - pattern PQSupportOff, RcvE2ERatchetParams, RcvE2ERatchetParamsUri, - SndE2ERatchetParams + SndE2ERatchetParams, + pattern PQEncOff, + pattern PQSupportOff, + pattern PQSupportOn, ) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers import Simplex.Messaging.Protocol ( AProtocolType, + BrokerErrorType (..), EntityId, ErrorType, MsgBody, @@ -213,14 +216,14 @@ import Simplex.Messaging.Protocol MsgId, NMsgMeta, ProtocolServer (..), + SMPClientVersion, SMPMsgMeta, SMPServer, SMPServerWithAuth, SndPublicAuthKey, SubscriptionMode, - SMPClientVersion, - VersionSMPC, VersionRangeSMPC, + VersionSMPC, initialSMPClientVersion, legacyEncodeServer, legacyServerP, @@ -233,7 +236,7 @@ import Simplex.Messaging.Protocol ) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme -import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP) +import Simplex.Messaging.Transport (Transport (..)) import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..)) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -272,16 +275,11 @@ deliveryRcptsSMPAgentVersion = VersionSMPA 4 pqdrSMPAgentVersion :: VersionSMPA pqdrSMPAgentVersion = VersionSMPA 5 --- TODO v5.7 increase to 5 currentSMPAgentVersion :: VersionSMPA -currentSMPAgentVersion = VersionSMPA 4 +currentSMPAgentVersion = VersionSMPA 5 --- TODO v5.7 remove dependency of version range on whether PQ support is needed -supportedSMPAgentVRange :: PQSupport -> VersionRangeSMPA -supportedSMPAgentVRange pq = - mkVersionRange duplexHandshakeSMPAgentVersion $ case pq of - PQSupportOn -> pqdrSMPAgentVersion - PQSupportOff -> currentSMPAgentVersion +supportedSMPAgentVRange :: VersionRangeSMPA +supportedSMPAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion currentSMPAgentVersion -- it is shorter to allow all handshake headers, -- including E2E (double-ratchet) parameters and @@ -292,8 +290,8 @@ e2eEncConnInfoLength v = \case PQSupportOn | v >= pqdrSMPAgentVersion -> 11122 _ -> 14848 -e2eEncUserMsgLength :: VersionSMPA -> PQSupport -> Int -e2eEncUserMsgLength v = \case +e2eEncAgentMsgLength :: VersionSMPA -> PQSupport -> Int +e2eEncAgentMsgLength v = \case -- reduced by 2222 (the increase of message ratchet header size) PQSupportOn | v >= pqdrSMPAgentVersion -> 13634 _ -> 15856 @@ -398,7 +396,8 @@ data ACommand (p :: AParty) (e :: AEntity) where RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> ACommand Agent AEConn SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn - SENT :: AgentMsgId -> ACommand Agent AEConn + SENT :: AgentMsgId -> Maybe SMPServer -> ACommand Agent AEConn + MWARN :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn @@ -462,6 +461,7 @@ data ACommandTag (p :: AParty) (e :: AEntity) where SEND_ :: ACommandTag Client AEConn MID_ :: ACommandTag Agent AEConn SENT_ :: ACommandTag Agent AEConn + MWARN_ :: ACommandTag Agent AEConn MERR_ :: ACommandTag Agent AEConn MERRS_ :: ACommandTag Agent AEConn MSG_ :: ACommandTag Agent AEConn @@ -517,7 +517,8 @@ aCommandTag = \case RSYNC {} -> RSYNC_ SEND {} -> SEND_ MID {} -> MID_ - SENT _ -> SENT_ + SENT {} -> SENT_ + MWARN {} -> MWARN_ MERR {} -> MERR_ MERRS {} -> MERRS_ MSG {} -> MSG_ @@ -913,7 +914,7 @@ instance Encoding AgentMsgEnvelope where -- AgentRatchetInfo is not encrypted with double ratchet, but with per-queue E2E encryption data AgentMessage = -- used by the initiating party when confirming reply queue - AgentConnInfo ConnInfo + AgentConnInfo ConnInfo | -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation. -- It made removed REPLY message unnecessary. AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo @@ -1161,7 +1162,7 @@ instance StrEncoding MsgReceipt where msgRcptStatus <- strP pure MsgReceipt {agentMsgId, msgRcptStatus} -instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where +instance ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where strEncode = \case CRInvitationUri crData e2eParams -> crEncode "invitation" crData (Just e2eParams) CRContactUri crData -> crEncode "contact" crData Nothing @@ -1175,34 +1176,40 @@ instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) whe [("v", strEncode crAgentVRange), ("smp", strEncode crSmpQueues)] <> maybe [] (\e2e -> [("e2e", strEncode e2e)]) e2eParams <> maybe [] (\cd -> [("data", encodeUtf8 cd)]) crClientData - strP = do - ACR m cr <- strP - case testEquality m $ sConnectionMode @m of - Just Refl -> pure cr - _ -> fail "bad connection request mode" + strP = connReqUriP' (Just SSSimplex) + +connReqUriP' :: forall m. ConnectionModeI m => Maybe ServiceScheme -> Parser (ConnectionRequestUri m) +connReqUriP' overrideScheme = do + ACR m cr <- connReqUriP overrideScheme + case testEquality m $ sConnectionMode @m of + Just Refl -> pure cr + _ -> fail "bad connection request mode" instance StrEncoding AConnectionRequestUri where strEncode (ACR _ cr) = strEncode cr - strP = do - _crScheme :: ServiceScheme <- strP - crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?" - query <- strP - aVRange <- queryParam "v" query - crSmpQueues <- queryParam "smp" query - let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query - let crData = ConnReqUriData {crScheme = SSSimplex, crAgentVRange = aVRange, crSmpQueues, crClientData} - case crMode of - CMInvitation -> do - crE2eParams <- queryParam "e2e" query - pure . ACR SCMInvitation $ CRInvitationUri crData crE2eParams - -- contact links are adjusted to the minimum version supported by the agent - -- to preserve compatibility with the old links published online - CMContact -> pure . ACR SCMContact $ CRContactUri crData {crAgentVRange = adjustAgentVRange aVRange} - where - crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact - adjustAgentVRange vr = - let v = max duplexHandshakeSMPAgentVersion $ minVersion vr - in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) + strP = connReqUriP (Just SSSimplex) + +connReqUriP :: Maybe ServiceScheme -> Parser AConnectionRequestUri +connReqUriP overrideScheme = do + crScheme <- (`fromMaybe` overrideScheme) <$> strP + crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?" + query <- strP + aVRange <- queryParam "v" query + crSmpQueues <- queryParam "smp" query + let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query + crData = ConnReqUriData {crScheme, crAgentVRange = aVRange, crSmpQueues, crClientData} + case crMode of + CMInvitation -> do + crE2eParams <- queryParam "e2e" query + pure . ACR SCMInvitation $ CRInvitationUri crData crE2eParams + -- contact links are adjusted to the minimum version supported by the agent + -- to preserve compatibility with the old links published online + CMContact -> pure . ACR SCMContact $ CRContactUri crData {crAgentVRange = adjustAgentVRange aVRange} + where + crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact + adjustAgentVRange vr = + let v = max duplexHandshakeSMPAgentVersion $ minVersion vr + in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr) instance ConnectionModeI m => FromJSON (ConnectionRequestUri m) where parseJSON = strParseJSON "ConnectionRequestUri" @@ -1305,6 +1312,7 @@ instance VersionRangeI SMPClientVersion SMPQueueUri where type VersionT SMPClientVersion SMPQueueUri = SMPQueueInfo versionRange = clientVRange toVersionT (SMPQueueUri _vr addr) v = SMPQueueInfo v addr + toVersionRange (SMPQueueUri _vr addr) vr = SMPQueueUri vr addr -- | SMP queue information sent out-of-band. -- @@ -1387,9 +1395,9 @@ deriving instance Show (ConnectionRequestUri m) data AConnectionRequestUri = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequestUri m) instance Eq AConnectionRequestUri where - ACR m cr == ACR m' cr' = case testEquality m m' of - Just Refl -> cr == cr' - _ -> False + ACR m cr == ACR m' cr' = case testEquality m m' of + Just Refl -> cr == cr' + _ -> False deriving instance Show AConnectionRequestUri @@ -1470,15 +1478,17 @@ instance StrEncoding MsgErrorType where -- | Error type used in errors sent to agent clients. data AgentErrorType = -- | command or response error - CMD {cmdErr :: CommandErrorType} + CMD {cmdErr :: CommandErrorType, errContext :: String} | -- | connection errors CONN {connErr :: ConnectionErrorType} | -- | SMP protocol errors forwarded to agent clients - SMP {smpErr :: ErrorType} + SMP {serverAddress :: String, smpErr :: ErrorType} | -- | NTF protocol errors forwarded to agent clients - NTF {ntfErr :: ErrorType} + NTF {serverAddress :: String, ntfErr :: ErrorType} | -- | XFTP protocol errors forwarded to agent clients - XFTP {xftpErr :: XFTPErrorType} + XFTP {serverAddress :: String, xftpErr :: XFTPErrorType} + | -- | SMP proxy errors + PROXY {proxyServer :: String, relayServer :: String, proxyErr :: ProxyClientError} | -- | XRCP protocol errors forwarded to agent clients RCP {rcpErr :: RCErrorType} | -- | SMP server errors @@ -1521,28 +1531,12 @@ data ConnectionErrorType NOT_AVAILABLE deriving (Eq, Read, Show, Exception) --- | SMP server errors. -data BrokerErrorType - = -- | invalid server response (failed to parse) - RESPONSE {smpErr :: String} - | -- | unexpected response - UNEXPECTED - | -- | network error - NETWORK - | -- | no compatible server host (e.g. onion when public is required, or vice versa) - HOST - | -- | handshake or other transport error - TRANSPORT {transportErr :: TransportError} - | -- | command response timeout - TIMEOUT - deriving (Eq, Read, Show, Exception) - -- | Errors of another SMP agent. data SMPAgentError = -- | client or agent message that failed to parse A_MESSAGE | -- | prohibited SMP/agent message - A_PROHIBITED + A_PROHIBITED {prohibitedErr :: String} | -- | incompatible version of SMP client, agent or encryption protocols A_VERSION | -- | cannot decrypt message @@ -1571,12 +1565,14 @@ data AgentCryptoError instance StrEncoding AgentCryptoError where strP = - "DECRYPT_AES" $> DECRYPT_AES - <|> "DECRYPT_CB" $> DECRYPT_CB - <|> "RATCHET_HEADER" $> RATCHET_HEADER - <|> "RATCHET_EARLIER " *> (RATCHET_EARLIER <$> strP) - <|> "RATCHET_SKIPPED " *> (RATCHET_SKIPPED <$> strP) - <|> "RATCHET_SYNC" $> RATCHET_SYNC + A.takeTill (== ' ') >>= \case + "DECRYPT_AES" -> pure DECRYPT_AES + "DECRYPT_CB" -> pure DECRYPT_CB + "RATCHET_HEADER" -> pure RATCHET_HEADER + "RATCHET_EARLIER" -> RATCHET_EARLIER <$> _strP + "RATCHET_SKIPPED" -> RATCHET_SKIPPED <$> _strP + "RATCHET_SYNC" -> pure RATCHET_SYNC + _ -> fail "AgentCryptoError" strEncode = \case DECRYPT_AES -> "DECRYPT_AES" DECRYPT_CB -> "DECRYPT_CB" @@ -1587,42 +1583,61 @@ instance StrEncoding AgentCryptoError where instance StrEncoding AgentErrorType where strP = - "CMD " *> (CMD <$> parseRead1) - <|> "CONN " *> (CONN <$> parseRead1) - <|> "SMP " *> (SMP <$> strP) - <|> "NTF " *> (NTF <$> strP) - <|> "XFTP " *> (XFTP <$> strP) - <|> "RCP " *> (RCP <$> strP) - <|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP)) - <|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP)) - <|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1) - <|> "AGENT CRYPTO " *> (AGENT . A_CRYPTO <$> parseRead A.takeByteString) - <|> "AGENT QUEUE " *> (AGENT . A_QUEUE <$> parseRead A.takeByteString) - <|> "AGENT " *> (AGENT <$> parseRead1) - <|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString) - <|> "CRITICAL " *> (CRITICAL <$> parseRead1 <* A.space <*> parseRead A.takeByteString) - <|> "INACTIVE" $> INACTIVE + A.takeTill (== ' ') + >>= \case + "CMD" -> CMD <$> (A.space *> parseRead1) <*> (A.space *> textP) + "CONN" -> CONN <$> (A.space *> parseRead1) + "SMP" -> SMP <$> (A.space *> srvP) <*> _strP + "NTF" -> NTF <$> (A.space *> srvP) <*> _strP + "XFTP" -> XFTP <$> (A.space *> srvP) <*> _strP + "PROXY" -> PROXY <$> (A.space *> srvP) <* A.space <*> srvP <*> _strP + "RCP" -> RCP <$> _strP + "BROKER" -> BROKER <$> (A.space *> srvP) <*> _strP + "AGENT" -> AGENT <$> _strP + "INTERNAL" -> INTERNAL <$> (A.space *> textP) + "CRITICAL" -> CRITICAL <$> (A.space *> parseRead1) <*> (A.space *> textP) + "INACTIVE" -> pure INACTIVE + _ -> fail "bad AgentErrorType" where - textP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') + srvP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ') strEncode = \case - CMD e -> "CMD " <> bshow e + CMD e cxt -> "CMD " <> bshow e <> " " <> text cxt CONN e -> "CONN " <> bshow e - SMP e -> "SMP " <> strEncode e - NTF e -> "NTF " <> strEncode e - XFTP e -> "XFTP " <> strEncode e + SMP srv e -> "SMP " <> text srv <> " " <> strEncode e + NTF srv e -> "NTF " <> text srv <> " " <> strEncode e + XFTP srv e -> "XFTP " <> text srv <> " " <> strEncode e + PROXY pxy srv e -> B.unwords ["PROXY", text pxy, text srv, strEncode e] RCP e -> "RCP " <> strEncode e - BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e - BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e - BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e - AGENT (A_CRYPTO e) -> "AGENT CRYPTO " <> bshow e - AGENT (A_QUEUE e) -> "AGENT QUEUE " <> bshow e - AGENT e -> "AGENT " <> bshow e - INTERNAL e -> "INTERNAL " <> bshow e - CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> bshow e + BROKER srv e -> B.unwords ["BROKER", text srv, strEncode e] + AGENT e -> "AGENT " <> strEncode e + INTERNAL e -> "INTERNAL " <> encodeUtf8 (T.pack e) + CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> encodeUtf8 (T.pack e) INACTIVE -> "INACTIVE" where text = encodeUtf8 . T.pack +instance StrEncoding SMPAgentError where + strP = + A.takeTill (== ' ') + >>= \case + "MESSAGE" -> pure A_MESSAGE + "PROHIBITED" -> A_PROHIBITED <$> (A.space *> textP) + "VERSION" -> pure A_VERSION + "CRYPTO" -> A_CRYPTO <$> _strP + "DUPLICATE" -> pure A_DUPLICATE + "QUEUE" -> A_QUEUE <$> (A.space *> textP) + _ -> fail "bad SMPAgentError" + strEncode = \case + A_MESSAGE -> "MESSAGE" + A_PROHIBITED e -> "PROHIBITED " <> encodeUtf8 (T.pack e) + A_VERSION -> "VERSION" + A_CRYPTO e -> "CRYPTO " <> strEncode e + A_DUPLICATE -> "DUPLICATE" + A_QUEUE e -> "QUEUE " <> encodeUtf8 (T.pack e) + +textP :: Parser String +textP = T.unpack . safeDecodeUtf8 <$> A.takeByteString + cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState cryptoErrToSyncState = \case DECRYPT_AES -> RSAllowed @@ -1665,6 +1680,7 @@ instance StrEncoding ACmdTag where "SEND" -> t SEND_ "MID" -> ct MID_ "SENT" -> ct SENT_ + "MWARN" -> ct MWARN_ "MERR" -> ct MERR_ "MERRS" -> ct MERRS_ "MSG" -> ct MSG_ @@ -1723,6 +1739,7 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where SEND_ -> "SEND" MID_ -> "MID" SENT_ -> "SENT" + MWARN_ -> "MWARN" MERR_ -> "MERR" MERRS_ -> "MERRS" MSG_ -> "MSG" @@ -1793,7 +1810,8 @@ commandP binaryP = SWITCH_ -> s (SWITCH <$> strP_ <*> strP_ <*> strP) RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP) MID_ -> s (MID <$> A.decimal <*> _strP) - SENT_ -> s (SENT <$> A.decimal) + SENT_ -> s (SENT <$> A.decimal <*> _strP) + MWARN_ -> s (MWARN <$> A.decimal <* A.space <*> strP) MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP) MERRS_ -> s (MERRS <$> strP_ <*> strP) MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP) @@ -1832,7 +1850,7 @@ commandP binaryP = sd : rds -> SFDONE <$> strDecode (encodeUtf8 sd) <*> mapM (strDecode . encodeUtf8) rds parseCommand :: ByteString -> Either AgentErrorType ACmd -parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX +parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX "parseCommand" -- | Serialize SMP agent command. serializeCommand :: ACommand p e -> ByteString @@ -1856,7 +1874,8 @@ serializeCommand = \case RSYNC rrState cryptoErr cstats -> s (RSYNC_, rrState, cryptoErr, cstats) SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody] MID mId pqEnc -> s (MID_, mId, pqEnc) - SENT mId -> s (SENT_, mId) + SENT mId proxySrv_ -> s (SENT_, mId, proxySrv_) + MWARN mId e -> s (MWARN_, mId, e) MERR mId e -> s (MERR_, mId, e) MERRS mIds e -> s (MERRS_, mIds, e) MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody] @@ -1921,7 +1940,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody fromParty :: ACmd -> Either AgentErrorType (APartyCmd p) fromParty (ACmd (p :: p1) e cmd) = case testEquality party p of Just Refl -> Right $ APC e cmd - _ -> Left $ CMD PROHIBITED + _ -> Left $ CMD PROHIBITED "fromParty" tConnId :: ARawTransmission -> APartyCmd p -> Either AgentErrorType (APartyCmd p) tConnId (_, entId, _) (APC e cmd) = @@ -1939,7 +1958,7 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody SUSPENDED {} -> Right cmd -- other responses must have connection ID _ - | B.null entId -> Left $ CMD NO_CONN + | B.null entId -> Left $ CMD NO_CONN "tConnId" | otherwise -> Right cmd cmdWithMsgBody :: APartyCmd p -> IO (Either AgentErrorType (APartyCmd p)) @@ -1962,11 +1981,11 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody str -> case readMaybe str :: Maybe Int of Just size -> runExceptT $ do body <- liftIO $ cGet h size - unless (B.length body == size) $ throwError $ CMD SIZE + unless (B.length body == size) $ throwE $ CMD SIZE "getBody" s <- liftIO $ getLn h - unless (B.null s) $ throwError $ CMD SIZE + unless (B.null s) $ throwE $ CMD SIZE "getBody" pure body - Nothing -> return . Left $ CMD SYNTAX + Nothing -> pure . Left $ CMD SYNTAX "getBody" $(J.deriveJSON defaultJSON ''RcvQueueInfo) @@ -1982,8 +2001,6 @@ $(J.deriveJSON (sumTypeJSON id) ''CommandErrorType) $(J.deriveJSON (sumTypeJSON id) ''ConnectionErrorType) -$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) - $(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError) $(J.deriveJSON (sumTypeJSON id) ''SMPAgentError) diff --git a/src/Simplex/Messaging/Agent/Server.hs b/src/Simplex/Messaging/Agent/Server.hs index 368c0a23d..da87fde11 100644 --- a/src/Simplex/Messaging/Agent/Server.hs +++ b/src/Simplex/Messaging/Agent/Server.hs @@ -49,7 +49,7 @@ runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, smpAgent :: forall c. Transport c => TProxy c -> ServiceName -> Env -> IO () smpAgent _ port env = do -- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation - tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile + tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile Nothing clientId <- newTVarIO initClientId runTransportServer started port tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index ce76d5c89..b3decd8f0 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -30,7 +30,7 @@ import Data.Type.Equality import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval (RI2State) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (RatchetX448, PQEncryption, PQSupport) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport, RatchetX448) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol ( MsgBody, @@ -593,6 +593,8 @@ type AsyncCmdId = Int64 data StoreError = -- | IO exceptions in store actions. SEInternal ByteString + | -- | Database busy + SEDatabaseBusy ByteString | -- | Failed to generate unique random ID SEUniqueID | -- | User ID not found diff --git a/src/Simplex/Messaging/Agent/Store/SQLite.hs b/src/Simplex/Messaging/Agent/Store/SQLite.hs index b8b1c7c52..6c2c5906d 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite.hs @@ -221,6 +221,7 @@ module Simplex.Messaging.Agent.Store.SQLite ) where +import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class @@ -268,7 +269,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations (DownMigration (..), MTRE import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) -import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys, PQEncryption (..), PQSupport (..)) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -278,7 +279,7 @@ import Simplex.Messaging.Parsers (blobFieldParser, defaultJSON, dropPrefix, from import Simplex.Messaging.Protocol import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost) -import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, ($>>=), (<$$>)) +import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, tshow, ($>>=), (<$$>)) import Simplex.Messaging.Version.Internal import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist) import System.Exit (exitFailure) @@ -286,6 +287,7 @@ import System.FilePath (takeDirectory) import System.IO (hFlush, stdout) import UnliftIO.Exception (bracketOnError, onException) import qualified UnliftIO.Exception as E +import UnliftIO.MVar import UnliftIO.STM -- * SQLite Store implementation @@ -381,8 +383,8 @@ connectSQLiteStore :: FilePath -> ScrubbedBytes -> Bool -> IO SQLiteStore connectSQLiteStore dbFilePath key keepKey = do dbNew <- not <$> doesFileExist dbFilePath dbConn <- dbBusyLoop (connectDB dbFilePath key) + dbConnection <- newMVar dbConn atomically $ do - dbConnection <- newTMVar dbConn dbKey <- newTVar $! storeKey key keepKey dbClosed <- newTVar False pure SQLiteStore {dbFilePath, dbKey, dbConnection, dbNew, dbClosed} @@ -420,14 +422,14 @@ openSQLiteStore st@SQLiteStore {dbClosed} key keepKey = openSQLiteStore_ :: SQLiteStore -> ScrubbedBytes -> Bool -> IO () openSQLiteStore_ SQLiteStore {dbConnection, dbFilePath, dbKey, dbClosed} key keepKey = bracketOnError - (atomically $ takeTMVar dbConnection) - (atomically . tryPutTMVar dbConnection) + (takeMVar dbConnection) + (tryPutMVar dbConnection) $ \DB.Connection {slow} -> do DB.Connection {conn} <- connectDB dbFilePath key atomically $ do - putTMVar dbConnection DB.Connection {conn, slow} writeTVar dbClosed False writeTVar dbKey $! storeKey key keepKey + putMVar dbConnection DB.Connection {conn, slow} reopenSQLiteStore :: SQLiteStore -> IO () reopenSQLiteStore st@SQLiteStore {dbKey, dbClosed} = @@ -1272,12 +1274,16 @@ createCommand :: DB.Connection -> ACorrId -> ConnId -> Maybe SMPServer -> AgentC createCommand db corrId connId srv_ cmd = runExceptT $ do (host_, port_, serverKeyHash_) <- serverFields createdAt <- liftIO getCurrentTime - liftIO $ + liftIO . E.handle handleErr $ DB.execute db "INSERT INTO commands (host, port, corr_id, conn_id, command_tag, command, server_key_hash, created_at) VALUES (?,?,?,?,?,?,?,?)" - (host_, port_, corrId, connId, agentCommandTag cmd, cmd, serverKeyHash_, createdAt) + (host_, port_, corrId, connId, cmdTag, cmd, serverKeyHash_, createdAt) where + cmdTag = agentCommandTag cmd + handleErr e + | SQL.sqlError e == SQL.ErrorConstraint = logError $ "tried to create command " <> tshow cmdTag <> " for deleted connection" + | otherwise = E.throwIO e serverFields :: ExceptT StoreError IO (Maybe (NonEmpty TransportHost), Maybe ServiceName, Maybe C.KeyHash) serverFields = case srv_ of Just srv@(SMPServer host port _) -> @@ -2269,20 +2275,20 @@ getXFTPServerId_ db ProtocolServer {host, port, keyHash} = do firstRow fromOnly SEXFTPServerNotFound $ DB.query db "SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ?" (host, port, keyHash) -createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId) -createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file = runExceptT $ do - (rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing +createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId) +createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file approvedRelays = runExceptT $ do + (rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing approvedRelays liftIO $ forM_ chunks $ \fc@FileChunk {replicas} -> do chunkId <- insertRcvFileChunk db fc rcvFileId forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId pure rcvFileEntityId -createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId) -createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect" -createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile = runExceptT $ do - (dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing - (_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId) +createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> Bool -> IO (Either StoreError RcvFileId) +createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect" +createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile approvedRelays = runExceptT $ do + (dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing approvedRelays + (_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId) approvedRelays liftIO $ forM_ redirectChunks $ \fc@FileChunk {replicas} -> do chunkId <- insertRcvFileChunk db fc redirectId @@ -2302,8 +2308,8 @@ createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redire chunks = [] } -insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> IO (Either StoreError (RcvFileId, DBRcvFileId)) -insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ = runExceptT $ do +insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> Bool -> IO (Either StoreError (RcvFileId, DBRcvFileId)) +insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ approvedRelays = runExceptT $ do let (redirectDigest_, redirectSize_) = case redirect of Just RedirectFileInfo {digest = d, size = s} -> (Just d, Just s) Nothing -> (Nothing, Nothing) @@ -2311,8 +2317,8 @@ insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSiz createWithRandomId gVar $ \rcvFileEntityId -> DB.execute db - "INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_)) + "INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_, approvedRelays)) rcvFileId <- liftIO $ insertedRowId db pure (rcvFileEntityId, rcvFileId) @@ -2462,7 +2468,7 @@ deleteRcvFile' :: DB.Connection -> DBRcvFileId -> IO () deleteRcvFile' db rcvFileId = DB.execute db "DELETE FROM rcv_files WHERE rcv_file_id = ?" (Only rcvFileId) -getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFileChunk)) +getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe (RcvFileChunk, Bool))) getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = do getWorkItem "rcv_file_download" getReplicaId getChunkData (markRcvFileFailed db . snd) where @@ -2486,7 +2492,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d LIMIT 1 |] (host, port, keyHash, RFSReceiving, cutoffTs) - getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError RcvFileChunk) + getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError (RcvFileChunk, Bool)) getChunkData (rcvFileChunkReplicaId, _fileId) = firstRow toChunk SEFileNotFound $ DB.query @@ -2494,7 +2500,8 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d [sql| SELECT f.rcv_file_id, f.rcv_file_entity_id, f.user_id, c.rcv_file_chunk_id, c.chunk_no, c.chunk_size, c.digest, f.tmp_path, c.tmp_path, - r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries + r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries, + f.approved_relays FROM rcv_file_chunk_replicas r JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id @@ -2503,20 +2510,22 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d |] (Only rcvFileChunkReplicaId) where - toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int)) -> RcvFileChunk - toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries)) = - RcvFileChunk - { rcvFileId, - rcvFileEntityId, - userId, - rcvChunkId, - chunkNo, - chunkSize, - digest, - fileTmpPath, - chunkTmpPath, - replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}] - } + toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int) :. Only Bool) -> (RcvFileChunk, Bool) + toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries) :. (Only approvedRelays)) = + ( RcvFileChunk + { rcvFileId, + rcvFileEntityId, + userId, + rcvChunkId, + chunkNo, + chunkSize, + digest, + fileTmpPath, + chunkTmpPath, + replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}] + }, + approvedRelays + ) getNextRcvFileToDecrypt :: DB.Connection -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFile)) getNextRcvFileToDecrypt db ttl = diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs index 18c16cc8b..b9a9bd501 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Common.hs @@ -22,8 +22,8 @@ import Database.SQLite.Simple (SQLError) import qualified Database.SQLite.Simple as SQL import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Util (diffToMilliseconds) -import UnliftIO.Exception (bracket) import qualified UnliftIO.Exception as E +import UnliftIO.MVar import UnliftIO.STM storeKey :: ScrubbedBytes -> Bool -> Maybe ScrubbedBytes @@ -32,16 +32,13 @@ storeKey key keepKey = if keepKey || BA.null key then Just key else Nothing data SQLiteStore = SQLiteStore { dbFilePath :: FilePath, dbKey :: TVar (Maybe ScrubbedBytes), - dbConnection :: TMVar DB.Connection, + dbConnection :: MVar DB.Connection, dbClosed :: TVar Bool, dbNew :: Bool } withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a -withConnection SQLiteStore {dbConnection} = - bracket - (atomically $ takeTMVar dbConnection) - (atomically . putTMVar dbConnection) +withConnection SQLiteStore {dbConnection} = withMVar dbConnection withConnection' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a withConnection' st action = withConnection st $ action . DB.conn @@ -71,9 +68,9 @@ dbBusyLoop action = loop 500 3000000 loop :: Int -> Int -> IO a loop t tLim = action `E.catch` \(e :: SQLError) -> - let se = SQL.sqlError e in - if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked) - then do - threadDelay t - loop (t * 9 `div` 8) (tLim - t) - else E.throwIO e + let se = SQL.sqlError e + in if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked) + then do + threadDelay t + loop (t * 9 `div` 8) (tLim - t) + else E.throwIO e diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs index 344a3f9ce..5a5ed5b5b 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs @@ -71,6 +71,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_deliver import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem +import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Transport.Client (TransportHost) @@ -110,7 +111,8 @@ schemaMigrations = ("m20240121_message_delivery_indexes", m20240121_message_delivery_indexes, Just down_m20240121_message_delivery_indexes), ("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect), ("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery), - ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem) + ("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem), + ("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs new file mode 100644 index 000000000..9eb10c27a --- /dev/null +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20240417_rcv_files_approved_relays.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240417_rcv_files_approved_relays :: Query +m20240417_rcv_files_approved_relays = + [sql| +ALTER TABLE rcv_files ADD COLUMN approved_relays INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240417_rcv_files_approved_relays :: Query +down_m20240417_rcv_files_approved_relays = + [sql| +ALTER TABLE rcv_files DROP COLUMN approved_relays; +|] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index 0818be904..caf94418a 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -287,6 +287,7 @@ CREATE TABLE rcv_files( redirect_entity_id BLOB, redirect_size INTEGER, redirect_digest BLOB, + approved_relays INTEGER NOT NULL DEFAULT 0, UNIQUE(rcv_file_entity_id) ); CREATE TABLE rcv_file_chunks( diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index e0591b14d..56ebf7a3f 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -30,9 +30,11 @@ module Simplex.Messaging.Client TransportSession, ProtocolClient (thParams, sessionTs), SMPClient, + ProxiedRelay (..), getProtocolClient, closeProtocolClient, protocolClientServer, + protocolClientServer', transportHost', transportSession', @@ -54,14 +56,24 @@ module Simplex.Messaging.Client suspendSMPQueue, deleteSMPQueue, deleteSMPQueues, + connectSMPProxiedRelay, + proxySMPMessage, + forwardSMPMessage, + getSMPQueueInfo, sendProtocolCommand, -- * Supporting types and client configuration ProtocolClientError (..), SMPClientError, + ProxyClientError (..), + unexpectedResponse, ProtocolClientConfig (..), NetworkConfig (..), TransportSessionMode (..), + HostMode (..), + SocksMode (..), + SMPProxyMode (..), + SMPProxyFallback (..), defaultClientConfig, defaultSMPClientConfig, defaultNetworkConfig, @@ -69,7 +81,9 @@ module Simplex.Messaging.Client chooseTransportHost, proxyUsername, temporaryClientError, - ServerTransmission, + smpProxyError, + ServerTransmissionBatch, + ServerTransmission (..), ClientCommand, -- * For testing @@ -77,18 +91,26 @@ module Simplex.Messaging.Client mkTransmission, authTransmission, smpClientStub, + + -- * For debugging + TBQueueInfo (..), + getTBQueueInfo, + getProtocolClientQueuesInfo, ) where +import Control.Applicative ((<|>)) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception +import Control.Logger.Simple import Control.Monad -import Control.Monad.IO.Class (liftIO) import Control.Monad.Except +import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J +import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -96,21 +118,25 @@ import Data.Int (Int64) import Data.List (find) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) -import Data.Time.Clock (UTCTime (..), getCurrentTime) +import Data.Maybe (catMaybes, fromMaybe) +import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime) +import qualified Data.X509 as X +import qualified Data.X509.Validation as XV import Network.Socket (ServiceName) import Numeric.Natural import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Protocol +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 (SocksProxy, TransportClientConfig (..), TransportHost (..), runTransportClient) +import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient) import Simplex.Messaging.Transport.KeepAlive import Simplex.Messaging.Transport.WebSockets (WS) -import Simplex.Messaging.Util (bshow, raceAny_, threadDelay') +import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM) import Simplex.Messaging.Version import System.Timeout (timeout) @@ -119,7 +145,7 @@ import System.Timeout (timeout) -- Use 'getSMPClient' to connect to an SMP server and create a client handle. data ProtocolClient v err msg = ProtocolClient { action :: Maybe (Async ()), - thParams :: THandleParams v, + thParams :: THandleParams v 'TClient, sessionTs :: UTCTime, client_ :: PClient v err msg } @@ -128,22 +154,27 @@ data PClient v err msg = PClient { connected :: TVar Bool, transportSession :: TransportSession msg, transportHost :: TransportHost, + tcpConnectTimeout :: Int, tcpTimeout :: Int, - batchDelay :: Maybe Int, - pingErrorCount :: TVar Int, + sendPings :: TVar Bool, + lastReceived :: TVar UTCTime, + timeoutErrorCount :: TVar Int, clientCorrId :: TVar ChaChaDRG, sentCommands :: TMap CorrId (Request err msg), - sndQ :: TBQueue ByteString, + sndQ :: TBQueue (Maybe (TVar Bool), ByteString), rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)), - msgQ :: Maybe (TBQueue (ServerTransmission v msg)) + msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg)) } -smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe THandleAuth -> STM SMPClient +smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient smpClientStub g sessionId thVersion thAuth = do + let ts = UTCTime (read "2024-03-31") 0 connected <- newTVar False clientCorrId <- C.newRandomDRG g sentCommands <- TM.empty - pingErrorCount <- newTVar 0 + sendPings <- newTVar False + lastReceived <- newTVar ts + timeoutErrorCount <- newTVar 0 sndQ <- newTBQueue 100 rcvQ <- newTBQueue 100 return @@ -153,20 +184,23 @@ smpClientStub g sessionId thVersion thAuth = do THandleParams { sessionId, thVersion, + thServerVRange = supportedServerSMPRelayVRange, thAuth, blockSize = smpBlockSize, implySessId = thVersion >= authCmdsSMPVersion, batch = True }, - sessionTs = UTCTime (read "2024-03-31") 0, + sessionTs = ts, client_ = PClient { connected, transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing), transportHost = "localhost", + tcpConnectTimeout = 20_000_000, tcpTimeout = 15_000_000, - batchDelay = Nothing, - pingErrorCount, + sendPings, + lastReceived, + timeoutErrorCount, clientCorrId, sentCommands, sndQ, @@ -180,8 +214,14 @@ type SMPClient = ProtocolClient SMPVersion ErrorType BrokerMsg -- | Type for client command data type ClientCommand msg = (Maybe C.APrivateAuthKey, EntityId, ProtoCommand msg) --- | Type synonym for transmission from some SPM server queue. -type ServerTransmission v msg = (TransportSession msg, Version v, SessionId, EntityId, msg) +-- | Type synonym for transmission from SPM servers. +-- Batch response is presented as a single `ServerTransmissionBatch` tuple. +type ServerTransmissionBatch v err msg = (TransportSession msg, Version v, SessionId, NonEmpty (EntityId, ServerTransmission err msg)) + +data ServerTransmission err msg + = STEvent (Either (ProtocolClientError err) msg) + | STResponse (ProtoCommand msg) (Either (ProtocolClientError err) msg) + | STUnexpectedError (ProtocolClientError err) data HostMode = -- | prefer (or require) onion hosts when connecting via SOCKS proxy @@ -192,27 +232,43 @@ data HostMode HMPublic deriving (Eq, Show) +data SocksMode + = -- | always use SOCKS proxy when enabled + SMAlways + | -- | use SOCKS proxy only for .onion hosts when no public host is available + -- This mode is used in SMP proxy to minimize SOCKS proxy usage. + SMOnion + deriving (Eq, Show) + -- | network configuration for the client data NetworkConfig = NetworkConfig { -- | use SOCKS5 proxy socksProxy :: Maybe SocksProxy, + -- | when to use SOCKS proxy + socksMode :: SocksMode, -- | determines critera which host is chosen from the list hostMode :: HostMode, -- | if above criteria is not met, if the below setting is True return error, otherwise use the first host requiredHostMode :: Bool, -- | transport sessions are created per user or per entity sessionMode :: TransportSessionMode, + -- | SMP proxy mode + smpProxyMode :: SMPProxyMode, + -- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions + smpProxyFallback :: SMPProxyFallback, -- | timeout for the initial client TCP/TLS connection (microseconds) tcpConnectTimeout :: Int, -- | timeout of protocol commands (microseconds) tcpTimeout :: Int, -- | additional timeout per kilobyte (1024 bytes) to be sent tcpTimeoutPerKb :: Int64, + -- | break response timeouts into groups, so later responses get later deadlines + rcvConcurrency :: Int, -- | TCP keep-alive options, Nothing to skip enabling keep-alive tcpKeepAlive :: Maybe KeepAliveOpts, -- | period for SMP ping commands (microseconds, 0 to disable) smpPingInterval :: Int64, - -- | the count of PING errors after which SMP client terminates (and will be reconnected), 0 to disable + -- | the count of timeout errors after which SMP client terminates (and will be reconnected), 0 to disable smpPingCount :: Int, logTLSErrors :: Bool } @@ -221,25 +277,74 @@ data NetworkConfig = NetworkConfig data TransportSessionMode = TSMUser | TSMEntity deriving (Eq, Show) +-- SMP proxy mode for sending messages +data SMPProxyMode + = SPMAlways + | SPMUnknown -- use with unknown relays + | SPMUnprotected -- use with unknown relays when IP address is not protected (i.e., when neither SOCKS proxy nor .onion address is used) + | SPMNever + deriving (Eq, Show) + +data SMPProxyFallback + = SPFAllow -- connect directly when chosen proxy or destination relay do not support proxy protocol. + | SPFAllowProtected -- connect directly only when IP address is protected (SOCKS proxy or .onion address is used). + | SPFProhibit -- prohibit direct connection to destination relay. + deriving (Eq, Show) + +instance StrEncoding SMPProxyMode where + strEncode = \case + SPMAlways -> "always" + SPMUnknown -> "unknown" + SPMUnprotected -> "unprotected" + SPMNever -> "never" + strP = + A.takeTill (== ' ') >>= \case + "always" -> pure SPMAlways + "unknown" -> pure SPMUnknown + "unprotected" -> pure SPMUnprotected + "never" -> pure SPMNever + _ -> fail "Invalid SMP proxy mode" + +instance StrEncoding SMPProxyFallback where + strEncode = \case + SPFAllow -> "yes" + SPFAllowProtected -> "protected" + SPFProhibit -> "no" + strP = + A.takeTill (== ' ') >>= \case + "yes" -> pure SPFAllow + "protected" -> pure SPFAllowProtected + "no" -> pure SPFProhibit + _ -> fail "Invalid SMP proxy fallback mode" + defaultNetworkConfig :: NetworkConfig defaultNetworkConfig = NetworkConfig { socksProxy = Nothing, + socksMode = SMAlways, hostMode = HMOnionViaSocks, requiredHostMode = False, sessionMode = TSMUser, - tcpConnectTimeout = 20_000_000, + smpProxyMode = SPMNever, + smpProxyFallback = SPFAllow, + tcpConnectTimeout = defaultTcpConnectTimeout, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 5_000, + rcvConcurrency = 8, tcpKeepAlive = Just defaultKeepAliveOpts, smpPingInterval = 600_000_000, -- 10min smpPingCount = 3, logTLSErrors = False } -transportClientConfig :: NetworkConfig -> TransportClientConfig -transportClientConfig NetworkConfig {socksProxy, tcpKeepAlive, logTLSErrors} = - TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} +transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig +transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host = + TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing} + where + useSocksProxy SMAlways = socksProxy + useSocksProxy SMOnion = case host of + THOnionHost _ -> socksProxy + _ -> Nothing {-# INLINE transportClientConfig #-} -- | protocol client configuration. @@ -250,30 +355,35 @@ data ProtocolClientConfig v = ProtocolClientConfig defaultTransport :: (ServiceName, ATransport), -- | network configuration networkConfig :: NetworkConfig, + clientALPN :: Maybe [ALPN], -- | client-server protocol version range serverVRange :: VersionRange v, - -- | delay between sending batches of commands (microseconds) - batchDelay :: Maybe Int + -- | agree shared session secret (used in SMP proxy for additional encryption layer) + agreeSecret :: Bool } -- | Default protocol client configuration. -defaultClientConfig :: VersionRange v -> ProtocolClientConfig v -defaultClientConfig serverVRange = +defaultClientConfig :: Maybe [ALPN] -> VersionRange v -> ProtocolClientConfig v +defaultClientConfig clientALPN serverVRange = ProtocolClientConfig { qSize = 64, defaultTransport = ("443", transport @TLS), networkConfig = defaultNetworkConfig, + clientALPN, serverVRange, - batchDelay = Nothing + agreeSecret = False } {-# INLINE defaultClientConfig #-} defaultSMPClientConfig :: ProtocolClientConfig SMPVersion -defaultSMPClientConfig = defaultClientConfig supportedClientSMPRelayVRange +defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) supportedClientSMPRelayVRange {-# INLINE defaultSMPClientConfig #-} data Request err msg = Request - { entityId :: EntityId, + { corrId :: CorrId, + entityId :: EntityId, + command :: ProtoCommand msg, + pending :: TVar Bool, responseVar :: TMVar (Either (ProtocolClientError err) msg) } @@ -297,10 +407,14 @@ chooseTransportHost NetworkConfig {socksProxy, hostMode, requiredHostMode} hosts publicHost = find (not . isOnionHost) hosts protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient v err msg -> String -protocolClientServer = B.unpack . strEncode . snd3 . transportSession . client_ +protocolClientServer = B.unpack . strEncode . protocolClientServer' +{-# INLINE protocolClientServer #-} + +protocolClientServer' :: ProtocolClient v err msg -> ProtoServer msg +protocolClientServer' = snd3 . transportSession . client_ where snd3 (_, s, _) = s -{-# INLINE protocolClientServer #-} +{-# INLINE protocolClientServer' #-} transportHost' :: ProtocolClient v err msg -> TransportHost transportHost' = transportHost . client_ @@ -320,19 +434,21 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId) -- -- 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 (ServerTransmission v msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) -getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, serverVRange, batchDelay} msgQ disconnected = do +getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> (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 disconnected = do case chooseTransportHost networkConfig (host srv) of Right useHost -> - (atomically (mkProtocolClient useHost) >>= runClient useTransport useHost) + (getCurrentTime >>= atomically . mkProtocolClient useHost >>= runClient useTransport useHost) `catch` \(e :: IOException) -> pure . Left $ PCEIOError e Left e -> pure $ Left e where NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig - mkProtocolClient :: TransportHost -> STM (PClient v err msg) - mkProtocolClient transportHost = do + mkProtocolClient :: TransportHost -> UTCTime -> STM (PClient v err msg) + mkProtocolClient transportHost ts = do connected <- newTVar False - pingErrorCount <- newTVar 0 + sendPings <- newTVar False + lastReceived <- newTVar ts + timeoutErrorCount <- newTVar 0 clientCorrId <- C.newRandomDRG g sentCommands <- TM.empty sndQ <- newTBQueue qSize @@ -342,9 +458,11 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize { connected, transportSession, transportHost, + tcpConnectTimeout, tcpTimeout, - batchDelay, - pingErrorCount, + sendPings, + lastReceived, + timeoutErrorCount, clientCorrId, sentCommands, sndQ, @@ -355,7 +473,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 + let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN} username = proxyUsername transportSession action <- async $ @@ -375,61 +493,95 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize client :: forall c. Transport c => TProxy c -> PClient v err msg -> TMVar (Either (ProtocolClientError err) (ProtocolClient v err msg)) -> c -> IO () client _ c cVar h = do - ks <- atomically $ C.generateKeyPair g + ks <- if agreeSecret then Just <$> atomically (C.generateKeyPair g) else pure Nothing runExceptT (protocolClientHandshake @v @err @msg h ks (keyHash srv) serverVRange) >>= \case Left e -> atomically . putTMVar cVar . Left $ PCETransportError e Right th@THandle {params} -> do sessionTs <- getCurrentTime let c' = ProtocolClient {action = Nothing, client_ = c, thParams = params, sessionTs} + atomically $ writeTVar (lastReceived c) sessionTs atomically $ do writeTVar (connected c) True putTMVar cVar $ Right c' - raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0]) + raceAny_ ([send c' th, process c', receive c' th] <> [monitor c' | smpPingInterval > 0]) `finally` disconnected c' - send :: Transport c => ProtocolClient v err msg -> THandle v c -> IO () - send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= tPutLog h - - receive :: Transport c => ProtocolClient v err msg -> THandle v c -> IO () - receive ProtocolClient {client_ = PClient {rcvQ}} h = forever $ tGet h >>= atomically . writeTBQueue rcvQ - - ping :: ProtocolClient v err msg -> IO () - ping c@ProtocolClient {client_ = PClient {pingErrorCount}} = do - threadDelay' smpPingInterval - runExceptT (sendProtocolCommand c Nothing "" $ protocolPing @v @err @msg) >>= \case - Left PCEResponseTimeout -> do - cnt <- atomically $ stateTVar pingErrorCount $ \cnt -> (cnt + 1, cnt + 1) - when (maxCnt == 0 || cnt < maxCnt) $ ping c - _ -> ping c -- sendProtocolCommand resets pingErrorCount + send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () + send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= sendPending where + sendPending (Nothing, s) = send_ s + sendPending (Just pending, s) = whenM (readTVarIO pending) $ send_ s + send_ = void . tPutLog h + + receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO () + receive ProtocolClient {client_ = PClient {rcvQ, lastReceived, timeoutErrorCount}} h = forever $ do + tGet h >>= atomically . writeTBQueue rcvQ + getCurrentTime >>= atomically . writeTVar lastReceived + atomically $ writeTVar timeoutErrorCount 0 + + monitor :: ProtocolClient v err msg -> IO () + monitor c@ProtocolClient {client_ = PClient {sendPings, lastReceived, timeoutErrorCount}} = loop smpPingInterval + where + loop :: Int64 -> IO () + loop delay = do + threadDelay' delay + diff <- diffUTCTime <$> getCurrentTime <*> readTVarIO lastReceived + let idle = diffToMicroseconds diff + remaining = smpPingInterval - idle + if remaining > 1_000_000 -- delay pings only for significant time + then loop remaining + else do + whenM (readTVarIO sendPings) $ void . runExceptT $ sendProtocolCommand c Nothing "" (protocolPing @v @err @msg) + -- sendProtocolCommand/getResponse updates counter for each command + cnt <- readTVarIO timeoutErrorCount + -- drop client when maxCnt of commands have timed out in sequence, but only after some time has passed after last received response + when (maxCnt == 0 || cnt < maxCnt || diff < recoverWindow) $ loop smpPingInterval + recoverWindow = 15 * 60 -- seconds maxCnt = smpPingCount networkConfig process :: ProtocolClient v err msg -> IO () - process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= mapM_ (processMsg c) + process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= processMsgs c - processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO () - processMsg c@ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr)) = - if B.null $ bs corrId - then sendMsg respOrErr - else do + processMsgs :: ProtocolClient v err msg -> NonEmpty (SignedTransmission err msg) -> IO () + processMsgs c ts = do + ts' <- catMaybes <$> mapM (processMsg c) (L.toList ts) + forM_ msgQ $ \q -> + mapM_ (atomically . writeTBQueue q . serverTransmission c) (L.nonEmpty ts') + + processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO (Maybe (EntityId, ServerTransmission err msg)) + processMsg ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr)) + | B.null $ bs corrId = sendMsg $ STEvent clientResp + | otherwise = atomically (TM.lookup corrId sentCommands) >>= \case - Nothing -> sendMsg respOrErr - Just Request {entityId, responseVar} -> atomically $ do - TM.delete corrId sentCommands - putTMVar responseVar $ response entityId + Nothing -> sendMsg $ STUnexpectedError unexpected + Just Request {entityId, command, pending, responseVar} -> do + wasPending <- + atomically $ do + TM.delete corrId sentCommands + ifM + (swapTVar pending False) + (True <$ tryPutTMVar responseVar (if entityId == entId then clientResp else Left unexpected)) + (pure False) + if wasPending + then pure Nothing + else sendMsg $ if entityId == entId then STResponse command clientResp else STUnexpectedError unexpected where - response entityId - | entityId == entId = - case respOrErr of - Left e -> Left $ PCEResponseError e - Right r -> case protocolError r of - Just e -> Left $ PCEProtocolError e - _ -> Right r - | otherwise = Left . PCEUnexpectedResponse $ bshow respOrErr - sendMsg :: Either err msg -> IO () - sendMsg = \case - Right msg -> atomically $ mapM_ (`writeTBQueue` serverTransmission c entId msg) msgQ - Left e -> putStrLn $ "SMP client error: " <> show e + unexpected = unexpectedResponse respOrErr + clientResp = case respOrErr of + Left e -> Left $ PCEResponseError e + Right r -> case protocolError r of + Just e -> Left $ PCEProtocolError e + _ -> Right r + sendMsg :: ServerTransmission err msg -> IO (Maybe (EntityId, ServerTransmission err msg)) + sendMsg t = case msgQ of + Just _ -> pure $ Just (entId, t) + Nothing -> + Nothing <$ case clientResp of + Left e -> logError $ "SMP client error: " <> tshow e + Right _ -> logWarn "SMP client unprocessed event" + +unexpectedResponse :: Show r => r -> ProtocolClientError err +unexpectedResponse = PCEUnexpectedResponse . B.pack . take 32 . show proxyUsername :: TransportSession msg -> ByteString proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_ @@ -480,6 +632,19 @@ temporaryClientError = \case _ -> False {-# INLINE temporaryClientError #-} +-- converts error of client running on proxy to the error sent to client connected to proxy +smpProxyError :: SMPClientError -> ErrorType +smpProxyError = \case + PCEProtocolError e -> PROXY $ PROTOCOL e + PCEResponseError e -> PROXY $ BROKER $ RESPONSE $ B.unpack $ strEncode e + PCEUnexpectedResponse e -> PROXY $ BROKER $ UNEXPECTED $ B.unpack e + PCEResponseTimeout -> PROXY $ BROKER TIMEOUT + PCENetworkError -> PROXY $ BROKER NETWORK + PCEIncompatibleHost -> PROXY $ BROKER HOST + PCETransportError t -> PROXY $ BROKER $ TRANSPORT t + PCECryptoError _ -> CRYPTO + PCEIOError _ -> INTERNAL + -- | Create a new SMP queue. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command @@ -493,21 +658,24 @@ createSMPQueue :: createSMPQueue c (rKey, rpKey) dhKey auth subMode = sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode) >>= \case IDS qik -> pure qik - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Subscribe to the SMP queue. -- -- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue subscribeSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO () -subscribeSMPQueue c rpKey rId = +subscribeSMPQueue c@ProtocolClient {client_ = PClient {sendPings}} rpKey rId = do + liftIO . atomically $ writeTVar sendPings True sendSMPCommand c (Just rpKey) rId SUB >>= \case - OK -> return () + OK -> pure () cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Subscribe to multiple SMP queues batching commands if supported. subscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ())) -subscribeSMPQueues c qs = sendProtocolCommands c cs >>= mapM (processSUBResponse c) +subscribeSMPQueues c@ProtocolClient {client_ = PClient {sendPings}} qs = do + atomically $ writeTVar sendPings True + sendProtocolCommands c cs >>= mapM (processSUBResponse c) where cs = L.map (\(rpKey, rId) -> (Just rpKey, rId, Cmd SRecipient SUB)) qs @@ -521,15 +689,15 @@ processSUBResponse :: SMPClient -> Response ErrorType BrokerMsg -> IO (Either SM processSUBResponse c (Response rId r) = case r of Right OK -> pure $ Right () Right cmd@MSG {} -> writeSMPMessage c rId cmd $> Right () - Right r' -> pure . Left . PCEUnexpectedResponse $ bshow r' + Right r' -> pure . Left $ unexpectedResponse r' Left e -> pure $ Left e writeSMPMessage :: SMPClient -> RecipientId -> BrokerMsg -> IO () -writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c rId msg) (msgQ $ client_ c) +writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c [(rId, STEvent (Right msg))]) (msgQ $ client_ c) -serverTransmission :: ProtocolClient v err msg -> RecipientId -> msg -> ServerTransmission v msg -serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} entityId message = - (transportSession, thVersion, sessionId, entityId, message) +serverTransmission :: ProtocolClient v err msg -> NonEmpty (RecipientId, ServerTransmission err msg) -> ServerTransmissionBatch v err msg +serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} ts = + (transportSession, thVersion, sessionId, ts) -- | Get message from SMP queue. The server returns ERR PROHIBITED if a client uses SUB and GET via the same transport connection for the same queue -- @@ -539,7 +707,7 @@ getSMPMessage c rpKey rId = sendSMPCommand c (Just rpKey) rId GET >>= \case OK -> pure Nothing cmd@(MSG msg) -> liftIO (writeSMPMessage c rId cmd) $> Just msg - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Subscribe to the SMP queue notifications. -- @@ -567,7 +735,7 @@ enableSMPQueueNotifications :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> enableSMPQueueNotifications c rpKey rId notifierKey rcvNtfPublicDhKey = sendSMPCommand c (Just rpKey) rId (NKEY notifierKey rcvNtfPublicDhKey) >>= \case NID nId rcvNtfSrvPublicDhKey -> pure (nId, rcvNtfSrvPublicDhKey) - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Enable notifications for the multiple queues for push notifications server. enableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId, NtfPublicAuthKey, RcvNtfPublicDhKey) -> IO (NonEmpty (Either SMPClientError (NotifierId, RcvNtfPublicDhKey))) @@ -576,7 +744,7 @@ enableSMPQueuesNtfs c qs = L.map process <$> sendProtocolCommands c cs cs = L.map (\(rpKey, rId, notifierKey, rcvNtfPublicDhKey) -> (Just rpKey, rId, Cmd SRecipient $ NKEY notifierKey rcvNtfPublicDhKey)) qs process (Response _ r) = case r of Right (NID nId rcvNtfSrvPublicDhKey) -> Right (nId, rcvNtfSrvPublicDhKey) - Right r' -> Left . PCEUnexpectedResponse $ bshow r' + Right r' -> Left $ unexpectedResponse r' Left e -> Left e -- | Disable notifications for the queue for push notifications server. @@ -598,7 +766,7 @@ sendSMPMessage :: SMPClient -> Maybe SndPrivateAuthKey -> SenderId -> MsgFlags - sendSMPMessage c spKey sId flags msg = sendSMPCommand c spKey sId (SEND flags msg) >>= \case OK -> pure () - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Acknowledge message delivery (server deletes the message). -- @@ -608,7 +776,7 @@ ackSMPMessage c rpKey rId msgId = sendSMPCommand c (Just rpKey) rId (ACK msgId) >>= \case OK -> return () cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r -- | Irreversibly suspend SMP queue. -- The existing messages from the queue will still be delivered. @@ -630,11 +798,167 @@ deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO deleteSMPQueues = okSMPCommands DEL {-# INLINE deleteSMPQueues #-} +-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender +-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg +connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay +connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth + | thVersion (thParams c) >= sendingProxySMPVersion = + sendProtocolCommand_ c Nothing tOut Nothing "" (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case + PKEY sId vr (chain, key) -> + case supportedClientSMPRelayVRange `compatibleVersion` vr of + Nothing -> throwE $ transportErr TEVersion + Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key + r -> throwE $ unexpectedResponse r + | otherwise = throwE $ PCETransportError TEVersion + where + tOut = Just $ tcpConnectTimeout + tcpTimeout + transportErr = PCEProtocolError . PROXY . BROKER . TRANSPORT + validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519 + validateRelay (X.CertificateChain cert) exact = do + serverKey <- case cert of + [leaf, ca] + | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> + C.x509ToPublic (X.certPubKey . X.signedObject $ X.getSigned leaf, []) >>= C.pubKey + _ -> throwError "bad certificate" + pubKey <- C.verifyX509 serverKey exact + C.x509ToPublic (pubKey, []) >>= C.pubKey + +data ProxiedRelay = ProxiedRelay + { prSessionId :: SessionId, + prVersion :: VersionSMP, + prServerKey :: C.PublicKeyX25519 + } + +data ProxyClientError + = -- | protocol error response from proxy + ProxyProtocolError ErrorType + | -- | unexpexted response + ProxyUnexpectedResponse String + | -- | error between proxy and server + ProxyResponseError ErrorType + deriving (Eq, Show, Exception) + +instance StrEncoding ProxyClientError where + strEncode = \case + ProxyProtocolError e -> "PROTOCOL " <> strEncode e + ProxyUnexpectedResponse s -> "UNEXPECTED " <> B.pack s + ProxyResponseError e -> "SYNTAX " <> strEncode e + strP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> ProxyProtocolError <$> _strP + "UNEXPECTED" -> ProxyUnexpectedResponse . B.unpack <$> (A.space *> A.takeByteString) + "SYNTAX" -> ProxyResponseError <$> _strP + _ -> fail "bad ProxyClientError" + +-- consider how to process slow responses - is it handled somehow locally or delegated to the caller +-- this method is used in the client +-- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender +-- receives PRES :: EncResponse -> BrokerMsg -- proxy to client + +-- When client sends message via proxy, there may be one successful scenario and 9 error scenarios +-- as shown below (WTF stands for unexpected response, ??? for response that failed to parse). +-- client proxy relay proxy client +-- 0) PFWD(SEND) -> RFWD -> RRES -> PRES(OK) -> ok +-- 1) PFWD(SEND) -> RFWD -> RRES -> PRES(ERR) -> PCEProtocolError - business logic error for client +-- 2) PFWD(SEND) -> RFWD -> RRES -> PRES(WTF) -> PCEUnexpectedReponse - relay/client protocol logic error +-- 3) PFWD(SEND) -> RFWD -> RRES -> PRES(???) -> PCEResponseError - relay/client syntax error +-- 4) PFWD(SEND) -> RFWD -> ERR -> ERR PROXY PROTOCOL -> ProxyProtocolError - proxy/relay business logic error +-- 5) PFWD(SEND) -> RFWD -> WTF -> ERR PROXY $ BROKER (UNEXPECTED s) -> ProxyProtocolError - proxy/relay protocol logic +-- 6) PFWD(SEND) -> RFWD -> ??? -> ERR PROXY $ BROKER (RESPONSE s) -> ProxyProtocolError - - proxy/relay syntax +-- 7) PFWD(SEND) -> ERR -> ProxyProtocolError - client/proxy business logic +-- 8) PFWD(SEND) -> WTF -> ProxyUnexpectedResponse - client/proxy protocol logic +-- 9) PFWD(SEND) -> ??? -> ProxyResponseError - client/proxy syntax +-- +-- We report as proxySMPMessage error (ExceptT error) the errors of two kinds: +-- - protocol errors from the destination relay wrapped in PRES - to simplify processing of AUTH and QUOTA errors, in this case proxy is "transparent" for such errors (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other response/transport/connection errors from the client connected to proxy itself +-- Other errors are reported in the function result as `Either ProxiedRelayError ()`, including +-- - protocol errors from the client connected to proxy in ProxyClientError (PCEProtocolError, PCEUnexpectedResponse, PCEResponseError) +-- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError + +proxySMPMessage :: + SMPClient -> + -- proxy session from PKEY + ProxiedRelay -> + -- message to deliver + Maybe SndPrivateAuthKey -> + SenderId -> + MsgFlags -> + MsgBody -> + ExceptT SMPClientError IO (Either ProxyClientError ()) +proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId flags msg = do + -- prepare params + let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams + serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth} + (cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g + let cmdSecret = C.dh' serverKey cmdPrivKey + nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g + -- encode + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender (SEND flags msg)) + auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth + b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of + [] -> throwE $ PCETransportError TELargeMsg + TBError e _ : _ -> throwE $ PCETransportError e + TBTransmission s _ : _ -> pure s + TBTransmissions s _ _ : _ -> pure s + et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength + -- proxy interaction errors are wrapped + let tOut = Just $ 2 * tcpTimeout + tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case + Right r -> case r of + PRES (EncResponse er) -> do + -- server interaction errors are thrown directly + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + case tParse serverThParams t' of + t'' :| [] -> case tDecodeParseValidate serverThParams t'' of + (_auth, _signed, (_c, _e, cmd)) -> case cmd of + Right OK -> pure $ Right () + Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay + Right r' -> throwE $ unexpectedResponse r' + Left e -> throwE $ PCEResponseError e + _ -> throwE $ PCETransportError TEBadBlock + ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left + _ -> pure . Left $ ProxyUnexpectedResponse $ take 32 $ show r + Left e -> case e of + PCEProtocolError e' -> pure . Left $ ProxyProtocolError e' + PCEUnexpectedResponse e' -> pure . Left $ ProxyUnexpectedResponse $ B.unpack e' + PCEResponseError e' -> pure . Left $ ProxyResponseError e' + _ -> throwE e + +-- this method is used in the proxy +-- sends RFWD :: EncFwdTransmission -> Command Sender +-- receives RRES :: EncFwdResponse -> BrokerMsg +-- proxy should send PRES to the client with EncResponse +forwardSMPMessage :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do + -- prepare params + sessSecret <- case thAuth thParams of + Nothing -> throwError $ PCETransportError TENoServerAuth + Just THAuthClient {sessSecret} -> maybe (throwError $ PCETransportError TENoServerAuth) pure sessSecret + nonce <- liftIO . atomically $ C.randomCbNonce g + -- wrap + let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission} + eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) + -- send + sendProtocolCommand_ c (Just nonce) Nothing Nothing "" (Cmd SSender (RFWD eft)) >>= \case + RRES (EncFwdResponse efr) -> do + -- unwrap + r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr + FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' + pure fwdResponse + r -> throwE $ unexpectedResponse r + +getSMPQueueInfo :: SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO QueueInfo +getSMPQueueInfo c pKey qId = + sendSMPCommand c (Just pKey) qId QUE >>= \case + INFO info -> pure info + r -> throwE $ unexpectedResponse r + okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO () okSMPCommand cmd c pKey qId = sendSMPCommand c (Just pKey) qId cmd >>= \case OK -> return () - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r okSMPCommands :: PartyI p => Command p -> SMPClient -> NonEmpty (C.APrivateAuthKey, QueueId) -> IO (NonEmpty (Either SMPClientError ())) okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs @@ -643,7 +967,7 @@ okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs cs = L.map (\(pKey, qId) -> (Just pKey, qId, aCmd)) qs process (Response _ r) = case r of Right OK -> Right () - Right r' -> Left . PCEUnexpectedResponse $ bshow r' + Right r' -> Left $ unexpectedResponse r' Left e -> Left e -- | Send SMP command @@ -654,7 +978,7 @@ sendSMPCommand c pKey qId cmd = sendProtocolCommand c pKey qId (Cmd sParty cmd) type PCTransmission err msg = (Either TransportError SentRawTransmission, Request err msg) -- | Send multiple commands with batching and collect responses -sendProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg)) +sendProtocolCommands :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg)) sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs = do bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs validate . concat =<< mapM (sendBatch c) bs @@ -671,7 +995,7 @@ sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSiz where diff = L.length cs - length rs -streamProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO () +streamProtocolCommands :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO () streamProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs cb = do bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs mapM_ (cb <=< sendBatch c) bs @@ -684,70 +1008,121 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do pure [Response entityId $ Left $ PCETransportError e] TBTransmissions s n rs | n > 0 -> do - atomically $ writeTBQueue sndQ s - mapConcurrently (getResponse c) rs + atomically $ writeTBQueue sndQ (Nothing, s) -- do not expire batched responses + mapConcurrently (getResponse c Nothing) rs | otherwise -> pure [] TBTransmission s r -> do - atomically $ writeTBQueue sndQ s - (: []) <$> getResponse c r + atomically $ writeTBQueue sndQ (Nothing, s) + (: []) <$> getResponse c Nothing r -- | Send Protocol command -sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg -sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} pKey entId cmd = - ExceptT $ uncurry sendRecv =<< mkTransmission c (pKey, entId, cmd) +sendProtocolCommand :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand c = sendProtocolCommand_ c Nothing Nothing + +-- Currently there is coupling - batch commands do not expire, and individually sent commands do. +-- This is to reflect the fact that we send subscriptions only as batches, and also because we do not track a separate timeout for the whole batch, so it is not obvious when should we expire it. +-- We could expire a batch of deletes, for example, either when the first response expires or when the last one does. +-- But a better solution is to process delayed delete responses. +sendProtocolCommand_ :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe Int -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg +sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ tOut pKey entId cmd = + ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd) where -- two separate "atomically" needed to avoid blocking sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg) - sendRecv t_ r = case t_ of + sendRecv t_ r@Request {pending} = case t_ of Left e -> pure . Left $ PCETransportError e Right t | B.length s > blockSize - 2 -> pure . Left $ PCETransportError TELargeMsg - | otherwise -> atomically (writeTBQueue sndQ s) >> response <$> getResponse c r + | otherwise -> do + atomically $ writeTBQueue sndQ (Just pending, s) + response <$> getResponse c tOut r where s | batch = tEncodeBatch1 t | otherwise = tEncode t --- TODO switch to timeout or TimeManager that supports Int64 -getResponse :: ProtocolClient v err msg -> Request err msg -> IO (Response err msg) -getResponse ProtocolClient {client_ = PClient {tcpTimeout, pingErrorCount}} Request {entityId, responseVar} = do - response <- - timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case - Just r -> atomically (writeTVar pingErrorCount 0) $> r - Nothing -> pure $ Left PCEResponseTimeout +getResponse :: ProtocolClient v err msg -> Maybe Int -> Request err msg -> IO (Response err msg) +getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount}} tOut Request {entityId, pending, responseVar} = do + r <- fromMaybe tcpTimeout tOut `timeout` atomically (takeTMVar responseVar) + response <- atomically $ do + writeTVar pending False + -- Try to read response again in case it arrived after timeout expired + -- but before `pending` was set to False above. + -- See `processMsg`. + ((r <|>) <$> tryTakeTMVar responseVar) >>= \case + Just r' -> writeTVar timeoutErrorCount 0 $> r' + Nothing -> modifyTVar' timeoutErrorCount (+ 1) $> Left PCEResponseTimeout pure Response {entityId, response} -mkTransmission :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) -mkTransmission ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} (pKey_, entId, cmd) = do - corrId <- atomically getNextCorrId - let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, entId, cmd) - auth = authTransmission (thAuth thParams) pKey_ corrId tForAuth - r <- atomically $ mkRequest corrId +mkTransmission :: Protocol v err msg => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission c = mkTransmission_ c Nothing + +mkTransmission_ :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.CbNonce -> ClientCommand msg -> IO (PCTransmission err msg) +mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (pKey_, entityId, command) = do + nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_ + let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entityId, command) + auth = authTransmission (thAuth thParams) pKey_ nonce tForAuth + r <- atomically $ mkRequest (CorrId corrId) pure ((,tToSend) <$> auth, r) where - getNextCorrId :: STM CorrId - getNextCorrId = CorrId <$> C.randomBytes 24 clientCorrId -- also used as nonce mkRequest :: CorrId -> STM (Request err msg) mkRequest corrId = do - r <- Request entId <$> newEmptyTMVar + pending <- newTVar True + responseVar <- newEmptyTMVar + let r = + Request + { corrId, + entityId, + command, + pending, + responseVar + } TM.insert corrId r sentCommands pure r -authTransmission :: Maybe THandleAuth -> Maybe C.APrivateAuthKey -> CorrId -> ByteString -> Either TransportError (Maybe TransmissionAuth) -authTransmission thAuth pKey_ (CorrId corrId) t = traverse authenticate pKey_ +authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth) +authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_ where authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth authenticate (C.APrivateAuthKey a pk) = case a of C.SX25519 -> case thAuth of - Just THandleAuth {peerPubKey} -> Right $ TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t + Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t Nothing -> Left TENoServerAuth C.SEd25519 -> sign pk C.SEd448 -> sign pk sign :: forall a. (C.AlgorithmI a, C.SignatureAlgorithm a) => C.PrivateKey a -> Either TransportError TransmissionAuth sign pk = Right $ TASignature $ C.ASignature (C.sAlgorithm @a) (C.sign' pk t) +data TBQueueInfo = TBQueueInfo + { qLength :: Int, + qFull :: Bool + } + deriving (Show) + +getTBQueueInfo :: TBQueue a -> STM TBQueueInfo +getTBQueueInfo q = do + qLength <- fromIntegral <$> lengthTBQueue q + qFull <- isFullTBQueue q + pure TBQueueInfo {qLength, qFull} + +getProtocolClientQueuesInfo :: ProtocolClient v err msg -> IO (TBQueueInfo, TBQueueInfo) +getProtocolClientQueuesInfo ProtocolClient {client_ = PClient {sndQ, rcvQ}} = do + sndQInfo <- atomically $ getTBQueueInfo sndQ + rcvQInfo <- atomically $ getTBQueueInfo rcvQ + pure (sndQInfo, rcvQInfo) + $(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SM") ''SocksMode) + $(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode) +$(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode) + +$(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback) + $(J.deriveJSON defaultJSON ''NetworkConfig) + +$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError) + +$(J.deriveJSON defaultJSON ''TBQueueInfo) diff --git a/src/Simplex/Messaging/Client/Agent.hs b/src/Simplex/Messaging/Client/Agent.hs index 8e21aada1..8781d87a6 100644 --- a/src/Simplex/Messaging/Client/Agent.hs +++ b/src/Simplex/Messaging/Client/Agent.hs @@ -2,24 +2,24 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} module Simplex.Messaging.Client.Agent where import Control.Concurrent (forkIO) import Control.Concurrent.Async (Async, uninterruptibleCancel) +import Control.Concurrent.STM (retry) import Control.Logger.Simple import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Trans.Except -import Control.Monad.Trans.Reader import Crypto.Random (ChaChaDRG) import Data.Bifunctor (bimap, first) import Data.ByteString.Char8 (ByteString) @@ -33,29 +33,29 @@ import qualified Data.Map.Strict as M import Data.Maybe (listToMaybe) import Data.Set (Set) import Data.Text.Encoding +import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime) import Data.Tuple (swap) import Numeric.Natural import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (BrokerMsg, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer) +import Simplex.Messaging.Protocol (BrokerMsg, ErrorType, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer) +import Simplex.Messaging.Session import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport -import Simplex.Messaging.Util (catchAll_, toChunks, ($>>=)) +import Simplex.Messaging.Util (catchAll_, ifM, toChunks, whenM, ($>>=), (<$$>)) import System.Timeout (timeout) import UnliftIO (async) -import UnliftIO.Exception (Exception) import qualified UnliftIO.Exception as E import UnliftIO.STM -type SMPClientVar = TMVar (Either SMPClientError SMPClient) +type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) (OwnServer, SMPClient)) data SMPClientAgentEvent = CAConnected SMPServer | CADisconnected SMPServer (Set SMPSub) - | CAReconnected SMPServer | CAResubscribed SMPServer (NonEmpty SMPSub) | CASubError SMPServer (NonEmpty (SMPSub, SMPClientError)) @@ -69,9 +69,11 @@ type SMPSub = (SMPSubParty, QueueId) data SMPClientAgentConfig = SMPClientAgentConfig { smpCfg :: ProtocolClientConfig SMPVersion, reconnectInterval :: RetryInterval, + persistErrorInterval :: NominalDiffTime, msgQSize :: Natural, agentQSize :: Natural, - agentSubsBatchSize :: Int + agentSubsBatchSize :: Int, + ownServerDomains :: [ByteString] } defaultSMPClientAgentConfig :: SMPClientAgentConfig @@ -84,119 +86,128 @@ defaultSMPClientAgentConfig = increaseAfter = 10 * second, maxInterval = 10 * second }, + persistErrorInterval = 30, -- seconds msgQSize = 256, agentQSize = 256, - agentSubsBatchSize = 900 + agentSubsBatchSize = 900, + ownServerDomains = [] } where second = 1000000 data SMPClientAgent = SMPClientAgent { agentCfg :: SMPClientAgentConfig, - msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg), + active :: TVar Bool, + msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg), agentQ :: TBQueue SMPClientAgentEvent, randomDrg :: TVar ChaChaDRG, smpClients :: TMap SMPServer SMPClientVar, + smpSessions :: TMap SessionId (OwnServer, SMPClient), srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey), - reconnections :: TVar [Async ()], - asyncClients :: TVar [Async ()] + smpSubWorkers :: TMap SMPServer (SessionVar (Async ())), + workerSeq :: TVar Int } -newtype InternalException e = InternalException {unInternalException :: e} - deriving (Eq, Show) - -instance Exception e => Exception (InternalException e) - -instance Exception e => MonadUnliftIO (ExceptT e IO) where - {-# INLINE withRunInIO #-} - withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b - withRunInIO inner = - ExceptT . fmap (first unInternalException) . E.try $ - withRunInIO $ \run -> - inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT) - -- as MonadUnliftIO instance for IO is `withRunInIO inner = inner id`, - -- the last two lines could be replaced with: - -- inner $ either (E.throwIO . InternalException) pure <=< runExceptT - -instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where - {-# INLINE withRunInIO #-} - withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b - withRunInIO inner = - withExceptT unInternalException . ExceptT . E.try $ - withRunInIO $ \run -> - inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT) +type OwnServer = Bool newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do + active <- newTVar True msgQ <- newTBQueue msgQSize agentQ <- newTBQueue agentQSize smpClients <- TM.empty + smpSessions <- TM.empty srvSubs <- TM.empty pendingSrvSubs <- TM.empty - reconnections <- newTVar [] - asyncClients <- newTVar [] - pure SMPClientAgent {agentCfg, msgQ, agentQ, randomDrg, smpClients, srvSubs, pendingSrvSubs, reconnections, asyncClients} + smpSubWorkers <- TM.empty + workerSeq <- newTVar 0 + pure + SMPClientAgent + { agentCfg, + active, + msgQ, + agentQ, + randomDrg, + smpClients, + smpSessions, + srvSubs, + pendingSrvSubs, + smpSubWorkers, + workerSeq + } +-- | Get or create SMP client for SMPServer getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient -getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg} srv = - atomically getClientVar >>= either newSMPClient waitForSMPClient +getSMPServerClient' ca srv = snd <$> getSMPServerClient'' ca srv +{-# INLINE getSMPServerClient' #-} + +getSMPServerClient'' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO (OwnServer, SMPClient) +getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, workerSeq} srv = do + ts <- liftIO getCurrentTime + atomically (getClientVar ts) >>= either (ExceptT . newSMPClient) waitForSMPClient where - getClientVar :: STM (Either SMPClientVar SMPClientVar) - getClientVar = maybe (Left <$> newClientVar) (pure . Right) =<< TM.lookup srv smpClients + getClientVar :: UTCTime -> STM (Either SMPClientVar SMPClientVar) + getClientVar = getSessVar workerSeq srv smpClients - newClientVar :: STM SMPClientVar - newClientVar = do - smpVar <- newEmptyTMVar - TM.insert srv smpVar smpClients - pure smpVar - - waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient - waitForSMPClient smpVar = do + waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO (OwnServer, SMPClient) + waitForSMPClient v = do let ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg - smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar smpVar) - liftEither $ case smpClient_ of - Just (Right smpClient) -> Right smpClient - Just (Left e) -> Left e - Nothing -> Left PCEResponseTimeout + smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v) + case smpClient_ of + Just (Right smpClient) -> pure smpClient + Just (Left (e, ts_)) -> case ts_ of + Nothing -> throwE e + Just ts -> + ifM + ((ts <) <$> liftIO getCurrentTime) + (atomically (removeSessVar v srv smpClients) >> getSMPServerClient'' ca srv) + (throwE e) + Nothing -> throwE PCEResponseTimeout - newSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient - newSMPClient smpVar = tryConnectClient pure (liftIO tryConnectAsync) - where - tryConnectClient :: (SMPClient -> ExceptT SMPClientError IO a) -> ExceptT SMPClientError IO () -> ExceptT SMPClientError IO a - tryConnectClient successAction retryAction = - tryE connectClient >>= \r -> case r of - Right smp -> do - logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv - atomically $ putTMVar smpVar r - successAction smp - Left e -> do - if e == PCENetworkError || e == PCEResponseTimeout - then retryAction - else atomically $ do - putTMVar smpVar (Left e) - TM.delete srv smpClients - throwE e - tryConnectAsync :: IO () - tryConnectAsync = do - a <- async $ void $ runExceptT connectAsync - atomically $ modifyTVar' (asyncClients ca) (a :) - connectAsync :: ExceptT SMPClientError IO () - connectAsync = - withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> - void $ tryConnectClient (const reconnectClient) loop + newSMPClient :: SMPClientVar -> IO (Either SMPClientError (OwnServer, SMPClient)) + newSMPClient v = do + r <- connectClient ca srv v `E.catch` (pure . Left . PCEIOError) + case r of + Right smp -> do + logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv + let c = (isOwnServer ca srv, smp) + atomically $ do + putTMVar (sessionVar v) (Right c) + TM.insert (sessionId $ thParams smp) c smpSessions + notify ca $ CAConnected srv + pure $ Right c + Left e -> do + let ei = persistErrorInterval agentCfg + if ei == 0 + then atomically $ do + putTMVar (sessionVar v) (Left (e, Nothing)) + removeSessVar v srv smpClients + else do + ts <- addUTCTime ei <$> liftIO getCurrentTime + atomically $ putTMVar (sessionVar v) (Left (e, Just ts)) + reconnectClient ca srv + pure $ Left e - connectClient :: ExceptT SMPClientError IO SMPClient - connectClient = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected +isOwnServer :: SMPClientAgent -> SMPServer -> OwnServer +isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} = + let srv = strEncode $ L.head host + in any (\s -> s == srv || (B.cons '.' s) `B.isSuffixOf` srv) (ownServerDomains agentCfg) +-- | Run an SMP client for SMPClientVar +connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient) +connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg} srv v = + getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected + where clientDisconnected :: SMPClient -> IO () - clientDisconnected _ = do - removeClientAndSubs >>= (`forM_` serverDown) + clientDisconnected smp = do + removeClientAndSubs smp >>= (`forM_` serverDown) logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv - removeClientAndSubs :: IO (Maybe (Map SMPSub C.APrivateAuthKey)) - removeClientAndSubs = atomically $ do - TM.delete srv smpClients + removeClientAndSubs :: SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey)) + removeClientAndSubs smp = atomically $ do + removeSessVar v srv smpClients + TM.delete (sessionId $ thParams smp) smpSessions TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs where updateSubs sVar = do @@ -207,72 +218,109 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ, randomDrg} sr addPendingSubs sVar ss = do let ps = pendingSrvSubs ca TM.lookup srv ps >>= \case - Just v -> TM.union ss v + Just ss' -> TM.union ss ss' _ -> TM.insert srv sVar ps serverDown :: Map SMPSub C.APrivateAuthKey -> IO () serverDown ss = unless (M.null ss) $ do - notify . CADisconnected srv $ M.keysSet ss - reconnectServer + notify ca . CADisconnected srv $ M.keysSet ss + reconnectClient ca srv - reconnectServer :: IO () - reconnectServer = do - a <- async $ void $ runExceptT tryReconnectClient - atomically $ modifyTVar' (reconnections ca) (a :) +-- | Spawn reconnect worker if needed +reconnectClient :: SMPClientAgent -> SMPServer -> IO () +reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} srv = do + ts <- getCurrentTime + whenM (readTVarIO active) $ atomically (getWorkerVar ts) >>= mapM_ (either newSubWorker (\_ -> pure ())) + where + getWorkerVar ts = + ifM + (null <$> getPending) + (pure Nothing) -- prevent race with cleanup and adding pending queues in another call + (Just <$> getSessVar workerSeq srv smpSubWorkers ts) + newSubWorker :: SessionVar (Async ()) -> IO () + newSubWorker v = do + a <- async $ void (E.tryAny runSubWorker) >> atomically (cleanup v) + atomically $ putTMVar (sessionVar v) a + runSubWorker = + withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> do + pending <- atomically getPending + forM_ pending $ \cs -> whenM (readTVarIO active) $ do + void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv cs) + loop + ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg + getPending = mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca) + cleanup :: SessionVar (Async ()) -> STM () + cleanup v = do + -- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar. + -- Not waiting may result in terminated worker remaining in the map. + whenM (isEmptyTMVar $ sessionVar v) retry + removeSessVar v srv smpSubWorkers - tryReconnectClient :: ExceptT SMPClientError IO () - tryReconnectClient = do - withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> - reconnectClient `catchE` const loop - - reconnectClient :: ExceptT SMPClientError IO () - reconnectClient = do - withSMP ca srv $ \smp -> do - liftIO $ notify $ CAReconnected srv - cs_ <- atomically $ mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca) - forM_ cs_ $ \cs -> do - subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs - let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs' - subscribe_ smp SPNotifier nSubs - subscribe_ smp SPRecipient rSubs +reconnectSMPClient :: SMPClientAgent -> SMPServer -> Map SMPSub C.APrivateAuthKey -> ExceptT SMPClientError IO () +reconnectSMPClient ca@SMPClientAgent {agentCfg} srv cs = + withSMP ca srv $ \smp -> do + subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs + let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs' + subscribe_ smp SPNotifier nSubs + subscribe_ smp SPRecipient rSubs + where + isNotifier = \case + SPNotifier -> True + SPRecipient -> False + subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO () + subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg) where - isNotifier = \case - SPNotifier -> True - SPRecipient -> False + subscribeBatch subs' = do + let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs' + rs <- liftIO $ smpSubscribeQueues party ca smp srv subs'' + let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) = + L.zipWith (first . const) subs' rs + rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] = + map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs' + (errs, oks) = partitionEithers rs'' + (tempErrs, finalErrs) = partition (temporaryClientError . snd) errs + mapM_ (atomically . addSubscription ca srv) oks + mapM_ (notify ca . CAResubscribed srv) $ L.nonEmpty $ map fst oks + mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs + mapM_ (notify ca . CASubError srv) $ L.nonEmpty finalErrs + mapM_ (throwE . snd) $ listToMaybe tempErrs - subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO () - subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg) - where - subscribeBatch subs' = do - let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs' - rs <- liftIO $ smpSubscribeQueues party ca smp srv subs'' - let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) = - L.zipWith (first . const) subs' rs - rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] = - map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs' - (errs, oks) = partitionEithers rs'' - (tempErrs, finalErrs) = partition (temporaryClientError . snd) errs - mapM_ (atomically . addSubscription ca srv) oks - mapM_ (liftIO . notify . CAResubscribed srv) $ L.nonEmpty $ map fst oks - mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs - mapM_ (liftIO . notify . CASubError srv) $ L.nonEmpty finalErrs - mapM_ (throwE . snd) $ listToMaybe tempErrs +notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m () +notify ca evt = atomically $ writeTBQueue (agentQ ca) evt +{-# INLINE notify #-} - notify :: SMPClientAgentEvent -> IO () - notify evt = atomically $ writeTBQueue (agentQ ca) evt +-- Returns already connected client for proxying messages or Nothing if client is absent, not connected yet or stores expired error. +-- If Nothing is return proxy will spawn a new thread to wait or to create another client connection to destination relay. +getConnectedSMPServerClient :: SMPClientAgent -> SMPServer -> IO (Maybe (Either SMPClientError (OwnServer, SMPClient))) +getConnectedSMPServerClient SMPClientAgent {smpClients} srv = + atomically (TM.lookup srv smpClients $>>= \v -> (v,) <$$> tryReadTMVar (sessionVar v)) -- Nothing: client is absent or not connected yet + $>>= \case + (_, Right r) -> pure $ Just $ Right r + (v, Left (e, ts_)) -> + pure ts_ $>>= \ts -> -- proxy will create a new connection if ts_ is Nothing + ifM + ((ts <) <$> liftIO getCurrentTime) -- error persistence interval period expired? + (Nothing <$ atomically (removeSessVar v srv smpClients)) -- proxy will create a new connection + (pure $ Just $ Left e) -- not expired, returning error + +lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe (OwnServer, SMPClient)) +lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions closeSMPClientAgent :: SMPClientAgent -> IO () closeSMPClientAgent c = do + atomically $ writeTVar (active c) False closeSMPServerClients c - cancelActions $ reconnections c - cancelActions $ asyncClients c + atomically (swapTVar (smpSubWorkers c) M.empty) >>= mapM_ cancelReconnect + where + cancelReconnect :: SessionVar (Async ()) -> IO () + cancelReconnect v = void . forkIO $ atomically (readTMVar $ sessionVar v) >>= uninterruptibleCancel closeSMPServerClients :: SMPClientAgent -> IO () -closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ (forkIO . closeClient) +closeSMPServerClients c = atomically (smpClients c `swapTVar` M.empty) >>= mapM_ (forkIO . closeClient) where - closeClient smpVar = - atomically (readTMVar smpVar) >>= \case - Right smp -> closeProtocolClient smp `catchAll_` pure () + closeClient v = + atomically (readTMVar $ sessionVar v) >>= \case + Right (_, smp) -> closeProtocolClient smp `catchAll_` pure () _ -> pure () cancelActions :: Foldable f => TVar (f (Async ())) -> IO () diff --git a/src/Simplex/Messaging/Compression.hs b/src/Simplex/Messaging/Compression.hs index cd9f641ff..19d91a300 100644 --- a/src/Simplex/Messaging/Compression.hs +++ b/src/Simplex/Messaging/Compression.hs @@ -4,20 +4,9 @@ module Simplex.Messaging.Compression where import qualified Codec.Compression.Zstd as Z1 -import qualified Codec.Compression.Zstd.FFI as Z -import Control.Monad (forM) -import Control.Monad.Except -import Control.Monad.IO.Class import Data.ByteString (ByteString) import qualified Data.ByteString as B -import qualified Data.ByteString.Unsafe as B -import Data.Either (fromRight) -import Data.List.NonEmpty (NonEmpty) -import Foreign -import Foreign.C.Types -import GHC.IO (unsafePerformIO) import Simplex.Messaging.Encoding -import UnliftIO.Exception (bracket) data Compressed = -- | Short messages are left intact to skip copying and FFI festivities. @@ -42,57 +31,15 @@ instance Encoding Compressed where '1' -> Compressed <$> smpP x -> fail $ "unknown Compressed tag: " <> show x --- | Compress as single chunk using stack-allocated context. compress1 :: ByteString -> Compressed compress1 bs | B.length bs <= maxLengthPassthrough = Passthrough bs | otherwise = Compressed . Large $ Z1.compress compressionLevel bs -type CompressCtx = (Ptr Z.CCtx, Ptr CChar, CSize) - -withCompressCtx :: CSize -> (CompressCtx -> IO a) -> IO a -withCompressCtx scratchSize = bracket (createCompressCtx scratchSize) freeCompressCtx - -createCompressCtx :: CSize -> IO CompressCtx -createCompressCtx scratchSize = do - ctx <- Z.createCCtx - scratch <- mallocBytes (fromIntegral scratchSize) - pure (ctx, scratch, scratchSize) - -freeCompressCtx :: CompressCtx -> IO () -freeCompressCtx (ctx, scratch, _) = do - free scratch - Z.freeCCtx ctx - --- | Compress bytes, falling back to Passthrough in case of some internal error. -compress :: CompressCtx -> ByteString -> IO Compressed -compress ctx bs = fromRight (Passthrough bs) <$> compress_ ctx bs - -compress_ :: CompressCtx -> ByteString -> IO (Either String Compressed) -compress_ (cctx, scratchPtr, scratchSize) bs - | B.length bs <= maxLengthPassthrough = pure . Right $ Passthrough bs - | otherwise = - B.unsafeUseAsCStringLen bs $ \(sourcePtr, sourceSize) -> runExceptT $ do - -- should not fail, unless input buffer is too short - dstSize <- ExceptT $ Z.checkError $ Z.compressCCtx cctx scratchPtr scratchSize sourcePtr (fromIntegral sourceSize) compressionLevel - liftIO $ Compressed . Large <$> B.packCStringLen (scratchPtr, fromIntegral dstSize) - -type DecompressCtx = (Ptr Z.DCtx, Ptr CChar, CSize) - -withDecompressCtx :: Int -> (DecompressCtx -> IO a) -> IO a -withDecompressCtx maxUnpackedSize action = - bracket Z.createDCtx Z.freeDCtx $ \dctx -> - allocaBytes maxUnpackedSize $ \scratchPtr -> - action (dctx, scratchPtr, fromIntegral maxUnpackedSize) - -decompress :: DecompressCtx -> Compressed -> IO (Either String ByteString) -decompress (dctx, scratchPtr, scratchSize) = \case - Passthrough bs -> pure $ Right bs - Compressed (Large bs) -> - B.unsafeUseAsCStringLen bs $ \(sourcePtr, sourceSize) -> do - res <- Z.checkError $ Z.decompressDCtx dctx scratchPtr scratchSize sourcePtr (fromIntegral sourceSize) - forM res $ \dstSize -> B.packCStringLen (scratchPtr, fromIntegral dstSize) - -decompressBatch :: Int -> NonEmpty Compressed -> NonEmpty (Either String ByteString) -decompressBatch maxUnpackedSize items = unsafePerformIO $ withDecompressCtx maxUnpackedSize $ forM items . decompress -{-# NOINLINE decompressBatch #-} -- prevent double-evaluation under unsafePerformIO +decompress1 :: Compressed -> Either String ByteString +decompress1 = \case + Passthrough bs -> Right bs + Compressed (Large bs) -> case Z1.decompress bs of + Z1.Error e -> Left e + Z1.Skip -> Right mempty + Z1.Decompress bs' -> Right bs' diff --git a/src/Simplex/Messaging/Crypto.hs b/src/Simplex/Messaging/Crypto.hs index 28183a1fc..7aefbd709 100644 --- a/src/Simplex/Messaging/Crypto.hs +++ b/src/Simplex/Messaging/Crypto.hs @@ -141,6 +141,7 @@ module Simplex.Messaging.Crypto sbEncrypt_, cbNonce, randomCbNonce, + reverseNonce, -- * NaCl crypto_secretbox SbKey (unSbKey), @@ -756,6 +757,8 @@ data Signature (a :: Algorithm) where SignatureEd25519 :: Ed25519.Signature -> Signature Ed25519 SignatureEd448 :: Ed448.Signature -> Signature Ed448 +deriving instance Eq (Signature a) + deriving instance Show (Signature a) data ASignature @@ -1290,6 +1293,9 @@ randomCbNonce = fmap CryptoBoxNonce . randomBytes 24 randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n +reverseNonce :: CbNonce -> CbNonce +reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s) + instance Encoding CbNonce where smpEncode = unCbNonce smpP = CryptoBoxNonce <$> A.take 24 diff --git a/src/Simplex/Messaging/Crypto/File.hs b/src/Simplex/Messaging/Crypto/File.hs index 2787df58e..9608d21b7 100644 --- a/src/Simplex/Messaging/Crypto/File.hs +++ b/src/Simplex/Messaging/Crypto/File.hs @@ -31,7 +31,6 @@ import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (isJust) -import Simplex.Messaging.Client.Agent () import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Lazy (LazyByteString) import qualified Simplex.Messaging.Crypto.Lazy as LC diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index 068f62776..14f567820 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -117,7 +117,7 @@ import Simplex.Messaging.Crypto.SNTRUP761.Bindings import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, parseE, parseE') -import Simplex.Messaging.Util ((<$?>), ($>>=)) +import Simplex.Messaging.Util (($>>=), (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import UnliftIO.STM @@ -143,16 +143,11 @@ kdfX3DHE2EEncryptVersion = VersionE2E 2 pqRatchetE2EEncryptVersion :: VersionE2E pqRatchetE2EEncryptVersion = VersionE2E 3 --- TODO v5.7 increase to 3 currentE2EEncryptVersion :: VersionE2E -currentE2EEncryptVersion = VersionE2E 2 +currentE2EEncryptVersion = VersionE2E 3 --- TODO v5.7 remove dependency of version range on whether PQ encryption is used -supportedE2EEncryptVRange :: PQSupport -> VersionRangeE2E -supportedE2EEncryptVRange pq = - mkVersionRange kdfX3DHE2EEncryptVersion $ case pq of - PQSupportOn -> pqRatchetE2EEncryptVersion - PQSupportOff -> currentE2EEncryptVersion +supportedE2EEncryptVRange :: VersionRangeE2E +supportedE2EEncryptVRange = mkVersionRange kdfX3DHE2EEncryptVersion currentE2EEncryptVersion data RatchetKEMState = RKSProposed -- only KEM encapsulation key @@ -171,9 +166,9 @@ instance TestEquality SRatchetKEMState where class RatchetKEMStateI (s :: RatchetKEMState) where sRatchetKEMState :: SRatchetKEMState s -instance RatchetKEMStateI RKSProposed where sRatchetKEMState = SRKSProposed +instance RatchetKEMStateI 'RKSProposed where sRatchetKEMState = SRKSProposed -instance RatchetKEMStateI RKSAccepted where sRatchetKEMState = SRKSAccepted +instance RatchetKEMStateI 'RKSAccepted where sRatchetKEMState = SRKSAccepted checkRatchetKEMState :: forall t s s' a. (RatchetKEMStateI s, RatchetKEMStateI s') => t s' a -> Either String (t s a) checkRatchetKEMState x = case testEquality (sRatchetKEMState @s) (sRatchetKEMState @s') of @@ -271,6 +266,7 @@ instance VersionRangeI E2EVersion (E2ERatchetParamsUri s a) where type VersionT E2EVersion (E2ERatchetParamsUri s a) = (E2ERatchetParams s a) versionRange (E2ERatchetParamsUri vr _ _ _) = vr toVersionT (E2ERatchetParamsUri _ k1 k2 kem_) v = E2ERatchetParams v k1 k2 kem_ + toVersionRange (E2ERatchetParamsUri _ k1 k2 kem_) vr = E2ERatchetParamsUri vr k1 k2 kem_ type RcvE2ERatchetParamsUri a = E2ERatchetParamsUri 'RKSProposed a @@ -382,13 +378,15 @@ generateE2EParams g v useKEM_ = do where kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s)) kemParams = case useKEM_ of - Just useKem | v >= pqRatchetE2EEncryptVersion -> Just <$> do - ks@(k, _) <- sntrup761Keypair g - case useKem of - ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) - AcceptKEM k' -> do - (ct, shared) <- sntrup761Enc g k' - pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) + Just useKem + | v >= pqRatchetE2EEncryptVersion -> + Just <$> do + ks@(k, _) <- sntrup761Keypair g + case useKem of + ProposeKEM -> pure (RKParamsProposed k, PrivateRKParamsProposed ks) + AcceptKEM k' -> do + (ct, shared) <- sntrup761Enc g k' + pure (RKParamsAccepted ct k, PrivateRKParamsAccepted ct shared ks) _ -> pure Nothing -- used by party initiating connection, Bob in double-ratchet spec @@ -461,7 +459,7 @@ pqX3dh (sk1, rk1) dh1 dh2 dh3 kemAccepted = pq = maybe "" (\RatchetKEMAccepted {rcPQRss = KEMSharedKey ss} -> BA.convert ss) kemAccepted (hk, nhk, sk) = let salt = B.replicate 64 '\0' - in hkdf3 salt dhs "SimpleXX3DH" + in hkdf3 salt dhs "SimpleXX3DH" type RatchetX448 = Ratchet 'X448 @@ -703,8 +701,8 @@ data EncMessageHeader = EncMessageHeader -- this encoding depends on version in EncMessageHeader because it is "current" ratchet version instance Encoding EncMessageHeader where - smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} - = smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody + smpEncode EncMessageHeader {ehVersion, ehIV, ehAuthTag, ehBody} = + smpEncode (ehVersion, ehIV, ehAuthTag) <> encodeLarge ehVersion ehBody smpP = do (ehVersion, ehIV, ehAuthTag) <- smpP ehBody <- largeP @@ -713,8 +711,6 @@ instance Encoding EncMessageHeader where -- the encoder always uses 2-byte lengths for the new version, even for short headers without PQ keys. encodeLarge :: VersionE2E -> ByteString -> ByteString encodeLarge v s - -- the condition for length is not necessary, it's here as a fallback. - -- | v >= pqRatchetE2EEncryptVersion || B.length s > 255 = smpEncode $ Large s | v >= pqRatchetE2EEncryptVersion = smpEncode $ Large s | otherwise = smpEncode s @@ -734,8 +730,8 @@ data EncRatchetMessage = EncRatchetMessage } encodeEncRatchetMessage :: VersionE2E -> EncRatchetMessage -> ByteString -encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} - = encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) +encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag} = + encodeLarge v emHeader <> smpEncode (emAuthTag, Tail emBody) encRatchetMessageP :: Parser EncRatchetMessage encRatchetMessageP = do diff --git a/src/Simplex/Messaging/Encoding/String.hs b/src/Simplex/Messaging/Encoding/String.hs index fcefdc73d..6b9fb5624 100644 --- a/src/Simplex/Messaging/Encoding/String.hs +++ b/src/Simplex/Messaging/Encoding/String.hs @@ -75,6 +75,8 @@ instance StrEncoding Str where strEncode = unStr strP = Str <$> A.takeTill (== ' ') <* optional A.space +-- inherited from ByteString, the parser only allows non-empty strings +-- only Char8 elements may round-trip as B.pack truncates unicode instance StrEncoding String where strEncode = strEncode . B.pack strP = B.unpack <$> strP diff --git a/src/Simplex/Messaging/Notifications/Client.hs b/src/Simplex/Messaging/Notifications/Client.hs index 72a92c278..32d92faf3 100644 --- a/src/Simplex/Messaging/Notifications/Client.hs +++ b/src/Simplex/Messaging/Notifications/Client.hs @@ -10,22 +10,21 @@ import Data.Word (Word16) import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol -import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange) +import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes) import Simplex.Messaging.Protocol (ErrorType) -import Simplex.Messaging.Util (bshow) type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse type NtfClientError = ProtocolClientError ErrorType defaultNTFClientConfig :: ProtocolClientConfig NTFVersion -defaultNTFClientConfig = defaultClientConfig supportedClientNTFVRange +defaultNTFClientConfig = defaultClientConfig (Just supportedNTFHandshakes) supportedClientNTFVRange ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519) ntfRegisterToken c pKey newTkn = sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case NRTknId tknId dhKey -> pure (tknId, dhKey) - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfVerifyToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> NtfRegCode -> ExceptT NtfClientError IO () ntfVerifyToken c pKey tknId code = okNtfCommand (TVFY code) c pKey tknId @@ -34,7 +33,7 @@ ntfCheckToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> ExceptT NtfClie ntfCheckToken c pKey tknId = sendNtfCommand c (Just pKey) tknId TCHK >>= \case NRTkn stat -> pure stat - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfReplaceToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> DeviceToken -> ExceptT NtfClientError IO () ntfReplaceToken c pKey tknId token = okNtfCommand (TRPL token) c pKey tknId @@ -49,13 +48,13 @@ ntfCreateSubscription :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Subscri ntfCreateSubscription c pKey newSub = sendNtfCommand c (Just pKey) "" (SNEW newSub) >>= \case NRSubId subId -> pure subId - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfCheckSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus ntfCheckSubscription c pKey subId = sendNtfCommand c (Just pKey) subId SCHK >>= \case NRSub stat -> pure stat - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r ntfDeleteSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO () ntfDeleteSubscription = okNtfCommand SDEL @@ -68,4 +67,4 @@ okNtfCommand :: NtfEntityI e => NtfCommand e -> NtfClient -> C.APrivateAuthKey - okNtfCommand cmd c pKey entId = sendNtfCommand c (Just pKey) entId cmd >>= \case NROk -> return () - r -> throwE . PCEUnexpectedResponse $ bshow r + r -> throwE $ unexpectedResponse r diff --git a/src/Simplex/Messaging/Notifications/Protocol.hs b/src/Simplex/Messaging/Notifications/Protocol.hs index 2c3d2e4df..58a5e5193 100644 --- a/src/Simplex/Messaging/Notifications/Protocol.hs +++ b/src/Simplex/Messaging/Notifications/Protocol.hs @@ -152,7 +152,7 @@ instance Encoding ANewNtfEntity where instance Protocol NTFVersion ErrorType NtfResponse where type ProtoCommand NtfResponse = NtfCmd type ProtoType NtfResponse = 'PNTF - protocolClientHandshake = ntfClientHandshake + protocolClientHandshake c _ks = ntfClientHandshake c protocolPing = NtfCmd SSubscription PING protocolError = \case NRErr e -> Just e diff --git a/src/Simplex/Messaging/Notifications/Server.hs b/src/Simplex/Messaging/Notifications/Server.hs index 55ab40718..892560660 100644 --- a/src/Simplex/Messaging/Notifications/Server.hs +++ b/src/Simplex/Messaging/Notifications/Server.hs @@ -31,7 +31,7 @@ import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) import Data.Time.Clock.System (getSystemTime) import Data.Time.Format.ISO8601 (iso8601Show) import Network.Socket (ServiceName) -import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError) +import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError, ServerTransmission (..)) import Simplex.Messaging.Client.Agent import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String @@ -47,7 +47,7 @@ import qualified Simplex.Messaging.Protocol as SMP 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 (..)) +import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..)) import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials) import Simplex.Messaging.Util import System.Exit (exitFailure) @@ -98,7 +98,9 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do stopServer = do withNtfLog closeStoreLog saveServerStats - asks (smpSubscribers . subscriber) >>= readTVarIO >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (liftIO . deRefWeak >=> mapM_ killThread)) + NtfSubscriber {smpSubscribers, smpAgent} <- asks subscriber + liftIO $ readTVarIO smpSubscribers >>= mapM_ (\SMPSubscriber {subThreadId} -> readTVarIO subThreadId >>= mapM_ (deRefWeak >=> mapM_ killThread)) + liftIO $ closeSMPClientAgent smpAgent serverStatsThread_ :: NtfServerConfig -> [M ()] serverStatsThread_ NtfServerConfig {logStatsInterval = Just interval, logStatsStartTime, serverStatsLogFile} = @@ -218,33 +220,38 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge receiveSMP :: M () receiveSMP = forever $ do - ((_, srv, _), _, _, ntfId, msg) <- atomically $ readTBQueue msgQ - let smpQueue = SMPQueueNtf srv ntfId - case msg of - SMP.NMSG nmsgNonce encNMsgMeta -> do - ntfTs <- liftIO getSystemTime - st <- asks store - NtfPushServer {pushQ} <- asks pushServer - stats <- asks serverStats - atomically $ updatePeriodStats (activeSubs stats) ntfId - atomically $ - findNtfSubscriptionToken st smpQueue - >>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta})) - incNtfStat ntfReceived - SMP.END -> updateSubStatus smpQueue NSEnd - _ -> pure () + ((_, srv, _), _, _, ts) <- atomically $ readTBQueue msgQ + forM ts $ \(ntfId, t) -> case t of + STUnexpectedError e -> logError $ "SMP client unexpected error: " <> tshow e -- uncorrelated response, should not happen + STResponse {} -> pure () -- it was already reported as timeout error + STEvent msgOrErr -> do + let smpQueue = SMPQueueNtf srv ntfId + case msgOrErr of + Right (SMP.NMSG nmsgNonce encNMsgMeta) -> do + ntfTs <- liftIO getSystemTime + st <- asks store + NtfPushServer {pushQ} <- asks pushServer + stats <- asks serverStats + atomically $ updatePeriodStats (activeSubs stats) ntfId + atomically $ + findNtfSubscriptionToken st smpQueue + >>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta})) + incNtfStat ntfReceived + Right SMP.END -> updateSubStatus smpQueue NSEnd + Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e + Right _ -> logError $ "SMP server unexpected response" + Left e -> logError $ "SMP client error: " <> tshow e receiveAgent = forever $ atomically (readTBQueue agentQ) >>= \case - CAConnected _ -> pure () + CAConnected srv -> + logInfo $ "SMP server reconnected " <> showServer' srv CADisconnected srv subs -> do logSubStatus srv "disconnected" $ length subs forM_ subs $ \(_, ntfId) -> do let smpQueue = SMPQueueNtf srv ntfId updateSubStatus smpQueue NSInactive - CAReconnected srv -> - logInfo $ "SMP server reconnected " <> showServer' srv CAResubscribed srv subs -> do forM_ subs $ \(_, ntfId) -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive logSubStatus srv "resubscribed" $ length subs @@ -339,7 +346,7 @@ updateTknStatus NtfTknData {ntfTknId, tknStatus} status = do old <- atomically $ stateTVar tknStatus (,status) when (old /= status) $ withNtfLog $ \sl -> logTokenStatus sl ntfTknId status -runNtfClientTransport :: Transport c => THandleNTF c -> M () +runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M () runNtfClientTransport th@THandle {params} = do qSize <- asks $ clientQSize . config ts <- liftIO getSystemTime @@ -356,7 +363,7 @@ runNtfClientTransport th@THandle {params} = do clientDisconnected :: NtfServerClient -> IO () clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connected False -receive :: Transport c => THandleNTF c -> NtfServerClient -> M () +receive :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> M () receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do ts <- liftIO $ tGet th forM_ ts $ \t@(_, _, (corrId, entId, cmdOrError)) -> do @@ -371,7 +378,7 @@ receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ where write q t = atomically $ writeTBQueue q t -send :: Transport c => THandleNTF c -> NtfServerClient -> IO () +send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO () send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do t <- atomically $ readTBQueue sndQ void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)] @@ -382,7 +389,7 @@ send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do data VerificationResult = VRVerified NtfRequest | VRFailed -verifyNtfTransmission :: Maybe (THandleAuth, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult +verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult verifyNtfTransmission auth_ (tAuth, authorized, (corrId, entId, _)) cmd = do st <- asks store case cmd of diff --git a/src/Simplex/Messaging/Notifications/Server/Env.hs b/src/Simplex/Messaging/Notifications/Server/Env.hs index 4a93a8a34..5ebd5230e 100644 --- a/src/Simplex/Messaging/Notifications/Server/Env.hs +++ b/src/Simplex/Messaging/Notifications/Server/Env.hs @@ -24,17 +24,17 @@ import Numeric.Natural import Simplex.Messaging.Client.Agent import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol -import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Server.Stats import Simplex.Messaging.Notifications.Server.Store import Simplex.Messaging.Notifications.Server.StoreLog +import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF) import Simplex.Messaging.Protocol (CorrId, SMPServer, Transmission) import Simplex.Messaging.Server.Expiration import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, THandleParams) -import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams) +import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..)) +import Simplex.Messaging.Transport.Server (TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) import UnliftIO.STM @@ -84,7 +84,7 @@ data NtfEnv = NtfEnv } newNtfServerEnv :: NtfServerConfig -> IO NtfEnv -newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile} = do +newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do random <- liftIO C.newRandom store <- atomically newNtfStore logInfo "restoring subscriptions..." @@ -92,7 +92,7 @@ newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsCo logInfo "restored subscriptions" subscriber <- atomically $ newNtfSubscriber subQSize smpAgentCfg random pushServer <- atomically $ newNtfPushServer pushQSize apnsConfig - tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile + tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile serverStats <- atomically . newNtfServerStats =<< liftIO getCurrentTime pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats} @@ -161,13 +161,13 @@ data NtfRequest data NtfServerClient = NtfServerClient { rcvQ :: TBQueue NtfRequest, sndQ :: TBQueue (Transmission NtfResponse), - ntfThParams :: THandleParams NTFVersion, + ntfThParams :: THandleParams NTFVersion 'TServer, connected :: TVar Bool, rcvActiveAt :: TVar SystemTime, sndActiveAt :: TVar SystemTime } -newNtfServerClient :: Natural -> THandleParams NTFVersion -> SystemTime -> STM NtfServerClient +newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient newNtfServerClient qSize ntfThParams ts = do rcvQ <- newTBQueue qSize sndQ <- newTBQueue qSize diff --git a/src/Simplex/Messaging/Notifications/Server/Main.hs b/src/Simplex/Messaging/Notifications/Server/Main.hs index 2f6ee7e29..351fe6d72 100644 --- a/src/Simplex/Messaging/Notifications/Server/Main.hs +++ b/src/Simplex/Messaging/Notifications/Server/Main.hs @@ -11,29 +11,27 @@ import Data.Functor (($>)) import Data.Ini (lookupValue, readIniFile) import Data.Maybe (fromMaybe) import qualified Data.Text as T +import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative -import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Server (runNtfServer) import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration) import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig) -import Simplex.Messaging.Notifications.Transport (supportedServerNTFVRange) +import Simplex.Messaging.Notifications.Transport (supportedNTFHandshakes, supportedServerNTFVRange) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer) import Simplex.Messaging.Server.CLI import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) +import Simplex.Messaging.Util (tshow) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) import Text.Read (readMaybe) -defaultSMPBatchDelay :: Int -defaultSMPBatchDelay = 10000 - ntfServerCLI :: FilePath -> FilePath -> IO () ntfServerCLI cfgPath logPath = getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case @@ -69,7 +67,7 @@ ntfServerCLI cfgPath logPath = fp <- createServerX509 cfgPath x509cfg let host = fromMaybe (if ip == "127.0.0.1" then "" else ip) fqdn srv = ProtoServerWithAuth (NtfServer [THDomainName host] "" (C.KeyHash fp)) Nothing - writeFile iniFile $ iniFileContent host + T.writeFile iniFile $ iniFileContent host putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server." warnCAPrivateKeyFile cfgPath x509cfg printServiceInfo serverVersion srv @@ -85,17 +83,15 @@ ntfServerCLI cfgPath logPath = <> "log_stats: off\n\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" - <> ("host: " <> host <> "\n") - <> ("port: " <> defaultServerPort <> "\n") - <> "log_tls_errors: off\n\ - \# delay between command batches sent to SMP relays (microseconds), 0 to disable\n" - <> ("smp_batch_delay: " <> show defaultSMPBatchDelay <> "\n") + <> ("host: " <> T.pack host <> "\n") + <> ("port: " <> T.pack defaultServerPort <> "\n") + <> "log_tls_errors: off\n" <> "websockets: off\n\n\ \[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" - <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") - <> ("# check_interval: " <> show (checkInterval defaultInactiveClientExpiration) <> "\n") + <> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n") + <> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n") runServer ini = do hSetBuffering stdout LineBuffering hSetBuffering stderr LineBuffering @@ -111,8 +107,6 @@ ntfServerCLI cfgPath logPath = enableStoreLog = settingIsOn "STORE_LOG" "enable" ini logStats = settingIsOn "STORE_LOG" "log_stats" ini c = combine cfgPath . ($ defaultX509Config) - smpBatchDelay = readIniDefault defaultSMPBatchDelay "TRANSPORT" "smp_batch_delay" ini - batchDelay = if smpBatchDelay <= 0 then Nothing else Just smpBatchDelay serverConfig = NtfServerConfig { transports = iniTransports ini, @@ -121,7 +115,7 @@ ntfServerCLI cfgPath logPath = clientQSize = 64, subQSize = 512, pushQSize = 1048, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {batchDelay}}, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 0}, apnsConfig = defaultAPNSPushClientConfig, subsBatchSize = 900, inactiveClientExpiration = @@ -141,7 +135,8 @@ ntfServerCLI cfgPath logPath = ntfServerVRange = supportedServerNTFVRange, transportConfig = defaultTransportServerConfig - { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini + { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, + alpn = Just supportedNTFHandshakes } } diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index bc68fab03..077ce634e 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -5,11 +5,13 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} module Simplex.Messaging.Notifications.Transport where import Control.Monad (forM) import Control.Monad.Except +import Control.Monad.Trans.Except import Data.Attoparsec.ByteString.Char8 (Parser) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -18,9 +20,9 @@ import qualified Data.X509 as X import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Transport +import Simplex.Messaging.Util (liftEitherWith) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal -import Simplex.Messaging.Util (liftEitherWith) ntfBlockSize :: Int ntfBlockSize = 512 @@ -43,18 +45,24 @@ authBatchCmdsNTFVersion :: VersionNTF authBatchCmdsNTFVersion = VersionNTF 2 currentClientNTFVersion :: VersionNTF -currentClientNTFVersion = VersionNTF 1 +currentClientNTFVersion = VersionNTF 2 currentServerNTFVersion :: VersionNTF -currentServerNTFVersion = VersionNTF 1 +currentServerNTFVersion = VersionNTF 2 supportedClientNTFVRange :: VersionRangeNTF supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVersion +legacyServerNTFVRange :: VersionRangeNTF +legacyServerNTFVRange = mkVersionRange initialNTFVersion initialNTFVersion + supportedServerNTFVRange :: VersionRangeNTF supportedServerNTFVRange = mkVersionRange initialNTFVersion currentServerNTFVersion -type THandleNTF c = THandle NTFVersion c +supportedNTFHandshakes :: [ALPN] +supportedNTFHandshakes = ["ntf/1"] + +type THandleNTF c p = THandle NTFVersion c p data NtfServerHandshake = NtfServerHandshake { ntfVersionRange :: VersionRangeNTF, @@ -67,9 +75,7 @@ data NtfClientHandshake = NtfClientHandshake { -- | agreed SMP notifications server protocol version ntfVersion :: VersionNTF, -- | server identity - CA certificate fingerprint - keyHash :: C.KeyHash, - -- pub key to agree shared secret for entity ID encryption, shared secret for command authorization is agreed using per-queue keys. - authPubKey :: Maybe C.PublicKeyX25519 + keyHash :: C.KeyHash } instance Encoding NtfServerHandshake where @@ -94,62 +100,74 @@ authEncryptCmdsP :: VersionNTF -> Parser a -> Parser (Maybe a) authEncryptCmdsP v p = if v >= authBatchCmdsNTFVersion then Just <$> p else pure Nothing instance Encoding NtfClientHandshake where - smpEncode NtfClientHandshake {ntfVersion, keyHash, authPubKey} = - smpEncode (ntfVersion, keyHash) <> encodeNtfAuthPubKey ntfVersion authPubKey + smpEncode NtfClientHandshake {ntfVersion, keyHash} = + smpEncode (ntfVersion, keyHash) smpP = do (ntfVersion, keyHash) <- smpP - -- TODO drop SMP v6: remove special parser and make key non-optional - authPubKey <- ntfAuthPubKeyP ntfVersion - pure NtfClientHandshake {ntfVersion, keyHash, authPubKey} - -ntfAuthPubKeyP :: VersionNTF -> Parser (Maybe C.PublicKeyX25519) -ntfAuthPubKeyP v = if v >= authBatchCmdsNTFVersion then Just <$> smpP else pure Nothing - -encodeNtfAuthPubKey :: VersionNTF -> Maybe C.PublicKeyX25519 -> ByteString -encodeNtfAuthPubKey v k - | v >= authBatchCmdsNTFVersion = maybe "" smpEncode k - | otherwise = "" + pure NtfClientHandshake {ntfVersion, keyHash} -- | Notifcations server transport handshake. -ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c) +ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TServer) ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c let sk = C.signX509 serverSignKey $ C.publicToX509 k - sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange = ntfVRange, authPubKey = Just sk} + let ntfVersionRange = maybe legacyServerNTFVRange (const ntfVRange) $ getSessionALPN c + sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange, authPubKey = Just sk} getHandshake th >>= \case - NtfClientHandshake {ntfVersion = v, keyHash, authPubKey = k'} + NtfClientHandshake {ntfVersion = v, keyHash} | keyHash /= kh -> throwError $ TEHandshake IDENTITY - | v `isCompatible` ntfVRange -> - pure $ ntfThHandle th v pk k' - | otherwise -> throwError $ TEHandshake VERSION + | otherwise -> + case compatibleVRange' ntfVersionRange v of + Just (Compatible vr) -> pure $ ntfThHandleServer th v vr pk + Nothing -> throwE TEVersion -- | Notifcations server client transport handshake. -ntfClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c) -ntfClientHandshake c (k, pk) keyHash ntfVRange = do +ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient) +ntfClientHandshake c keyHash ntfVRange = do let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th if sessionId /= sessId then throwError TEBadSession - else case ntfVersionRange `compatibleVersion` ntfVRange of - Just (Compatible v) -> do - sk_ <- forM sk' $ \exact -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do + else case ntfVersionRange `compatibleVRange` ntfVRange of + Just (Compatible vr) -> do + ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do serverKey <- getServerVerifyKey c - pubKey <- C.verifyX509 serverKey exact - C.x509ToPublic (pubKey, []) >>= C.pubKey - sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash, authPubKey = Just k} - pure $ ntfThHandle th v pk sk_ - Nothing -> throwError $ TEHandshake VERSION + pubKey <- C.verifyX509 serverKey signedKey + (,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) + let v = maxVersion vr + sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash} + pure $ ntfThHandleClient th v vr ck_ + Nothing -> throwE TEVersion -ntfThHandle :: forall c. THandleNTF c -> VersionNTF -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleNTF c -ntfThHandle th@THandle {params} v privKey k_ = +ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> VersionRangeNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer +ntfThHandleServer th v vr pk = + let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing} + in ntfThHandle_ th v vr (Just thAuth) + +ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> VersionRangeNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient +ntfThHandleClient th v vr ck_ = + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = Nothing}) <$> ck_ + in ntfThHandle_ th v vr thAuth + +ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> VersionRangeNTF -> Maybe (THandleAuth p) -> THandleNTF c p +ntfThHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional - let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_ - v3 = v >= authBatchCmdsNTFVersion - params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3} - in (th :: THandleNTF c) {params = params'} + let v3 = v >= authBatchCmdsNTFVersion + params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v3, batch = v3} + in (th :: THandleNTF c p) {params = params'} -ntfTHandle :: Transport c => c -> THandleNTF c +ntfTHandle :: Transport c => c -> THandleNTF c p ntfTHandle c = THandle {connection = c, params} where - params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False} + v = VersionNTF 0 + params = + THandleParams + { sessionId = tlsUnique c, + blockSize = ntfBlockSize, + thVersion = v, + thServerVRange = versionToRange v, + thAuth = Nothing, + implySessId = False, + batch = False + } diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index dcf48fa16..615e8a6fe 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -43,6 +43,7 @@ module Simplex.Messaging.Protocol ( -- * SMP protocol parameters supportedSMPClientVRange, maxMessageLength, + paddedProxiedMsgLength, e2eEncConfirmationLength, e2eEncMessageLength, @@ -56,6 +57,7 @@ module Simplex.Messaging.Protocol SubscriptionMode (..), Party (..), Cmd (..), + DirectParty, BrokerMsg (..), SParty (..), PartyI (..), @@ -63,6 +65,8 @@ module Simplex.Messaging.Protocol ProtocolErrorType (..), ErrorType (..), CommandError (..), + ProxyError (..), + BrokerErrorType (..), Transmission, TransmissionAuth (..), SignedTransmission, @@ -121,6 +125,12 @@ module Simplex.Messaging.Protocol EncNMsgMeta, SMPMsgMeta (..), NMsgMeta (..), + EncFwdResponse (..), + EncFwdTransmission (..), + EncResponse (..), + EncTransmission (..), + FwdResponse (..), + FwdTransmission (..), MsgFlags (..), initialSMPClientVersion, currentSMPClientVersion, @@ -147,6 +157,7 @@ module Simplex.Messaging.Protocol sameSrvAddr, sameSrvAddr', noAuthSrv, + toMsgInfo, -- * TCP transport functions TransportBatch (..), @@ -169,6 +180,7 @@ where import Control.Applicative (optional, (<|>)) import Control.DeepSeq (NFData (..)) +import Control.Exception (Exception) import Control.Monad import Control.Monad.Except import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -187,19 +199,25 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Maybe (isJust, isNothing) import Data.String -import Data.Time.Clock.System (SystemTime (..)) +import qualified Data.Text as T +import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Data.Type.Equality import Data.Word (Word16) +import qualified Data.X509 as X import GHC.TypeLits (ErrorMessage (..), TypeError, type (+)) +import qualified GHC.TypeLits as TE +import qualified GHC.TypeLits as Type import Network.Socket (ServiceName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.ServiceScheme import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -230,9 +248,16 @@ currentSMPClientVersion = VersionSMPC 2 supportedSMPClientVRange :: VersionRangeSMPC supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClientVersion -maxMessageLength :: Int -maxMessageLength = 16088 +-- TODO v6.0 remove dependency on version +maxMessageLength :: VersionSMP -> Int +maxMessageLength v + | v >= sendingProxySMPVersion = 16064 -- max 16067 + | otherwise = 16088 -- 16064 - always use this size to determine allowed ranges +paddedProxiedMsgLength :: Int +paddedProxiedMsgLength = 16242 -- 16241 .. 16243 + +-- TODO v6.0 change to 16064 type MaxMessageLen = 16088 -- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used) @@ -240,13 +265,13 @@ type MaxRcvMessageLen = MaxMessageLen + 16 -- 16104, the padded size is 16106 -- it is shorter to allow per-queue e2e encryption DH key in the "public" header e2eEncConfirmationLength :: Int -e2eEncConfirmationLength = 15936 +e2eEncConfirmationLength = 15920 -- 15881 .. 15976 e2eEncMessageLength :: Int -e2eEncMessageLength = 16032 +e2eEncMessageLength = 16016 -- 16004 .. 16021 -- | SMP protocol clients -data Party = Recipient | Sender | Notifier +data Party = Recipient | Sender | Notifier | ProxiedClient deriving (Show) -- | Singleton types for SMP protocol clients @@ -254,11 +279,13 @@ data SParty :: Party -> Type where SRecipient :: SParty Recipient SSender :: SParty Sender SNotifier :: SParty Notifier + SProxiedClient :: SParty ProxiedClient instance TestEquality SParty where testEquality SRecipient SRecipient = Just Refl testEquality SSender SSender = Just Refl testEquality SNotifier SNotifier = Just Refl + testEquality SProxiedClient SProxiedClient = Just Refl testEquality _ _ = Nothing deriving instance Show (SParty p) @@ -271,6 +298,15 @@ instance PartyI Sender where sParty = SSender instance PartyI Notifier where sParty = SNotifier +instance PartyI ProxiedClient where sParty = SProxiedClient + +type family DirectParty (p :: Party) :: Constraint where + DirectParty Recipient = () + DirectParty Sender = () + DirectParty Notifier = () + DirectParty p = + (Int ~ Bool, TypeError (Type.Text "Party " :<>: ShowType p :<>: Type.Text " is not direct")) + -- | Type for client command of any participant. data Cmd = forall p. PartyI p => Cmd (SParty p) (Command p) @@ -354,6 +390,7 @@ data Command (p :: Party) where ACK :: MsgId -> Command Recipient OFF :: Command Recipient DEL :: Command Recipient + QUE :: Command Recipient -- SMP sender commands -- SEND v1 has to be supported for encoding/decoding -- SEND :: MsgBody -> Command Sender @@ -361,6 +398,17 @@ data Command (p :: Party) where PING :: Command Sender -- SMP notification subscriber commands NSUB :: Command Notifier + PRXY :: SMPServer -> Maybe BasicAuth -> Command ProxiedClient -- request a relay server connection by URI + -- Transmission to proxy: + -- - entity ID: ID of the session with relay returned in PKEY (response to PRXY) + -- - corrId: also used as a nonce to encrypt transmission to relay, corrId + 1 - from relay + -- - key (1st param in the command) is used to agree DH secret for this particular transmission and its response + -- Encrypted transmission should include session ID (tlsunique) from proxy-relay connection. + PFWD :: VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> Command ProxiedClient -- use CorrId as CbNonce, client to proxy + -- Transmission forwarded to relay: + -- - entity ID: empty + -- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission + RFWD :: EncFwdTransmission -> Command Sender -- use CorrId as CbNonce, proxy to relay deriving instance Show (Command p) @@ -386,6 +434,26 @@ instance Encoding SubscriptionMode where 'C' -> pure SMOnlyCreate _ -> fail "bad SubscriptionMode" +newtype EncTransmission = EncTransmission ByteString + deriving (Show) + +data FwdTransmission = FwdTransmission + { fwdCorrId :: CorrId, + fwdVersion :: VersionSMP, + fwdKey :: C.PublicKeyX25519, + fwdTransmission :: EncTransmission + } + +instance Encoding FwdTransmission where + smpEncode FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} = + smpEncode (corrId, fwdVersion, fwdKey, Tail t) + smpP = do + (corrId, fwdVersion, fwdKey, Tail t) <- smpP + pure FwdTransmission {fwdCorrId = CorrId corrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission t} + +newtype EncFwdTransmission = EncFwdTransmission ByteString + deriving (Show) + data BrokerMsg where -- SMP broker messages (responses, client messages, notifications) IDS :: QueueIdsKeys -> BrokerMsg @@ -395,7 +463,12 @@ data BrokerMsg where MSG :: RcvMessage -> BrokerMsg NID :: NotifierId -> RcvNtfPublicDhKey -> BrokerMsg NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg + -- Should include certificate chain + PKEY :: SessionId -> VersionRangeSMP -> (X.CertificateChain, X.SignedExact X.PubKey) -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key + RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy + PRES :: EncResponse -> BrokerMsg -- proxy to client END :: BrokerMsg + INFO :: QueueInfo -> BrokerMsg OK :: BrokerMsg ERR :: ErrorType -> BrokerMsg PONG :: BrokerMsg @@ -407,6 +480,24 @@ data RcvMessage = RcvMessage } deriving (Eq, Show) +newtype EncFwdResponse = EncFwdResponse ByteString + deriving (Eq, Show) + +data FwdResponse = FwdResponse + { fwdCorrId :: CorrId, + fwdResponse :: EncResponse + } + +instance Encoding FwdResponse where + smpEncode FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} = + smpEncode (corrId, Tail t) + smpP = do + (corrId, Tail t) <- smpP + pure FwdResponse {fwdCorrId = CorrId corrId, fwdResponse = EncResponse t} + +newtype EncResponse = EncResponse ByteString + deriving (Eq, Show) + -- | received message without server/recipient encryption data Message = Message @@ -420,6 +511,13 @@ data Message msgTs :: SystemTime } +toMsgInfo :: Message -> MsgInfo +toMsgInfo = \case + Message {msgId, msgTs} -> msgInfo msgId msgTs MTMessage + MessageQuota {msgId, msgTs} -> msgInfo msgId msgTs MTQuota + where + msgInfo msgId msgTs msgType = MsgInfo {msgId = decodeLatin1 $ B64.encode msgId, msgTs = systemToUTCTime msgTs, msgType} + messageId :: Message -> MsgId messageId = \case Message {msgId} -> msgId @@ -567,8 +665,12 @@ data CommandTag (p :: Party) where ACK_ :: CommandTag Recipient OFF_ :: CommandTag Recipient DEL_ :: CommandTag Recipient + QUE_ :: CommandTag Recipient SEND_ :: CommandTag Sender PING_ :: CommandTag Sender + PRXY_ :: CommandTag ProxiedClient + PFWD_ :: CommandTag ProxiedClient + RFWD_ :: CommandTag Sender NSUB_ :: CommandTag Notifier data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p) @@ -582,7 +684,11 @@ data BrokerMsgTag | MSG_ | NID_ | NMSG_ + | PKEY_ + | RRES_ + | PRES_ | END_ + | INFO_ | OK_ | ERR_ | PONG_ @@ -607,8 +713,12 @@ instance PartyI p => Encoding (CommandTag p) where ACK_ -> "ACK" OFF_ -> "OFF" DEL_ -> "DEL" + QUE_ -> "QUE" SEND_ -> "SEND" PING_ -> "PING" + PRXY_ -> "PRXY" + PFWD_ -> "PFWD" + RFWD_ -> "RFWD" NSUB_ -> "NSUB" smpP = messageTagP @@ -623,8 +733,12 @@ instance ProtocolMsgTag CmdTag where "ACK" -> Just $ CT SRecipient ACK_ "OFF" -> Just $ CT SRecipient OFF_ "DEL" -> Just $ CT SRecipient DEL_ + "QUE" -> Just $ CT SRecipient QUE_ "SEND" -> Just $ CT SSender SEND_ "PING" -> Just $ CT SSender PING_ + "PRXY" -> Just $ CT SProxiedClient PRXY_ + "PFWD" -> Just $ CT SProxiedClient PFWD_ + "RFWD" -> Just $ CT SSender RFWD_ "NSUB" -> Just $ CT SNotifier NSUB_ _ -> Nothing @@ -641,7 +755,11 @@ instance Encoding BrokerMsgTag where MSG_ -> "MSG" NID_ -> "NID" NMSG_ -> "NMSG" + PKEY_ -> "PKEY" + RRES_ -> "RRES" + PRES_ -> "PRES" END_ -> "END" + INFO_ -> "INFO" OK_ -> "OK" ERR_ -> "ERR" PONG_ -> "PONG" @@ -653,7 +771,11 @@ instance ProtocolMsgTag BrokerMsgTag where "MSG" -> Just MSG_ "NID" -> Just NID_ "NMSG" -> Just NMSG_ + "PKEY" -> Just PKEY_ + "RRES" -> Just RRES_ + "PRES" -> Just PRES_ "END" -> Just END_ + "INFO" -> Just INFO_ "OK" -> Just OK_ "ERR" -> Just ERR_ "PONG" -> Just PONG_ @@ -833,7 +955,7 @@ type family UserProtocol (p :: ProtocolType) :: Constraint where UserProtocol PSMP = () UserProtocol PXFTP = () UserProtocol a = - (Int ~ Bool, TypeError (Text "Servers for protocol " :<>: ShowType a :<>: Text " cannot be configured by the users")) + (Int ~ Bool, TypeError (TE.Text "Servers for protocol " :<>: ShowType a :<>: TE.Text " cannot be configured by the users")) userProtocol :: SProtocolType p -> Maybe (Dict (UserProtocol p)) userProtocol = \case @@ -1044,14 +1166,20 @@ data ErrorType SESSION | -- | SMP command is unknown or has invalid syntax CMD {cmdErr :: CommandError} + | -- | error from proxied relay + PROXY {proxyErr :: ProxyError} | -- | command authorization error - bad signature or non-existing SMP queue AUTH + | -- | encryption/decryption error in proxy protocol + CRYPTO | -- | SMP queue capacity is exceeded on the server QUOTA | -- | ACK command is sent without message to be acknowledged NO_MSG | -- | sent message is too large (> maxMessageLength = 16088 bytes) LARGE_MSG + | -- | relay public key is expired + EXPIRED | -- | internal server error INTERNAL | -- | used internally, never returned by the server (to be removed) @@ -1061,8 +1189,12 @@ data ErrorType instance StrEncoding ErrorType where strEncode = \case CMD e -> "CMD " <> bshow e + PROXY e -> "PROXY " <> strEncode e e -> bshow e - strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1 + strP = + "CMD " *> (CMD <$> parseRead1) + <|> "PROXY " *> (PROXY <$> strP) + <|> parseRead1 -- | SMP command error type. data CommandError @@ -1080,8 +1212,36 @@ data CommandError NO_ENTITY deriving (Eq, Read, Show) +data ProxyError + = -- | Correctly parsed SMP server ERR response. + -- This error is forwarded to the agent client as AgentErrorType `ERR PROXY PROTOCOL err`. + PROTOCOL {protocolErr :: ErrorType} + | -- | destination server error + BROKER {brokerErr :: BrokerErrorType} + | -- | basic auth provided to proxy is invalid + BASIC_AUTH + | -- no destination server error + NO_SESSION + deriving (Eq, Read, Show) + +-- | SMP server errors. +data BrokerErrorType + = -- | invalid server response (failed to parse) + RESPONSE {respErr :: String} + | -- | unexpected response + UNEXPECTED {respErr :: String} + | -- | network error + NETWORK + | -- | no compatible server host (e.g. onion when public is required, or vice versa) + HOST + | -- | handshake or other transport error + TRANSPORT {transportErr :: TransportError} + | -- | command response timeout + TIMEOUT + deriving (Eq, Read, Show, Exception) + -- | SMP transmission parser. -transmissionP :: THandleParams v -> Parser RawTransmission +transmissionP :: THandleParams v p -> Parser RawTransmission transmissionP THandleParams {sessionId, implySessId} = do authenticator <- smpP authorized <- A.takeByteString @@ -1095,10 +1255,10 @@ transmissionP THandleParams {sessionId, implySessId} = do command <- A.takeByteString pure RawTransmission {authenticator, authorized = authorized', sessId, corrId, entityId, command} -class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where +class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where type ProtoCommand msg = cmd | cmd -> msg type ProtoType msg = (sch :: ProtocolType) | sch -> msg - protocolClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c) + protocolClientHandshake :: forall c. Transport c => c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c 'TClient) protocolPing :: ProtoCommand msg protocolError :: msg -> Maybe err @@ -1138,9 +1298,13 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where ACK msgId -> e (ACK_, ' ', msgId) OFF -> e OFF_ DEL -> e DEL_ + QUE -> e QUE_ SEND flags msg -> e (SEND_, ' ', flags, ' ', Tail msg) PING -> e PING_ NSUB -> e NSUB_ + PRXY host auth_ -> e (PRXY_, ' ', host, auth_) + PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s) + RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s) where e :: Encoding a => a -> ByteString e = smpEncode @@ -1150,24 +1314,33 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg {-# INLINE fromProtocolError #-} - checkCredentials (auth, _, queueId, _) cmd = case cmd of + checkCredentials (auth, _, entId, _) cmd = case cmd of -- NEW must have signature but NOT queue ID NEW {} | isNothing auth -> Left $ CMD NO_AUTH - | not (B.null queueId) -> Left $ CMD HAS_AUTH + | not (B.null entId) -> Left $ CMD HAS_AUTH | otherwise -> Right cmd -- SEND must have queue ID, signature is not always required SEND {} - | B.null queueId -> Left $ CMD NO_ENTITY + | B.null entId -> Left $ CMD NO_ENTITY | otherwise -> Right cmd - -- PING must not have queue ID or signature - PING - | isNothing auth && B.null queueId -> Right cmd + PING -> noAuthCmd + PRXY {} -> noAuthCmd + PFWD {} + | B.null entId -> Left $ CMD NO_ENTITY + | isNothing auth -> Right cmd | otherwise -> Left $ CMD HAS_AUTH + RFWD _ -> noAuthCmd -- other client commands must have both signature and queue ID _ - | isNothing auth || B.null queueId -> Left $ CMD NO_AUTH + | isNothing auth || B.null entId -> Left $ CMD NO_AUTH | otherwise -> Right cmd + where + -- command must not have entity ID (queue or session ID) or signature + noAuthCmd :: Either ErrorType (Command p) + noAuthCmd + | isNothing auth && B.null entId = Right cmd + | otherwise = Left $ CMD HAS_AUTH instance ProtocolEncoding SMPVersion ErrorType Cmd where type Tag Cmd = CmdTag @@ -1191,10 +1364,16 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where ACK_ -> ACK <$> _smpP OFF_ -> pure OFF DEL_ -> pure DEL + QUE_ -> pure QUE CT SSender tag -> Cmd SSender <$> case tag of SEND_ -> SEND <$> _smpP <*> (unTail <$> _smpP) PING_ -> pure PING + RFWD_ -> RFWD <$> (EncFwdTransmission . unTail <$> _smpP) + CT SProxiedClient tag -> + Cmd SProxiedClient <$> case tag of + PFWD_ -> PFWD <$> _smpP <*> smpP <*> (EncTransmission . unTail <$> smpP) + PRXY_ -> PRXY <$> _smpP <*> smpP CT SNotifier NSUB_ -> pure $ Cmd SNotifier NSUB fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg @@ -1210,7 +1389,11 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where e (MSG_, ' ', msgId, Tail body) NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta) + PKEY sid vr (cert, key) -> e (PKEY_, ' ', sid, vr, C.encodeCertChain cert, C.SignedObject key) + RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) + PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) END -> e END_ + INFO info -> e (INFO_, ' ', info) OK -> e OK_ ERR err -> e (ERR_, ' ', err) PONG -> e PONG_ @@ -1227,7 +1410,11 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where IDS_ -> IDS <$> (QIK <$> _smpP <*> smpP <*> smpP) NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP + PKEY_ -> PKEY <$> _smpP <*> smpP <*> ((,) <$> C.certChainP <*> (C.getSignedExact <$> smpP)) + RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) + PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) END_ -> pure END + INFO_ -> INFO <$> _smpP OK_ -> pure OK ERR_ -> ERR <$> _smpP PONG_ -> pure PONG @@ -1239,19 +1426,24 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where PEBlock -> BLOCK {-# INLINE fromProtocolError #-} - checkCredentials (_, _, queueId, _) cmd = case cmd of + checkCredentials (_, _, entId, _) cmd = case cmd of -- IDS response should not have queue ID IDS _ -> Right cmd -- ERR response does not always have queue ID ERR _ -> Right cmd -- PONG response must not have queue ID - PONG - | B.null queueId -> Right cmd - | otherwise -> Left $ CMD HAS_AUTH + PONG -> noEntityMsg + PKEY {} -> noEntityMsg + RRES _ -> noEntityMsg -- other broker responses must have queue ID _ - | B.null queueId -> Left $ CMD NO_ENTITY + | B.null entId -> Left $ CMD NO_ENTITY | otherwise -> Right cmd + where + noEntityMsg :: Either ErrorType BrokerMsg + noEntityMsg + | B.null entId = Right cmd + | otherwise = Left $ CMD HAS_AUTH -- | Parse SMP protocol commands and broker messages parseProtocol :: forall v err msg. ProtocolEncoding v err msg => Version v -> ByteString -> Either err msg @@ -1274,8 +1466,11 @@ instance Encoding ErrorType where BLOCK -> "BLOCK" SESSION -> "SESSION" CMD err -> "CMD " <> smpEncode err + PROXY err -> "PROXY " <> smpEncode err AUTH -> "AUTH" + CRYPTO -> "CRYPTO" QUOTA -> "QUOTA" + EXPIRED -> "EXPIRED" NO_MSG -> "NO_MSG" LARGE_MSG -> "LARGE_MSG" INTERNAL -> "INTERNAL" @@ -1286,13 +1481,16 @@ instance Encoding ErrorType where "BLOCK" -> pure BLOCK "SESSION" -> pure SESSION "CMD" -> CMD <$> _smpP + "PROXY" -> PROXY <$> _smpP "AUTH" -> pure AUTH + "CRYPTO" -> pure CRYPTO "QUOTA" -> pure QUOTA + "EXPIRED" -> pure EXPIRED "NO_MSG" -> pure NO_MSG "LARGE_MSG" -> pure LARGE_MSG "INTERNAL" -> pure INTERNAL "DUPLICATE_" -> pure DUPLICATE_ - _ -> fail "bad error type" + _ -> fail "bad ErrorType" instance Encoding CommandError where smpEncode e = case e of @@ -1310,11 +1508,77 @@ instance Encoding CommandError where "NO_AUTH" -> pure NO_AUTH "HAS_AUTH" -> pure HAS_AUTH "NO_ENTITY" -> pure NO_ENTITY - "NO_QUEUE" -> pure NO_ENTITY - _ -> fail "bad command error type" + "NO_QUEUE" -> pure NO_ENTITY -- for backward compatibility + _ -> fail "bad CommandError" + +instance Encoding ProxyError where + smpEncode = \case + PROTOCOL e -> "PROTOCOL " <> smpEncode e + BROKER e -> "BROKER " <> smpEncode e + BASIC_AUTH -> "BASIC_AUTH" + NO_SESSION -> "NO_SESSION" + smpP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> PROTOCOL <$> _smpP + "BROKER" -> BROKER <$> _smpP + "BASIC_AUTH" -> pure BASIC_AUTH + "NO_SESSION" -> pure NO_SESSION + _ -> fail "bad ProxyError" + +instance StrEncoding ProxyError where + strEncode = \case + PROTOCOL e -> "PROTOCOL " <> strEncode e + BROKER e -> "BROKER " <> strEncode e + BASIC_AUTH -> "BASIC_AUTH" + NO_SESSION -> "NO_SESSION" + strP = + A.takeTill (== ' ') >>= \case + "PROTOCOL" -> PROTOCOL <$> _strP + "BROKER" -> BROKER <$> _strP + "BASIC_AUTH" -> pure BASIC_AUTH + "NO_SESSION" -> pure NO_SESSION + _ -> fail "bad ProxyError" + +instance Encoding BrokerErrorType where + smpEncode = \case + RESPONSE e -> "RESPONSE " <> smpEncode e + UNEXPECTED e -> "UNEXPECTED " <> smpEncode e + TRANSPORT e -> "TRANSPORT " <> smpEncode e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + smpP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _smpP + "UNEXPECTED" -> UNEXPECTED <$> _smpP + "TRANSPORT" -> TRANSPORT <$> _smpP + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" + +instance StrEncoding BrokerErrorType where + strEncode = \case + RESPONSE e -> "RESPONSE " <> encodeUtf8 (T.pack e) + UNEXPECTED e -> "UNEXPECTED " <> encodeUtf8 (T.pack e) + TRANSPORT e -> "TRANSPORT " <> smpEncode e + NETWORK -> "NETWORK" + TIMEOUT -> "TIMEOUT" + HOST -> "HOST" + strP = + A.takeTill (== ' ') >>= \case + "RESPONSE" -> RESPONSE <$> _textP + "UNEXPECTED" -> UNEXPECTED <$> _textP + "TRANSPORT" -> TRANSPORT <$> _smpP + "NETWORK" -> pure NETWORK + "TIMEOUT" -> pure TIMEOUT + "HOST" -> pure HOST + _ -> fail "bad BrokerErrorType" + where + _textP = A.space *> (T.unpack . safeDecodeUtf8 <$> A.takeByteString) -- | Send signed SMP transmission to TCP transport. -tPut :: Transport c => THandle v c -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] +tPut :: Transport c => THandle v c p -> NonEmpty (Either TransportError SentRawTransmission) -> IO [Either TransportError ()] tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (batch params) (blockSize params) where tPutBatch :: TransportBatch () -> IO [Either TransportError ()] @@ -1323,7 +1587,7 @@ tPut th@THandle {params} = fmap concat . mapM tPutBatch . batchTransmissions (ba TBTransmissions s n _ -> replicate n <$> tPutLog th s TBTransmission s _ -> (: []) <$> tPutLog th s -tPutLog :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ()) +tPutLog :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutLog th s = do r <- tPutBlock th s case r of @@ -1389,7 +1653,7 @@ tEncodeBatch1 t = lenEncode 1 `B.cons` tEncodeForBatch t -- tForAuth is lazy to avoid computing it when there is no key to sign data TransmissionForAuth = TransmissionForAuth {tForAuth :: ~ByteString, tToSend :: ByteString} -encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> TransmissionForAuth +encodeTransmissionForAuth :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> TransmissionForAuth encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t = TransmissionForAuth {tForAuth, tToSend = if implySessId then t' else tForAuth} where @@ -1397,7 +1661,7 @@ encodeTransmissionForAuth THandleParams {thVersion = v, sessionId, implySessId} t' = encodeTransmission_ v t {-# INLINE encodeTransmissionForAuth #-} -encodeTransmission :: ProtocolEncoding v e c => THandleParams v -> Transmission c -> ByteString +encodeTransmission :: ProtocolEncoding v e c => THandleParams v p -> Transmission c -> ByteString encodeTransmission THandleParams {thVersion = v, sessionId, implySessId} t = if implySessId then t' else smpEncode sessionId <> t' where @@ -1410,11 +1674,11 @@ encodeTransmission_ v (CorrId corrId, queueId, command) = {-# INLINE encodeTransmission_ #-} -- | Receive and parse transmission from the TCP transport (ignoring any trailing padding). -tGetParse :: Transport c => THandle v c -> IO (NonEmpty (Either TransportError RawTransmission)) +tGetParse :: Transport c => THandle v c p -> IO (NonEmpty (Either TransportError RawTransmission)) tGetParse th@THandle {params} = eitherList (tParse params) <$> tGetBlock th {-# INLINE tGetParse #-} -tParse :: THandleParams v -> ByteString -> NonEmpty (Either TransportError RawTransmission) +tParse :: THandleParams v p -> ByteString -> NonEmpty (Either TransportError RawTransmission) tParse thParams@THandleParams {batch} s | batch = eitherList (L.map (\(Large t) -> tParse1 t)) ts | otherwise = [tParse1 s] @@ -1426,10 +1690,10 @@ eitherList :: (a -> NonEmpty (Either e b)) -> Either e a -> NonEmpty (Either e b eitherList = either (\e -> [Left e]) -- | Receive client and server transmissions (determined by `cmd` type). -tGet :: forall v err cmd c. (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (NonEmpty (SignedTransmission err cmd)) +tGet :: forall v err cmd c p. (ProtocolEncoding v err cmd, Transport c) => THandle v c p -> IO (NonEmpty (SignedTransmission err cmd)) tGet th@THandle {params} = L.map (tDecodeParseValidate params) <$> tGetParse th -tDecodeParseValidate :: forall v err cmd. ProtocolEncoding v err cmd => THandleParams v -> Either TransportError RawTransmission -> SignedTransmission err cmd +tDecodeParseValidate :: forall v p err cmd. ProtocolEncoding v err cmd => THandleParams v p -> Either TransportError RawTransmission -> SignedTransmission err cmd tDecodeParseValidate THandleParams {sessionId, thVersion = v, implySessId} = \case Right RawTransmission {authenticator, authorized, sessId, corrId, entityId, command} | implySessId || sessId == sessionId -> @@ -1450,4 +1714,7 @@ $(J.deriveJSON defaultJSON ''MsgFlags) $(J.deriveJSON (sumTypeJSON id) ''CommandError) -$(J.deriveJSON (sumTypeJSON id) ''ErrorType) +$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType) + +-- run deriveJSON in one TH splice to allow mutual instance +$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''ErrorType]) diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 4535c7bd5..88e3bf4a3 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -6,6 +6,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} @@ -13,7 +14,6 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} -{-# LANGUAGE TypeApplications #-} -- | -- Module : Simplex.Messaging.Server @@ -43,7 +43,9 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Control.Monad.Trans.Except import Crypto.Random +import Control.Monad.STM (retry) import Data.Bifunctor (first) import Data.ByteString.Base64 (encode) import Data.ByteString.Char8 (ByteString) @@ -53,10 +55,11 @@ import Data.Either (fromRight, partitionEithers) import Data.Functor (($>)) import Data.Int (Int64) import qualified Data.IntMap.Strict as IM -import Data.List (intercalate) +import Data.List (intercalate, mapAccumR) +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M -import Data.Maybe (isNothing) +import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime) @@ -67,8 +70,10 @@ import GHC.Stats (getRTSStats) import GHC.TypeLits (KnownNat) import Network.Socket (ServiceName, Socket, socketToHandle) import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Client (ProtocolClient (thParams), ProtocolClientError (..), SMPClient, SMPClientError, forwardSMPMessage, smpProxyError, temporaryClientError) +import Simplex.Messaging.Client.Agent (OwnServer, SMPClientAgent (..), SMPClientAgentEvent (..), closeSMPClientAgent, getSMPServerClient'', isOwnServer, lookupSMPServerClient, getConnectedSMPServerClient) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Encoding (Encoding (smpEncode)) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Control @@ -77,6 +82,7 @@ import Simplex.Messaging.Server.Expiration import Simplex.Messaging.Server.MsgStore import Simplex.Messaging.Server.MsgStore.STM import Simplex.Messaging.Server.QueueStore +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.Server.QueueStore.STM as QS import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog @@ -86,6 +92,7 @@ import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Buffer (trimCR) import Simplex.Messaging.Transport.Server import Simplex.Messaging.Util +import Simplex.Messaging.Version import System.Exit (exitFailure) import System.IO (hPrint, hPutStrLn, hSetNewlineMode, universalNewlineMode) import System.Mem.Weak (deRefWeak) @@ -97,7 +104,6 @@ import UnliftIO.IO import UnliftIO.STM #if MIN_VERSION_base(4,18,0) import Data.List (sort) -import Data.Maybe (fromMaybe) import GHC.Conc (listThreads, threadStatus) import GHC.Conc.Sync (threadLabel) #endif @@ -122,14 +128,16 @@ type M a = ReaderT Env IO a smpServer :: TMVar Bool -> ServerConfig -> M () smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do s <- asks server + pa <- asks proxyAgent expired <- restoreServerMessages restoreServerStats expired raceAny_ ( serverThread s "server subscribedQ" subscribedQ subscribers subscriptions cancelSub : serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubscriptions (\_ -> pure ()) + : receiveFromProxyAgent pa : map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg ) - `finally` withLock' (savingLock s) "final" (saveServer False) + `finally` withLock' (savingLock s) "final" (saveServer False >> closeServer) where runServer :: (ServiceName, ATransport) -> M () runServer (tcpPort, ATransport t) = do @@ -143,6 +151,9 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do saveServer :: Bool -> M () saveServer keepMsgs = withLog closeStoreLog >> saveServerMessages keepMsgs >> saveServerStats + closeServer :: M () + closeServer = asks (smpAgent . proxyAgent) >>= liftIO . closeSMPClientAgent + serverThread :: forall s. Server -> @@ -171,14 +182,22 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do TM.lookupInsert qId clnt (subs s) $>>= clientToBeNotified endPreviousSubscriptions :: (QueueId, Client) -> M (Maybe s) endPreviousSubscriptions (qId, c) = do - tId <- atomically $ stateTVar (endThreadSeq c) $ \next -> (next, next + 1) - t <- forkIO $ do - labelMyThread $ label <> ".endPreviousSubscriptions" + forkClient c (label <> ".endPreviousSubscriptions") $ atomically $ writeTBQueue (sndQ c) [(CorrId "", qId, END)] - atomically $ modifyTVar' (endThreads c) $ IM.delete tId - mkWeakThreadId t >>= atomically . modifyTVar' (endThreads c) . IM.insert tId atomically $ TM.lookupDelete qId (clientSubs c) + receiveFromProxyAgent :: ProxyAgent -> M () + receiveFromProxyAgent ProxyAgent {smpAgent = SMPClientAgent {agentQ}} = + forever $ + atomically (readTBQueue agentQ) >>= \case + CAConnected srv -> logInfo $ "SMP server connected " <> showServer' srv + CADisconnected srv [] -> logInfo $ "SMP server disconnected " <> showServer' srv + CADisconnected srv subs -> logError $ "SMP server disconnected " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CAResubscribed srv subs -> logError $ "SMP server resubscribed " <> showServer' srv <> " / subscriptions: " <> tshow (length subs) + CASubError srv errs -> logError $ "SMP server subscription errors " <> showServer' srv <> " / errors: " <> tshow (length errs) + where + showServer' = decodeLatin1 . strEncode . host + expireMessagesThread_ :: ServerConfig -> [M ()] expireMessagesThread_ ServerConfig {messageExpiration = Just msgExp} = [expireMessages msgExp] expireMessagesThread_ _ = [] @@ -210,7 +229,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath liftIO $ threadDelay' $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0) - ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount} <- asks serverStats + ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, qSub, qSubAuth, qSubDuplicate, qSubProhibited, msgSent, msgSentAuth, msgSentQuota, msgSentLarge, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv} <- asks serverStats let interval = 1000000 * logInterval forever $ do withFile statsFilePath AppendMode $ \h -> liftIO $ do @@ -222,39 +241,68 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do qDeletedAll' <- atomically $ swapTVar qDeletedAll 0 qDeletedNew' <- atomically $ swapTVar qDeletedNew 0 qDeletedSecured' <- atomically $ swapTVar qDeletedSecured 0 + qSub' <- atomically $ swapTVar qSub 0 + qSubAuth' <- atomically $ swapTVar qSubAuth 0 + qSubDuplicate' <- atomically $ swapTVar qSubDuplicate 0 + qSubProhibited' <- atomically $ swapTVar qSubProhibited 0 msgSent' <- atomically $ swapTVar msgSent 0 + msgSentAuth' <- atomically $ swapTVar msgSentAuth 0 + msgSentQuota' <- atomically $ swapTVar msgSentQuota 0 + msgSentLarge' <- atomically $ swapTVar msgSentLarge 0 msgRecv' <- atomically $ swapTVar msgRecv 0 msgExpired' <- atomically $ swapTVar msgExpired 0 ps <- atomically $ periodStatCounts activeQueues ts msgSentNtf' <- atomically $ swapTVar msgSentNtf 0 msgRecvNtf' <- atomically $ swapTVar msgRecvNtf 0 psNtf <- atomically $ periodStatCounts activeQueuesNtf ts + pRelays' <- atomically $ getResetProxyStatsData pRelays + pRelaysOwn' <- atomically $ getResetProxyStatsData pRelaysOwn + pMsgFwds' <- atomically $ getResetProxyStatsData pMsgFwds + pMsgFwdsOwn' <- atomically $ getResetProxyStatsData pMsgFwdsOwn + pMsgFwdsRecv' <- atomically $ swapTVar pMsgFwdsRecv 0 qCount' <- readTVarIO qCount msgCount' <- readTVarIO msgCount hPutStrLn h $ intercalate "," - [ iso8601Show $ utctDay fromTime', - show qCreated', - show qSecured', - show qDeletedAll', - show msgSent', - show msgRecv', - dayCount ps, - weekCount ps, - monthCount ps, - show msgSentNtf', - show msgRecvNtf', - dayCount psNtf, - weekCount psNtf, - monthCount psNtf, - show qCount', - show msgCount', - show msgExpired', - show qDeletedNew', - show qDeletedSecured' - ] + ( [ iso8601Show $ utctDay fromTime', + show qCreated', + show qSecured', + show qDeletedAll', + show msgSent', + show msgRecv', + dayCount ps, + weekCount ps, + monthCount ps, + show msgSentNtf', + show msgRecvNtf', + dayCount psNtf, + weekCount psNtf, + monthCount psNtf, + show qCount', + show msgCount', + show msgExpired', + show qDeletedNew', + show qDeletedSecured' + ] + <> showProxyStats pRelays' + <> showProxyStats pRelaysOwn' + <> showProxyStats pMsgFwds' + <> showProxyStats pMsgFwdsOwn' + <> [ show pMsgFwdsRecv', + show qSub', + show qSubAuth', + show qSubDuplicate', + show qSubProhibited', + show msgSentAuth', + show msgSentQuota', + show msgSentLarge' + ] + ) liftIO $ threadDelay' interval + where + showProxyStats ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} = + [show _pRequests, show _pSuccesses, show _pErrorsConnect, show _pErrorsCompat, show _pErrorsOther] runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M () runClient signKey tp h = do @@ -314,7 +362,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do CPResume -> withAdminRole $ hPutStrLn h "resume not implemented" CPClients -> withAdminRole $ do active <- unliftIO u (asks clients) >>= readTVarIO - hPutStrLn h $ "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions" + hPutStrLn h "clientId,sessionId,connected,createdAt,rcvActiveAt,sndActiveAt,age,subscriptions" forM_ (IM.toList active) $ \(cid, Client {sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, subscriptions}) -> do connected' <- bshow <$> readTVarIO connected rcvActiveAt' <- strEncode <$> readTVarIO rcvActiveAt @@ -324,7 +372,13 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do subscriptions' <- bshow . M.size <$> readTVarIO subscriptions hPutStrLn h . B.unpack $ B.intercalate "," [bshow cid, encode sessionId, connected', strEncode createdAt, rcvActiveAt', sndActiveAt', bshow age, subscriptions'] CPStats -> withAdminRole $ do - ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgSentNtf, msgRecvNtf, qCount, msgCount} <- unliftIO u $ asks serverStats + ss <- unliftIO u $ asks serverStats + let putStat :: Show a => ByteString -> (ServerStats -> TVar a) -> IO () + putStat label var = readTVarIO (var ss) >>= \v -> B.hPutStr h $ label <> ": " <> bshow v <> "\n" + putProxyStat :: ByteString -> (ServerStats -> ProxyStats) -> IO () + putProxyStat label var = do + ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} <- atomically $ getProxyStatsData $ var ss + B.hPutStr h $ label <> ": requests=" <> bshow _pRequests <> ", successes=" <> bshow _pSuccesses <> ", errorsConnect=" <> bshow _pErrorsConnect <> ", errorsCompat=" <> bshow _pErrorsCompat <> ", errorsOther=" <> bshow _pErrorsOther <> "\n" putStat "fromTime" fromTime putStat "qCreated" qCreated putStat "qSecured" qSecured @@ -337,9 +391,11 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do putStat "msgRecvNtf" msgRecvNtf putStat "qCount" qCount putStat "msgCount" msgCount - where - putStat :: Show a => String -> TVar a -> IO () - putStat label var = readTVarIO var >>= \v -> hPutStrLn h $ label <> ": " <> show v + putProxyStat "pRelays" pRelays + putProxyStat "pRelaysOwn" pRelaysOwn + putProxyStat "pMsgFwds" pMsgFwds + putProxyStat "pMsgFwdsOwn" pMsgFwdsOwn + putStat "pMsgFwdsRecv" pMsgFwdsRecv CPStatsRTS -> getRTSStats >>= hPrint h CPThreads -> withAdminRole $ do #if MIN_VERSION_base(4,18,0) @@ -409,8 +465,8 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do logError "Unauthorized control port command" hPutStrLn h "AUTH" -runClientTransport :: Transport c => THandleSMP c -> M () -runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = do +runClientTransport :: Transport c => THandleSMP c 'TServer -> M () +runClientTransport h@THandle {params = thParams@THandleParams {thVersion, sessionId}} = do q <- asks $ tbqSize . config ts <- liftIO getSystemTime active <- asks clients @@ -421,11 +477,12 @@ runClientTransport th@THandle {params = THandleParams {thVersion, sessionId}} = pure new s <- asks server expCfg <- asks $ inactiveClientExpiration . config + th <- newMVar h -- put TH under a fair lock to interleave messages and command responses labelMyThread . B.unpack $ "client $" <> encode sessionId - raceAny_ ([liftIO $ send th c, client c s, receive th c] <> disconnectThread_ c expCfg) + raceAny_ ([liftIO $ send th c, liftIO $ sendMsg th c, client thParams c s, receive h c] <> disconnectThread_ c expCfg) `finally` clientDisconnected c where - disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport th (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)] + disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport h (rcvActiveAt c) (sndActiveAt c) expCfg (noSubscriptions c)] disconnectThread_ _ _ = [] noSubscriptions c = atomically $ (&&) <$> TM.null (subscriptions c) <*> TM.null (ntfSubscriptions c) @@ -457,43 +514,70 @@ cancelSub sub = Sub {subThread = SubThread t} -> liftIO $ deRefWeak t >>= mapM_ killThread _ -> return () -receive :: Transport c => THandleSMP c -> Client -> M () -receive th@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do +receive :: Transport c => THandleSMP c 'TServer -> Client -> M () +receive h@THandle {params = THandleParams {thAuth}} Client {rcvQ, sndQ, rcvActiveAt, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " receive" forever $ do - ts <- L.toList <$> liftIO (tGet th) + ts <- L.toList <$> liftIO (tGet h) atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime - as <- partitionEithers <$> mapM cmdAction ts - write sndQ $ fst as - write rcvQ $ snd as + stats <- asks serverStats + (errs, cmds) <- partitionEithers <$> mapM (cmdAction stats) ts + write sndQ errs + write rcvQ cmds where - cmdAction :: SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) - cmdAction (tAuth, authorized, (corrId, queueId, cmdOrError)) = + cmdAction :: ServerStats -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) + cmdAction stats (tAuth, authorized, (corrId, entId, cmdOrError)) = case cmdOrError of - Left e -> pure $ Left (corrId, queueId, ERR e) - Right cmd -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized queueId cmd + Left e -> pure $ Left (corrId, entId, ERR e) + Right cmd -> verified =<< verifyTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) tAuth authorized entId cmd where verified = \case - VRVerified qr -> Right (qr, (corrId, queueId, cmd)) - VRFailed -> Left (corrId, queueId, ERR AUTH) + VRVerified qr -> pure $ Right (qr, (corrId, entId, cmd)) + VRFailed -> do + case cmd of + Cmd _ SEND {} -> atomically $ modifyTVar' (msgSentAuth stats) (+ 1) + Cmd _ SUB -> atomically $ modifyTVar' (qSubAuth stats) (+ 1) + _ -> pure () + pure $ Left (corrId, entId, ERR AUTH) write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty -send :: Transport c => THandleSMP c -> Client -> IO () -send h@THandle {params} Client {sndQ, sessionId, sndActiveAt} = do +send :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO () +send th c@Client {sndQ, msgQ, sessionId} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " send" - forever $ do - ts <- atomically $ L.sortWith tOrder <$> readTBQueue sndQ - -- TODO we can authorize responses as well - void . liftIO . tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts - atomically . writeTVar sndActiveAt =<< liftIO getSystemTime + forever $ atomically (readTBQueue sndQ) >>= sendTransmissions where - tOrder :: Transmission BrokerMsg -> Int - tOrder (_, _, cmd) = case cmd of - MSG {} -> 0 - NMSG {} -> 0 - _ -> 1 + sendTransmissions :: NonEmpty (Transmission BrokerMsg) -> IO () + sendTransmissions ts + | L.length ts <= 2 = tSend th c ts + | otherwise = do + let (msgs_, ts') = mapAccumR splitMessages [] ts + -- If the request had batched subscriptions (L.length ts > 2) + -- this will reply OK to all SUBs in the first batched transmission, + -- to reduce client timeouts. + tSend th c ts' + -- After that all messages will be sent in separate transmissions, + -- without any client response timeouts, and allowing them to interleave + -- with other requests responses. + mapM_ (atomically . writeTBQueue msgQ) $ L.nonEmpty msgs_ + where + splitMessages :: [Transmission BrokerMsg] -> Transmission BrokerMsg -> ([Transmission BrokerMsg], Transmission BrokerMsg) + splitMessages msgs t@(corrId, entId, cmd) = case cmd of + -- replace MSG response with OK, accumulating MSG in a separate list. + MSG {} -> ((CorrId "", entId, cmd) : msgs, (corrId, entId, OK)) + _ -> (msgs, t) -disconnectTransport :: Transport c => THandle v c -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () +sendMsg :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> IO () +sendMsg th c@Client {msgQ, sessionId} = do + labelMyThread . B.unpack $ "client $" <> encode sessionId <> " sendMsg" + forever $ atomically (readTBQueue msgQ) >>= mapM_ (\t -> tSend th c [t]) + +tSend :: Transport c => MVar (THandleSMP c 'TServer) -> Client -> NonEmpty (Transmission BrokerMsg) -> IO () +tSend th Client {sndActiveAt} ts = do + withMVar th $ \h@THandle {params} -> + void . tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts + atomically . writeTVar sndActiveAt =<< liftIO getSystemTime + +disconnectTransport :: Transport c => THandle v c 'TServer -> TVar SystemTime -> TVar SystemTime -> ExpirationConfig -> IO Bool -> IO () disconnectTransport THandle {connection, params = THandleParams {sessionId}} rcvActiveAt sndActiveAt expCfg noSubscriptions = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " disconnectTransport" loop @@ -514,7 +598,7 @@ data VerificationResult = VRVerified (Maybe QueueRec) | VRFailed -- - the queue or party key do not exist. -- In all cases, the time of the verification should depend only on the provided authorization type, -- a dummy key is used to run verification in the last two cases, and failure is returned irrespective of the result. -verifyTransmission :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult +verifyTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> QueueId -> Cmd -> M VerificationResult verifyTransmission auth_ tAuth authorized queueId cmd = case cmd of Cmd SRecipient (NEW k _ _ _) -> pure $ Nothing `verifiedWith` k @@ -522,21 +606,23 @@ verifyTransmission auth_ tAuth authorized queueId cmd = -- SEND will be accepted without authorization before the queue is secured with KEY command Cmd SSender SEND {} -> verifyQueue (\q -> Just q `verified` maybe (isNothing tAuth) verify (senderKey q)) <$> get SSender Cmd SSender PING -> pure $ VRVerified Nothing + Cmd SSender RFWD {} -> pure $ VRVerified Nothing -- NSUB will not be accepted without authorization - Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (Just q `verifiedWith`) (notifierKey <$> notifier q)) <$> get SNotifier + Cmd SNotifier NSUB -> verifyQueue (\q -> maybe dummyVerify (\n -> Just q `verifiedWith` notifierKey n) (notifier q)) <$> get SNotifier + Cmd SProxiedClient _ -> pure $ VRVerified Nothing where verify = verifyCmdAuthorization auth_ tAuth authorized dummyVerify = verify (dummyAuthKey tAuth) `seq` VRFailed verifyQueue :: (QueueRec -> VerificationResult) -> Either ErrorType QueueRec -> VerificationResult - verifyQueue = either (\_ -> dummyVerify) + verifyQueue = either (const dummyVerify) verified q cond = if cond then VRVerified q else VRFailed verifiedWith q k = q `verified` verify k - get :: SParty p -> M (Either ErrorType QueueRec) + get :: DirectParty p => SParty p -> M (Either ErrorType QueueRec) get party = do st <- asks queueStore atomically $ getQueue st party queueId -verifyCmdAuthorization :: Maybe (THandleAuth, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool +verifyCmdAuthorization :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> C.APublicAuthKey -> Bool verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAuth where verify :: C.APublicAuthKey -> TransmissionAuth -> Bool @@ -548,12 +634,12 @@ verifyCmdAuthorization auth_ tAuth authorized key = maybe False (verify key) tAu C.SX25519 -> verifyCmdAuth auth_ k s authorized _ -> verifyCmdAuth auth_ dummyKeyX25519 s authorized `seq` False -verifyCmdAuth :: Maybe (THandleAuth, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool +verifyCmdAuth :: Maybe (THandleAuth 'TServer, C.CbNonce) -> C.PublicKeyX25519 -> C.CbAuthenticator -> ByteString -> Bool verifyCmdAuth auth_ k authenticator authorized = case auth_ of - Just (THandleAuth {privKey}, nonce) -> C.cbVerify k privKey nonce authenticator authorized + Just (THAuthServer {serverPrivKey = pk}, nonce) -> C.cbVerify k pk nonce authenticator authorized Nothing -> False -dummyVerifyCmd :: Maybe (THandleAuth, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool +dummyVerifyCmd :: Maybe (THandleAuth 'TServer, C.CbNonce) -> ByteString -> TransmissionAuth -> Bool dummyVerifyCmd auth_ authorized = \case TASignature (C.ASignature a s) -> C.verify' (dummySignKey a) s authorized TAAuthenticator s -> verifyCmdAuth auth_ dummyKeyX25519 s authorized @@ -581,42 +667,133 @@ dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/Xo dummyKeyX25519 :: C.PublicKey 'C.X25519 dummyKeyX25519 = "MCowBQYDK2VuAyEA4JGSMYht18H4mas/jHeBwfcM7jLwNYJNOAhi2/g4RXg=" -client :: Client -> Server -> M () -client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do +forkClient :: Client -> String -> M () -> M () +forkClient Client {endThreads, endThreadSeq} label action = do + tId <- atomically $ stateTVar endThreadSeq $ \next -> (next, next + 1) + t <- forkIO $ do + labelMyThread label + action `finally` atomically (modifyTVar' endThreads $ IM.delete tId) + mkWeakThreadId t >>= atomically . modifyTVar' endThreads . IM.insert tId + +client :: THandleParams SMPVersion 'TServer -> Client -> Server -> M () +client thParams' clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId, procThreads} Server {subscribedQ, ntfSubscribedQ, notifiers} = do labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands" forever $ atomically (readTBQueue rcvQ) >>= mapM processCommand - >>= atomically . writeTBQueue sndQ + >>= mapM_ reply . L.nonEmpty . catMaybes . L.toList where - processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Transmission BrokerMsg) - processCommand (qr_, (corrId, queueId, cmd)) = do - st <- asks queueStore - case cmd of - Cmd SSender command -> - case command of - SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody - PING -> pure (corrId, "", PONG) - Cmd SNotifier NSUB -> subscribeNotifications - Cmd SRecipient command -> - case command of - NEW rKey dhKey auth subMode -> - ifM - allowNew - (createQueue st rKey dhKey subMode) - (pure (corrId, queueId, ERR AUTH)) - where - allowNew = do - ServerConfig {allowNewQueues, newQueueBasicAuth} <- asks config - pure $ allowNewQueues && maybe True ((== auth) . Just) newQueueBasicAuth - SUB -> withQueue (`subscribeQueue` queueId) - GET -> withQueue getMessage - ACK msgId -> withQueue (`acknowledgeMsg` msgId) - KEY sKey -> secureQueue_ st sKey - NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey - NDEL -> deleteQueueNotifier_ st - OFF -> suspendQueue_ st - DEL -> delQueueAndMsgs st + reply :: MonadIO m => NonEmpty (Transmission BrokerMsg) -> m () + reply = atomically . writeTBQueue sndQ + processProxiedCmd :: Transmission (Command 'ProxiedClient) -> M (Maybe (Transmission BrokerMsg)) + processProxiedCmd (corrId, sessId, command) = (corrId,sessId,) <$$> case command of + PRXY srv auth -> ifM allowProxy getRelay (pure $ Just $ ERR $ PROXY BASIC_AUTH) + where + allowProxy = do + ServerConfig {allowSMPProxy, newQueueBasicAuth} <- asks config + pure $ allowSMPProxy && maybe True ((== auth) . Just) newQueueBasicAuth + getRelay = do + ProxyAgent {smpAgent = a} <- asks proxyAgent + liftIO (getConnectedSMPServerClient a srv) >>= \case + Just r -> Just <$> proxyServerResponse a r + Nothing -> + forkProxiedCmd $ + liftIO (runExceptT (getSMPServerClient'' a srv) `catch` (pure . Left . PCEIOError)) + >>= proxyServerResponse a + proxyServerResponse :: SMPClientAgent -> Either SMPClientError (OwnServer, SMPClient) -> M BrokerMsg + proxyServerResponse a smp_ = do + ServerStats {pRelays, pRelaysOwn} <- asks serverStats + let inc = mkIncProxyStats pRelays pRelaysOwn + case smp_ of + Right (own, smp) -> do + inc own pRequests + case proxyResp smp of + r@PKEY {} -> r <$ inc own pSuccesses + r -> r <$ inc own pErrorsCompat + Left e -> do + let own = isOwnServer a srv + inc own pRequests + inc own $ if temporaryClientError e then pErrorsConnect else pErrorsOther + logError $ "Error connecting: " <> decodeLatin1 (strEncode $ host srv) <> " " <> tshow e + pure . ERR $ smpProxyError e + where + proxyResp smp = + let THandleParams {sessionId = srvSessId, thVersion, thServerVRange, thAuth} = thParams smp + in case compatibleVRange thServerVRange proxiedSMPRelayVRange of + -- Cap the destination relay version range to prevent client version fingerprinting. + -- See comment for proxiedSMPRelayVersion. + Just (Compatible vr) | thVersion >= sendingProxySMPVersion -> case thAuth of + Just THAuthClient {serverCertKey} -> PKEY srvSessId vr serverCertKey + Nothing -> ERR $ transportErr TENoServerAuth + _ -> ERR $ transportErr TEVersion + PFWD fwdV pubKey encBlock -> do + ProxyAgent {smpAgent = a} <- asks proxyAgent + ServerStats {pMsgFwds, pMsgFwdsOwn} <- asks serverStats + let inc = mkIncProxyStats pMsgFwds pMsgFwdsOwn + atomically (lookupSMPServerClient a sessId) >>= \case + Just (own, smp) -> do + inc own pRequests + if v >= sendingProxySMPVersion + then forkProxiedCmd $ do + liftIO (runExceptT (forwardSMPMessage smp corrId fwdV pubKey encBlock) `catch` (pure . Left . PCEIOError)) >>= \case + Right r -> PRES r <$ inc own pSuccesses + Left e -> ERR (smpProxyError e) <$ case e of + PCEProtocolError {} -> inc own pSuccesses + _ -> inc own pErrorsOther + else Just (ERR $ transportErr TEVersion) <$ inc own pErrorsCompat + where + THandleParams {thVersion = v} = thParams smp + Nothing -> inc False pRequests >> inc False pErrorsConnect $> Just (ERR $ PROXY NO_SESSION) + where + forkProxiedCmd :: M BrokerMsg -> M (Maybe BrokerMsg) + forkProxiedCmd cmdAction = do + bracket_ wait signal . forkClient clnt (B.unpack $ "client $" <> encode sessionId <> " proxy") $ do + -- commands MUST be processed under a reasonable timeout or the client would halt + cmdAction >>= \t -> reply [(corrId, sessId, t)] + pure Nothing + where + wait = do + ServerConfig {serverClientConcurrency} <- asks config + atomically $ do + used <- readTVar procThreads + when (used >= serverClientConcurrency) retry + writeTVar procThreads $! used + 1 + signal = atomically $ modifyTVar' procThreads (\t -> t - 1) + transportErr :: TransportError -> ErrorType + transportErr = PROXY . BROKER . TRANSPORT + mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> TVar Int) -> m () + mkIncProxyStats ps psOwn = \own sel -> do + atomically $ modifyTVar' (sel ps) (+ 1) + when own $ atomically $ modifyTVar' (sel psOwn) (+ 1) + processCommand :: (Maybe QueueRec, Transmission Cmd) -> M (Maybe (Transmission BrokerMsg)) + processCommand (qr_, (corrId, queueId, cmd)) = case cmd of + Cmd SProxiedClient command -> processProxiedCmd (corrId, queueId, command) + Cmd SSender command -> Just <$> case command of + SEND flags msgBody -> withQueue $ \qr -> sendMessage qr flags msgBody + PING -> pure (corrId, "", PONG) + RFWD encBlock -> (corrId, "",) <$> processForwardedCommand encBlock + Cmd SNotifier NSUB -> Just <$> subscribeNotifications + Cmd SRecipient command -> do + st <- asks queueStore + Just <$> case command of + NEW rKey dhKey auth subMode -> + ifM + allowNew + (createQueue st rKey dhKey subMode) + (pure (corrId, queueId, ERR AUTH)) + where + allowNew = do + ServerConfig {allowNewQueues, newQueueBasicAuth} <- asks config + pure $ allowNewQueues && maybe True ((== auth) . Just) newQueueBasicAuth + SUB -> withQueue (`subscribeQueue` queueId) + GET -> withQueue getMessage + ACK msgId -> withQueue (`acknowledgeMsg` msgId) + KEY sKey -> secureQueue_ st sKey + NKEY nKey dhKey -> addQueueNotifier_ st nKey dhKey + NDEL -> deleteQueueNotifier_ st + OFF -> suspendQueue_ st + DEL -> delQueueAndMsgs st + QUE -> withQueue getQueueInfo where createQueue :: QueueStore -> RcvPublicAuthKey -> RcvPublicDhKey -> SubscriptionMode -> M (Transmission BrokerMsg) createQueue st recipientKey dhKey subMode = time "NEW" $ do @@ -703,15 +880,19 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv subscribeQueue :: QueueRec -> RecipientId -> M (Transmission BrokerMsg) subscribeQueue qr rId = do + stats <- asks serverStats atomically (TM.lookup rId subscriptions) >>= \case - Nothing -> + Nothing -> do + atomically $ modifyTVar' (qSub stats) (+ 1) newSub >>= deliver Just sub -> readTVarIO sub >>= \case - Sub {subThread = ProhibitSub} -> + Sub {subThread = ProhibitSub} -> do -- cannot use SUB in the same connection where GET was used + atomically $ modifyTVar' (qSubProhibited stats) (+ 1) pure (corrId, rId, ERR $ CMD PROHIBITED) - s -> + s -> do + atomically $ modifyTVar' (qSubDuplicate stats) (+ 1) atomically (tryTakeTMVar $ delivered s) >> deliver sub where newSub :: M (TVar Sub) @@ -805,30 +986,39 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv sendMessage :: QueueRec -> MsgFlags -> MsgBody -> M (Transmission BrokerMsg) sendMessage qr msgFlags msgBody - | B.length msgBody > maxMessageLength = pure $ err LARGE_MSG - | otherwise = case status qr of - QueueOff -> return $ err AUTH - QueueActive -> - case C.maxLenBS msgBody of - Left _ -> pure $ err LARGE_MSG - Right body -> do - msg_ <- time "SEND" $ do - q <- getStoreMsgQueue "SEND" $ recipientId qr - expireMessages q - atomically . writeMsg q =<< mkMessage body - case msg_ of - Nothing -> pure $ err QUOTA - Just msg -> time "SEND ok" $ do - stats <- asks serverStats - when (notification msgFlags) $ do - atomically . trySendNotification msg =<< asks random - atomically $ modifyTVar' (msgSentNtf stats) (+ 1) - atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr) - atomically $ modifyTVar' (msgSent stats) (+ 1) - atomically $ modifyTVar' (msgCount stats) (+ 1) - atomically $ updatePeriodStats (activeQueues stats) (recipientId qr) - pure ok + | B.length msgBody > maxMessageLength thVersion = do + stats <- asks serverStats + atomically $ modifyTVar' (msgSentLarge stats) (+ 1) + pure $ err LARGE_MSG + | otherwise = do + stats <- asks serverStats + case status qr of + QueueOff -> do + atomically $ modifyTVar' (msgSentAuth stats) (+ 1) + pure $ err AUTH + QueueActive -> + case C.maxLenBS msgBody of + Left _ -> pure $ err LARGE_MSG + Right body -> do + msg_ <- time "SEND" $ do + q <- getStoreMsgQueue "SEND" $ recipientId qr + expireMessages q + atomically . writeMsg q =<< mkMessage body + case msg_ of + Nothing -> do + atomically $ modifyTVar' (msgSentQuota stats) (+ 1) + pure $ err QUOTA + Just msg -> time "SEND ok" $ do + when (notification msgFlags) $ do + atomically . trySendNotification msg =<< asks random + atomically $ modifyTVar' (msgSentNtf stats) (+ 1) + atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr) + atomically $ modifyTVar' (msgSent stats) (+ 1) + atomically $ modifyTVar' (msgCount stats) (+ 1) + atomically $ updatePeriodStats (activeQueues stats) (recipientId qr) + pure ok where + THandleParams {thVersion} = thParams' mkMessage :: C.MaxLenBS MaxMessageLen -> M Message mkMessage body = do msgId <- randomId =<< asks (msgIdBytes . config) @@ -863,6 +1053,58 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv encNMsgMeta = C.cbEncrypt rcvNtfDhSecret cbNonce (smpEncode msgMeta) 128 pure . (cbNonce,) $ fromRight "" encNMsgMeta + processForwardedCommand :: EncFwdTransmission -> M BrokerMsg + processForwardedCommand (EncFwdTransmission s) = fmap (either ERR id) . runExceptT $ do + THAuthServer {serverPrivKey, sessSecret'} <- maybe (throwE $ transportErr TENoServerAuth) pure (thAuth thParams') + sessSecret <- maybe (throwE $ transportErr TENoServerAuth) pure sessSecret' + let proxyNonce = C.cbNonce $ bs corrId + s' <- liftEitherWith (const CRYPTO) $ C.cbDecryptNoPad sessSecret proxyNonce s + FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission = EncTransmission et} <- liftEitherWith (const $ CMD SYNTAX) $ smpDecode s' + let clientSecret = C.dh' fwdKey serverPrivKey + clientNonce = C.cbNonce $ bs fwdCorrId + b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et + let clntTHParams = smpTHParamsSetVersion fwdVersion thParams' + -- only allowing single forwarded transactions + t' <- case tParse clntTHParams b of + t :| [] -> pure $ tDecodeParseValidate clntTHParams t + _ -> throwE BLOCK + let clntThAuth = Just $ THAuthServer {serverPrivKey, sessSecret' = Just clientSecret} + -- process forwarded SEND + r <- + lift (rejectOrVerify clntThAuth t') >>= \case + Left r -> pure r + Right t''@(_, (corrId', entId', cmd')) -> case cmd' of + Cmd SSender SEND {} -> + -- Left will not be returned by processCommand, as only SEND command is allowed + fromMaybe (corrId', entId', ERR INTERNAL) <$> lift (processCommand t'') + _ -> + pure (corrId', entId', ERR $ CMD PROHIBITED) + -- encode response + r' <- case batchTransmissions (batch clntTHParams) (blockSize clntTHParams) [Right (Nothing, encodeTransmission clntTHParams r)] of + [] -> throwE INTERNAL -- at least 1 item is guaranteed from NonEmpty/Right + TBError _ _ : _ -> throwE BLOCK + TBTransmission b' _ : _ -> pure b' + TBTransmissions b' _ _ : _ -> pure b' + -- encrypt to client + r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedMsgLength + -- encrypt to proxy + let fr = FwdResponse {fwdCorrId, fwdResponse = r2} + r3 = EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) + stats <- asks serverStats + atomically $ modifyTVar' (pMsgFwdsRecv stats) (+ 1) + pure $ RRES r3 + where + rejectOrVerify :: Maybe (THandleAuth 'TServer) -> SignedTransmission ErrorType Cmd -> M (Either (Transmission BrokerMsg) (Maybe QueueRec, Transmission Cmd)) + rejectOrVerify clntThAuth (tAuth, authorized, (corrId', entId', cmdOrError)) = + case cmdOrError of + Left e -> pure $ Left (corrId', entId', ERR e) + Right cmd'@(Cmd SSender SEND {}) -> verified <$> verifyTransmission ((,C.cbNonce (bs corrId')) <$> clntThAuth) tAuth authorized entId' cmd' + where + verified = \case + VRVerified qr -> Right (qr, (corrId', entId', cmd')) + VRFailed -> Left (corrId', entId', ERR AUTH) + Right _ -> pure $ Left (corrId', entId', ERR $ CMD PROHIBITED) + deliverMessage :: T.Text -> QueueRec -> RecipientId -> TVar Sub -> MsgQueue -> Maybe Message -> M (Transmission BrokerMsg) deliverMessage name qr rId sub q msg_ = time (name <> " deliver") $ do readTVarIO sub >>= \case @@ -871,9 +1113,10 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Just msg -> let encMsg = encryptMsg qr msg in atomically (setDelivered s msg) $> (corrId, rId, MSG encMsg) - _ -> forkSub $> ok - _ -> pure ok + _ -> forkSub $> resp + _ -> pure resp where + resp = (corrId, rId, OK) forkSub :: M () forkSub = do atomically . modifyTVar' sub $ \s -> s {subThread = SubPending} @@ -922,6 +1165,26 @@ client clnt@Client {subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Serv Right q -> updateDeletedStats q $> ok Left e -> pure $ err e + getQueueInfo :: QueueRec -> M (Transmission BrokerMsg) + getQueueInfo QueueRec {senderKey, notifier} = do + q@MsgQueue {size} <- getStoreMsgQueue "getQueueInfo" queueId + info <- atomically $ do + qiSub <- TM.lookup queueId subscriptions >>= mapM mkQSub + qiSize <- readTVar size + qiMsg <- toMsgInfo <$$> tryPeekMsg q + pure QueueInfo {qiSnd = isJust senderKey, qiNtf = isJust notifier, qiSub, qiSize, qiMsg} + pure (corrId, queueId, INFO info) + where + mkQSub sub = do + Sub {subThread, delivered} <- readTVar sub + let qSubThread = case subThread of + NoSub -> QNoSub + SubPending -> QSubPending + SubThread _ -> QSubThread + ProhibitSub -> QProhibitSub + qDelivered <- decodeLatin1 . encode <$$> tryReadTMVar delivered + pure QSub {qSubThread, qDelivered} + ok :: Transmission BrokerMsg ok = (corrId, queueId, OK) @@ -975,9 +1238,10 @@ saveServerMessages keepMsgs = asks (storeMsgsFile . config) >>= mapM_ saveMessag >>= mapM_ (B.hPutStrLn h . strEncode . MLRv3 rId) restoreServerMessages :: M Int -restoreServerMessages = asks (storeMsgsFile . config) >>= \case - Just f -> ifM (doesFileExist f) (restoreMessages f) (pure 0) - Nothing -> pure 0 +restoreServerMessages = + asks (storeMsgsFile . config) >>= \case + Just f -> ifM (doesFileExist f) (restoreMessages f) (pure 0) + Nothing -> pure 0 where restoreMessages f = do logInfo $ "restoring messages from file " <> T.pack f diff --git a/src/Simplex/Messaging/Server/CLI.hs b/src/Simplex/Messaging/Server/CLI.hs index 9531a2ca5..956b30816 100644 --- a/src/Simplex/Messaging/Server/CLI.hs +++ b/src/Simplex/Messaging/Server/CLI.hs @@ -254,7 +254,7 @@ onOffPrompt prompt def = "N" -> pure False _ -> putStrLn "Invalid input, please enter 'y' or 'n'" >> onOffPrompt prompt def -onOff :: Bool -> String +onOff :: Bool -> Text onOff True = "on" onOff _ = "off" diff --git a/src/Simplex/Messaging/Server/Env/STM.hs b/src/Simplex/Messaging/Server/Env/STM.hs index baadfc79b..4217ea9b9 100644 --- a/src/Simplex/Messaging/Server/Env/STM.hs +++ b/src/Simplex/Messaging/Server/Env/STM.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StrictData #-} module Simplex.Messaging.Server.Env.STM where @@ -15,6 +16,7 @@ import qualified Data.IntMap.Strict as IM import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import Data.Maybe (isJust, isNothing) import Data.Time.Clock (getCurrentTime) import Data.Time.Clock.System (SystemTime) import Data.X509.Validation (Fingerprint (..)) @@ -22,10 +24,12 @@ import Network.Socket (ServiceName) import qualified Network.TLS as T import Numeric.Natural import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Client.Agent (SMPClientAgent, SMPClientAgentConfig, newSMPClientAgent) import Simplex.Messaging.Crypto (KeyHash (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol import Simplex.Messaging.Server.Expiration +import Simplex.Messaging.Server.Information import Simplex.Messaging.Server.MsgStore.STM import Simplex.Messaging.Server.QueueStore (NtfCreds (..), QueueRec (..)) import Simplex.Messaging.Server.QueueStore.STM @@ -33,8 +37,8 @@ import Simplex.Messaging.Server.Stats import Simplex.Messaging.Server.StoreLog import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (ATransport, VersionSMP, VersionRangeSMP) -import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState) +import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP) +import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams, newSocketState) import System.IO (IOMode (..)) import System.Mem.Weak (Weak) import UnliftIO.STM @@ -79,7 +83,13 @@ data ServerConfig = ServerConfig -- | TCP transport config transportConfig :: TransportServerConfig, -- | run listener on control port - controlPort :: Maybe ServiceName + controlPort :: Maybe ServiceName, + -- | SMP proxy config + smpAgentCfg :: SMPClientAgentConfig, + allowSMPProxy :: Bool, -- auth is the same with `newQueueBasicAuth` + serverClientConcurrency :: Int, + -- | server public information + information :: Maybe ServerPublicInfo } defMsgExpirationDays :: Int64 @@ -99,8 +109,12 @@ defaultInactiveClientExpiration = checkInterval = 3600 -- seconds, 1 hours } +defaultProxyClientConcurrency :: Int +defaultProxyClientConcurrency = 32 + data Env = Env { config :: ServerConfig, + serverInfo :: ServerInformation, server :: Server, serverIdentity :: KeyHash, queueStore :: QueueStore, @@ -110,8 +124,9 @@ data Env = Env tlsServerParams :: T.ServerParams, serverStats :: ServerStats, sockets :: SocketState, - clientSeq :: TVar Int, - clients :: TVar (IntMap Client) + clientSeq :: TVar ClientId, + clients :: TVar (IntMap Client), + proxyAgent :: ProxyAgent -- senders served on this proxy } data Server = Server @@ -122,12 +137,20 @@ data Server = Server savingLock :: Lock } +newtype ProxyAgent = ProxyAgent + { smpAgent :: SMPClientAgent + } + +type ClientId = Int + data Client = Client - { clientId :: Int, + { clientId :: ClientId, subscriptions :: TMap RecipientId (TVar Sub), ntfSubscriptions :: TMap NotifierId (), rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)), sndQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), + msgQ :: TBQueue (NonEmpty (Transmission BrokerMsg)), + procThreads :: TVar Int, endThreads :: TVar (IntMap (Weak ThreadId)), endThreadSeq :: TVar Int, thVersion :: VersionSMP, @@ -154,19 +177,21 @@ newServer = do savingLock <- createLock return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock} -newClient :: TVar Int -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client +newClient :: TVar ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client newClient nextClientId qSize thVersion sessionId createdAt = do clientId <- stateTVar nextClientId $ \next -> (next, next + 1) subscriptions <- TM.empty ntfSubscriptions <- TM.empty rcvQ <- newTBQueue qSize sndQ <- newTBQueue qSize + msgQ <- newTBQueue qSize + procThreads <- newTVar 0 endThreads <- newTVar IM.empty endThreadSeq <- newTVar 0 connected <- newTVar True rcvActiveAt <- newTVar createdAt sndActiveAt <- newTVar createdAt - return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} + return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, procThreads, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt} newSubscription :: SubscriptionThread -> STM Sub newSubscription subThread = do @@ -174,20 +199,21 @@ newSubscription subThread = do return Sub {subThread, delivered} newEnv :: ServerConfig -> IO Env -newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do +newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg, transportConfig, information, messageExpiration} = do server <- atomically newServer queueStore <- atomically newQueueStore msgStore <- atomically newMsgStore random <- liftIO C.newRandom storeLog <- restoreQueues queueStore `mapM` storeLogFile - tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile + tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig) Fingerprint fp <- loadFingerprint caCertificateFile let serverIdentity = KeyHash fp serverStats <- atomically . newServerStats =<< getCurrentTime sockets <- atomically newSocketState clientSeq <- newTVarIO 0 clients <- newTVarIO mempty - return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients} + proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random + pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent} where restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode) restoreQueues QueueStore {queues, senders, notifiers} f = do @@ -203,3 +229,25 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, addNotifier q = case notifier q of Nothing -> id Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q) + serverInfo = + ServerInformation + { information, + config = + ServerPublicConfig + { persistence, + messageExpiration = ttl <$> messageExpiration, + statsEnabled = isJust $ logStatsInterval config, + newQueuesAllowed = allowNewQueues config, + basicAuthEnabled = isJust $ newQueueBasicAuth config + } + } + where + persistence + | isNothing storeLogFile = SPMMemoryOnly + | isJust (storeMsgsFile config) = SPMMessages + | otherwise = SPMQueues + +newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent +newSMPProxyAgent smpAgentCfg random = do + smpAgent <- newSMPClientAgent smpAgentCfg random + pure ProxyAgent {smpAgent} diff --git a/src/Simplex/Messaging/Server/Information.hs b/src/Simplex/Messaging/Server/Information.hs new file mode 100644 index 000000000..01052541d --- /dev/null +++ b/src/Simplex/Messaging/Server/Information.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Messaging.Server.Information where + +import qualified Data.Aeson.TH as J +import Data.Int (Int64) +import Data.Text (Text) +import Simplex.Messaging.Agent.Protocol (ConnectionMode (..), ConnectionRequestUri) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) + +data ServerInformation = ServerInformation + { config :: ServerPublicConfig, + information :: Maybe ServerPublicInfo + } + deriving (Show) + +-- based on server configuration +data ServerPublicConfig = ServerPublicConfig + { persistence :: ServerPersistenceMode, + messageExpiration :: Maybe Int64, + statsEnabled :: Bool, + newQueuesAllowed :: Bool, + basicAuthEnabled :: Bool -- server is private if enabled + } + deriving (Show) + +-- based on INFORMATION section of INI file +data ServerPublicInfo = ServerPublicInfo + { sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license + usageConditions :: Maybe ServerConditions, + operator :: Maybe Entity, + website :: Maybe Text, + adminContacts :: Maybe ServerContactAddress, + complaintsContacts :: Maybe ServerContactAddress, + hosting :: Maybe Entity, + serverCountry :: Maybe Text + } + deriving (Show) + +data ServerPersistenceMode = SPMMemoryOnly | SPMQueues | SPMMessages + deriving (Show) + +data ServerConditions = ServerConditions {conditions :: Text, amendments :: Maybe Text} + deriving (Show) + +data Entity = Entity {name :: Text, country :: Maybe Text} + deriving (Show) + +data ServerContactAddress = ServerContactAddress + { simplex :: Maybe (ConnectionRequestUri 'CMContact), + email :: Maybe Text, -- it is recommended that it matches DNS email address, if either is present + pgp :: Maybe PGPKey + } + deriving (Show) + +data PGPKey = PGPKey {pkURI :: Text, pkFingerprint :: Text} + deriving (Show) + +$(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''ServerPersistenceMode) + +$(J.deriveJSON defaultJSON ''ServerConditions) + +$(J.deriveJSON defaultJSON ''Entity) + +$(J.deriveJSON defaultJSON ''PGPKey) + +$(J.deriveJSON defaultJSON ''ServerContactAddress) + +$(J.deriveJSON defaultJSON ''ServerPublicConfig) + +$(J.deriveJSON defaultJSON ''ServerPublicInfo) + +$(J.deriveJSON defaultJSON ''ServerInformation) diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index a7844cc95..7af57ba25 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -5,37 +5,56 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Messaging.Server.Main where import Control.Concurrent.STM -import Control.Monad (void) +import Control.Logger.Simple +import Control.Monad (void, when, (<$!>)) +import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import Data.Char (isAlpha, isAscii, toUpper) import Data.Functor (($>)) -import Data.Ini (lookupValue, readIniFile) -import Data.Maybe (fromMaybe) +import Data.Ini (Ini, lookupValue, readIniFile) +import Data.List (find, isPrefixOf) +import qualified Data.List.NonEmpty as L +import Data.Maybe (fromMaybe, isJust, isNothing) +import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.IO as T import Network.Socket (HostName) import Options.Applicative +import Simplex.Messaging.Agent.Protocol (connReqUriP') +import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) 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.CLI -import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration) +import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration, defaultProxyClientConcurrency) import Simplex.Messaging.Server.Expiration -import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange) +import Simplex.Messaging.Server.Information +import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow) +import Simplex.Messaging.Version (mkVersionRange) import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (combine) import System.IO (BufferMode (..), hSetBuffering, stderr, stdout) import Text.Read (readMaybe) smpServerCLI :: FilePath -> FilePath -> IO () -smpServerCLI cfgPath logPath = +smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ()) + +smpServerCLI_ :: (ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) -> (EmbeddedWebParams -> IO ()) -> FilePath -> FilePath -> IO () +smpServerCLI_ generateSite serveStaticFiles cfgPath logPath = getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case Init opts -> doesFileExist iniFile >>= \case @@ -60,7 +79,10 @@ smpServerCLI cfgPath logPath = defaultServerPort = "5223" executableName = "smp-server" storeLogFilePath = combine logPath "smp-server-store.log" - initializeServer opts@InitOptions {ip, fqdn, scripted} + httpsCertFile = combine cfgPath "web.cert" + httpsKeyFile = combine cfgPath "web.key" + defaultStaticPath = combine logPath "www" + initializeServer opts@InitOptions {ip, fqdn, sourceCode = src', webStaticPath = sp', disableWeb = noWeb', scripted} | scripted = initialize opts | otherwise = do putStrLn "Use `smp-server init -h` for available options." @@ -71,7 +93,19 @@ smpServerCLI cfgPath logPath = password <- withPrompt "'r' for random (default), 'n' - no password, or enter password: " serverPassword let host = fromMaybe ip fqdn host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine - initialize opts {enableStoreLog, logStats, fqdn = if null host' then fqdn else Just host', password} + 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, + logStats, + fqdn = if null host' then fqdn else Just host', + password, + sourceCode = (T.pack <$> sourceCode') <|> src', + webStaticPath = if null staticPath' then sp' else Just staticPath', + disableWeb = not enableWeb + } where serverPassword = getLine >>= \case @@ -82,7 +116,7 @@ smpServerCLI cfgPath logPath = case strDecode $ encodeUtf8 $ T.pack s of Right auth -> pure . Just $ ServerPassword auth _ -> putStrLn "Invalid password. Only latin letters, digits and symbols other than '@' and ':' are allowed" >> serverPassword - initialize InitOptions {enableStoreLog, logStats, signAlgorithm, password} = do + initialize InitOptions {enableStoreLog, logStats, signAlgorithm, password, sourceCode, webStaticPath, disableWeb} = do clearDirIfExists cfgPath clearDirIfExists logPath createDirectoryIfMissing True cfgPath @@ -92,26 +126,29 @@ smpServerCLI cfgPath logPath = basicAuth <- mapM createServerPassword password let host = fromMaybe (if ip == "127.0.0.1" then "" else ip) fqdn srv = ProtoServerWithAuth (SMPServer [THDomainName host] "" (C.KeyHash fp)) basicAuth - writeFile iniFile $ iniFileContent host basicAuth - putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server." + T.writeFile iniFile $ iniFileContent host basicAuth $ Just "https://github.com/simplex-chat/simplexmq" + putStrLn $ "Server initialized, please provide additional server information in " <> iniFile <> "." + putStrLn $ "Run `" <> executableName <> " start` to start server." warnCAPrivateKeyFile cfgPath x509cfg printServiceInfo serverVersion srv + printSourceCode sourceCode where createServerPassword = \case ServerPassword s -> pure s SPRandom -> BasicAuth . strEncode <$> (atomically . C.randomBytes 32 =<< C.newRandom) - iniFileContent host basicAuth = - "[STORE_LOG]\n\ - \# The server uses STM memory for persistence,\n\ - \# that will be lost on restart (e.g., as with redis).\n\ - \# This option enables saving memory to append only log,\n\ - \# and restoring it when the server is started.\n\ - \# Log is compacted on start (deleted objects are removed).\n" + iniFileContent host basicAuth sourceCode' = + informationIniContent sourceCode' + <> "[STORE_LOG]\n\ + \# The server uses STM memory for persistence,\n\ + \# that will be lost on restart (e.g., as with redis).\n\ + \# This option enables saving memory to append only log,\n\ + \# and restoring it when the server is started.\n\ + \# Log is compacted on start (deleted objects are removed).\n" <> ("enable: " <> onOff enableStoreLog <> "\n\n") <> "# Undelivered messages are optionally saved and restored when the server restarts,\n\ \# they are preserved in the .bak file until the next restart.\n" <> ("restore_messages: " <> onOff enableStoreLog <> "\n") - <> ("expire_messages_days: " <> show defMsgExpirationDays <> "\n\n") + <> ("expire_messages_days: " <> tshow defMsgExpirationDays <> "\n\n") <> "# Log daily server statistics to CSV file\n" <> ("log_stats: " <> onOff logStats <> "\n\n") <> "[AUTH]\n\ @@ -124,33 +161,64 @@ smpServerCLI cfgPath logPath = \# The password will not be shared with the connecting contacts, you must share it only\n\ \# with the users who you want to allow creating messaging queues on your server.\n" <> ( case basicAuth of - Just auth -> "create_password: " <> T.unpack (safeDecodeUtf8 $ strEncode auth) + Just auth -> "create_password: " <> safeDecodeUtf8 (strEncode auth) _ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')" ) <> "\n\n\ \# control_port_admin_password:\n\ - \# control_port_user_password:\n\ + \# control_port_user_password:\n\n\ \[TRANSPORT]\n\ \# host is only used to print server address on start\n" - <> ("host: " <> host <> "\n") - <> ("port: " <> defaultServerPort <> "\n") + <> ("host: " <> T.pack host <> "\n") + <> ("port: " <> T.pack defaultServerPort <> "\n") <> "log_tls_errors: off\n\ \websockets: off\n\ \# control_port: 5224\n\n\ - \[INACTIVE_CLIENTS]\n\ + \[PROXY]\n\ + \# Network configuration for SMP proxy client.\n\ + \# `host_mode` can be 'public' (default) or 'onion'.\n\ + \# It defines prefferred hostname for destination servers with multiple hostnames.\n\ + \# host_mode: public\n\ + \# required_host_mode: off\n\n\ + \# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics.\n\ + \# own_server_domains: \n\n\ + \# SOCKS proxy port for forwarding messages to destination servers.\n\ + \# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\ + \# socks_proxy: localhost:9050\n\n\ + \# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\ + \# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\ + \# socks_mode: onion\n\n\ + \# Limit number of threads a client can spawn to process proxy commands in parrallel.\n" + <> ("# client_concurrency: " <> tshow defaultProxyClientConcurrency <> "\n\n") + <> "[INACTIVE_CLIENTS]\n\ \# TTL and interval to check inactive clients\n\ \disconnect: off\n" - <> ("# ttl: " <> show (ttl defaultInactiveClientExpiration) <> "\n") - <> ("# check_interval: " <> show (checkInterval defaultInactiveClientExpiration) <> "\n") + <> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n") + <> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n") + <> "\n\n\ + \[WEB]\n\ + \# Set path to generate static mini-site for server information and qr codes/links\n" + <> ("static_path: " <> T.pack (fromMaybe defaultStaticPath webStaticPath) <> "\n\n") + <> "# 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") + <> "# 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") 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 {transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig + cfg@ServerConfig {information, transports, 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 + printSourceCode sourceCode' printServerConfig transports storeLogFile putStrLn $ case messageExpiration of Just ExpirationConfig {ttl} -> "expiring messages after " <> showTTL ttl @@ -163,6 +231,20 @@ smpServerCLI cfgPath logPath = <> if allowNewQueues cfg then maybe "allowed" (const "requires password") newQueueBasicAuth else "NOT allowed" + -- print information + let persistence + | isNothing storeLogFile = SPMMemoryOnly + | isJust (storeMsgsFile cfg) = SPMMessages + | otherwise = SPMQueues + let config = + ServerPublicConfig + { persistence, + messageExpiration = ttl <$> messageExpiration, + statsEnabled = isJust logStats, + newQueuesAllowed = allowNewQueues cfg, + basicAuthEnabled = isJust newQueueBasicAuth + } + runWebServer ini ServerInformation {config, information} runSMPServer cfg where enableStoreLog = settingIsOn "STORE_LOG" "enable" ini @@ -190,9 +272,9 @@ smpServerCLI cfgPath logPath = _ -> enableStoreLog $> messagesPath, -- allow creating new queues by default allowNewQueues = fromMaybe True $ iniOnOff "AUTH" "new_queues" ini, - newQueueBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini, - controlPortAdminAuth = either error id <$> strDecodeIni "AUTH" "control_port_admin_password" ini, - controlPortUserAuth = either error id <$> strDecodeIni "AUTH" "control_port_user_password" ini, + newQueueBasicAuth = either error id <$!> strDecodeIni "AUTH" "create_password" ini, + controlPortAdminAuth = either error id <$!> strDecodeIni "AUTH" "control_port_admin_password" ini, + controlPortUserAuth = either error id <$!> strDecodeIni "AUTH" "control_port_user_password" ini, messageExpiration = Just defaultMessageExpiration @@ -211,10 +293,163 @@ smpServerCLI cfgPath logPath = smpServerVRange = supportedServerSMPRelayVRange, transportConfig = defaultTransportServerConfig - { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini + { logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini, + alpn = Just supportedSMPHandshakes }, - controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini + controlPort = eitherToMaybe $ T.unpack <$> lookupValue "TRANSPORT" "control_port" ini, + smpAgentCfg = + defaultSMPClientAgentConfig + { smpCfg = + (smpCfg defaultSMPClientAgentConfig) + { serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, + agreeSecret = True, + networkConfig = + defaultNetworkConfig + { socksProxy = either error id <$!> strDecodeIni "PROXY" "socks_proxy" ini, + socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini, + hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini, + requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini + } + }, + ownServerDomains = either (const []) textToOwnServers $ lookupValue "PROXY" "own_server_domains" ini, + persistErrorInterval = 30 -- seconds + }, + allowSMPProxy = True, + serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini, + information = serverPublicInfo ini } + textToSocksMode :: Text -> SocksMode + textToSocksMode = \case + "always" -> SMAlways + "onion" -> SMOnion + s -> error . T.unpack $ "Invalid socks_mode: " <> s + textToHostMode :: Text -> HostMode + textToHostMode = \case + "public" -> HMPublic + "onion" -> HMOnionViaSocks + s -> error . T.unpack $ "Invalid host_mode: " <> s + 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 + 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 + +data EmbeddedWebParams = EmbeddedWebParams + { webStaticPath :: FilePath, + webHttpPort :: Maybe Int, + webHttpsParams :: Maybe WebHttpsParams + } + +data WebHttpsParams = WebHttpsParams + { port :: Int, + cert :: FilePath, + key :: FilePath + } + +getServerSourceCode :: IO (Maybe String) +getServerSourceCode = + getLine >>= \case + "" -> pure Nothing + s | "https://" `isPrefixOf` s || "http://" `isPrefixOf` s -> pure $ Just s + _ -> putStrLn "Invalid source code. URI should start from http:// or https://" >> getServerSourceCode + +simplexmqSource :: String +simplexmqSource = "https://github.com/simplex-chat/simplexmq" + +informationIniContent :: Maybe Text -> Text +informationIniContent sourceCode_ = + "[INFORMATION]\n\ + \# AGPLv3 license requires that you make any source code modifications\n\ + \# available to the end users of the server.\n\ + \# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE\n\ + \# Include correct source code URI in case the server source code is modified in any way.\n\ + \# If any other information fields are present, source code property also MUST be present.\n\n" + <> (maybe "# source_code: URI" ("source_code: " <>) sourceCode_ <> "\n\n") + <> "# Declaring all below information is optional, any of these fields can be omitted.\n\ + \\n\ + \# Server usage conditions and amendments.\n\ + \# It is recommended to use standard conditions with any amendments in a separate document.\n\ + \# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md\n\ + \# condition_amendments: link\n\ + \\n\ + \# Server location and operator.\n\ + \# server_country: ISO-3166 2-letter code\n\ + \# operator: entity (organization or person name)\n\ + \# operator_country: ISO-3166 2-letter code\n\ + \# website:\n\ + \\n\ + \# Administrative contacts.\n\ + \# admin_simplex: SimpleX address\n\ + \# admin_email:\n\ + \# admin_pgp:\n\ + \# admin_pgp_fingerprint:\n\ + \\n\ + \# Contacts for complaints and feedback.\n\ + \# complaints_simplex: SimpleX address\n\ + \# complaints_email:\n\ + \# complaints_pgp:\n\ + \# complaints_pgp_fingerprint:\n\ + \\n\ + \# Hosting provider.\n\ + \# hosting: entity (organization or person name)\n\ + \# hosting_country: ISO-3166 2-letter code\n\n" + +serverPublicInfo :: Ini -> Maybe ServerPublicInfo +serverPublicInfo ini = serverInfo <$!> infoValue "source_code" + where + serverInfo sourceCode = + ServerPublicInfo + { sourceCode, + usageConditions = + (\conditions -> ServerConditions {conditions, amendments = infoValue "condition_amendments"}) + <$!> infoValue "usage_conditions", + serverCountry = countryValue "server_country", + operator = iniEntity "operator" "operator_country", + website = infoValue "website", + adminContacts = iniContacts "admin_simplex" "admin_email" "admin_pgp" "admin_pgp_fingerprint", + complaintsContacts = iniContacts "complaints_simplex" "complaints_email" "complaints_pgp" "complaints_pgp_fingerprint", + hosting = iniEntity "hosting" "hosting_country" + } + infoValue name = eitherToMaybe $ lookupValue "INFORMATION" name ini + iniEntity nameField countryField = + (\name -> Entity {name, country = countryValue countryField}) + <$!> infoValue nameField + countryValue field = + (\cs -> if T.length cs == 2 && T.all (\c -> isAscii c && isAlpha c) cs then T.map toUpper cs else error $ "Use ISO3166 2-letter code for " <> T.unpack field) + <$!> infoValue field + iniContacts simplexField emailField pgpKeyUriField pgpKeyFingerprintField = + let simplex = either error id . parseAll (connReqUriP' Nothing) . encodeUtf8 <$!> eitherToMaybe (lookupValue "INFORMATION" simplexField ini) + email = infoValue emailField + pkURI_ = infoValue pgpKeyUriField + pkFingerprint_ = infoValue pgpKeyFingerprintField + in case (simplex, email, pkURI_, pkFingerprint_) of + (Nothing, Nothing, Nothing, _) -> Nothing + (Nothing, Nothing, _, Nothing) -> Nothing + (_, _, pkURI, pkFingerprint) -> Just ServerContactAddress {simplex, email, pgp = PGPKey <$> pkURI <*> pkFingerprint} + +printSourceCode :: Maybe Text -> IO () +printSourceCode = \case + Just sourceCode -> T.putStrLn $ "Server source code: " <> sourceCode + Nothing -> do + putStrLn "Warning: server source code is not specified." + putStrLn "Add 'source_code' property to [INFORMATION] section of INI file." data CliCommand = Init InitOptions @@ -229,6 +464,9 @@ data InitOptions = InitOptions ip :: HostName, fqdn :: Maybe HostName, password :: Maybe ServerPassword, + sourceCode :: Maybe Text, + webStaticPath :: Maybe FilePath, + disableWeb :: Bool, scripted :: Bool } deriving (Show) @@ -283,7 +521,6 @@ cliCommandP cfgPath logPath iniFile = ( long "fqdn" <> short 'n' <> help "Server FQDN used as Common Name for TLS online certificate" - <> showDefault <> metavar "FQDN" ) password <- @@ -296,13 +533,41 @@ cliCommandP cfgPath logPath iniFile = <> help "Set password to create new messaging queues" <> value SPRandom ) + sourceCode <- + (optional . strOption) + ( long "source-code" + <> help "Server source code will be communicated to the users" + <> metavar "URI" + ) + webStaticPath <- + (optional . strOption) + ( long "web-path" + <> help "Directory to store generated static site with server information" + <> metavar "PATH" + ) + disableWeb <- + switch + ( long "disable-web" + <> help "Disable starting static web server with server information" + ) scripted <- switch ( long "yes" <> short 'y' <> help "Non-interactive initialization using command-line options" ) - pure InitOptions {enableStoreLog, logStats, signAlgorithm, ip, fqdn, password, scripted} + pure + InitOptions + { enableStoreLog, + logStats, + signAlgorithm, + ip, + fqdn, + password, + sourceCode, + webStaticPath, + disableWeb, + scripted + } parseBasicAuth :: ReadM ServerPassword parseBasicAuth = eitherReader $ fmap ServerPassword . strDecode . B.pack - diff --git a/src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs b/src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs new file mode 100644 index 000000000..b329a54ff --- /dev/null +++ b/src/Simplex/Messaging/Server/QueueStore/QueueInfo.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Messaging.Server.QueueStore.QueueInfo where + +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Text (Text) +import Data.Time.Clock (UTCTime) +import Simplex.Messaging.Encoding +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) +import Simplex.Messaging.Util ((<$?>)) + +data QueueInfo = QueueInfo + { qiSnd :: Bool, + qiNtf :: Bool, + qiSub :: Maybe QSub, + qiSize :: Int, + qiMsg :: Maybe MsgInfo + } + deriving (Eq, Show) + +data QSub = QSub + { qSubThread :: QSubThread, + qDelivered :: Maybe Text + } + deriving (Eq, Show) + +data QSubThread = QNoSub | QSubPending | QSubThread | QProhibitSub + deriving (Eq, Show) + +data MsgInfo = MsgInfo + { msgId :: Text, + msgTs :: UTCTime, + msgType :: MsgType + } + deriving (Eq, Show) + +data MsgType = MTMessage | MTQuota + deriving (Eq, Show) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "Q") ''QSubThread) + +$(JQ.deriveJSON defaultJSON ''QSub) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "MT") ''MsgType) + +$(JQ.deriveJSON defaultJSON ''MsgInfo) + +$(JQ.deriveJSON defaultJSON ''QueueInfo) + +instance Encoding QueueInfo where + smpEncode = LB.toStrict . J.encode + smpP = J.eitherDecodeStrict <$?> A.takeByteString diff --git a/src/Simplex/Messaging/Server/QueueStore/STM.hs b/src/Simplex/Messaging/Server/QueueStore/STM.hs index b76ad4998..8de7a38c6 100644 --- a/src/Simplex/Messaging/Server/QueueStore/STM.hs +++ b/src/Simplex/Messaging/Server/QueueStore/STM.hs @@ -54,7 +54,7 @@ addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = where hasId = (||) <$> TM.member rId queues <*> TM.member sId senders -getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) +getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec) getQueue QueueStore {queues, senders, notifiers} party qId = toResult <$> (mapM readTVar =<< getVar) where diff --git a/src/Simplex/Messaging/Server/Stats.hs b/src/Simplex/Messaging/Server/Stats.hs index 0b4c677c2..880791c3d 100644 --- a/src/Simplex/Messaging/Server/Stats.hs +++ b/src/Simplex/Messaging/Server/Stats.hs @@ -26,13 +26,25 @@ data ServerStats = ServerStats qDeletedAll :: TVar Int, qDeletedNew :: TVar Int, qDeletedSecured :: TVar Int, + qSub :: TVar Int, + qSubAuth :: TVar Int, + qSubDuplicate :: TVar Int, + qSubProhibited :: TVar Int, msgSent :: TVar Int, + msgSentAuth :: TVar Int, + msgSentQuota :: TVar Int, + msgSentLarge :: TVar Int, msgRecv :: TVar Int, msgExpired :: TVar Int, activeQueues :: PeriodStats RecipientId, msgSentNtf :: TVar Int, msgRecvNtf :: TVar Int, activeQueuesNtf :: PeriodStats RecipientId, + pRelays :: ProxyStats, + pRelaysOwn :: ProxyStats, + pMsgFwds :: ProxyStats, + pMsgFwdsOwn :: ProxyStats, + pMsgFwdsRecv :: TVar Int, qCount :: TVar Int, msgCount :: TVar Int } @@ -44,13 +56,25 @@ data ServerStatsData = ServerStatsData _qDeletedAll :: Int, _qDeletedNew :: Int, _qDeletedSecured :: Int, + _qSub :: Int, + _qSubAuth :: Int, + _qSubDuplicate :: Int, + _qSubProhibited :: Int, _msgSent :: Int, + _msgSentAuth :: Int, + _msgSentQuota :: Int, + _msgSentLarge :: Int, _msgRecv :: Int, _msgExpired :: Int, _activeQueues :: PeriodStatsData RecipientId, _msgSentNtf :: Int, _msgRecvNtf :: Int, _activeQueuesNtf :: PeriodStatsData RecipientId, + _pRelays :: ProxyStatsData, + _pRelaysOwn :: ProxyStatsData, + _pMsgFwds :: ProxyStatsData, + _pMsgFwdsOwn :: ProxyStatsData, + _pMsgFwdsRecv :: Int, _qCount :: Int, _msgCount :: Int } @@ -64,16 +88,28 @@ newServerStats ts = do qDeletedAll <- newTVar 0 qDeletedNew <- newTVar 0 qDeletedSecured <- newTVar 0 + qSub <- newTVar 0 + qSubAuth <- newTVar 0 + qSubDuplicate <- newTVar 0 + qSubProhibited <- newTVar 0 msgSent <- newTVar 0 + msgSentAuth <- newTVar 0 + msgSentQuota <- newTVar 0 + msgSentLarge <- newTVar 0 msgRecv <- newTVar 0 msgExpired <- newTVar 0 activeQueues <- newPeriodStats msgSentNtf <- newTVar 0 msgRecvNtf <- newTVar 0 activeQueuesNtf <- newPeriodStats + pRelays <- newProxyStats + pRelaysOwn <- newProxyStats + pMsgFwds <- newProxyStats + pMsgFwdsOwn <- newProxyStats + pMsgFwdsRecv <- newTVar 0 qCount <- newTVar 0 msgCount <- newTVar 0 - pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount} + pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, qSub, qSubAuth, qSubDuplicate, qSubProhibited, msgSent, msgSentAuth, msgSentQuota, msgSentLarge, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, pRelays, pRelaysOwn, pMsgFwds, pMsgFwdsOwn, pMsgFwdsRecv, qCount, msgCount} getServerStatsData :: ServerStats -> STM ServerStatsData getServerStatsData s = do @@ -83,16 +119,28 @@ getServerStatsData s = do _qDeletedAll <- readTVar $ qDeletedAll s _qDeletedNew <- readTVar $ qDeletedNew s _qDeletedSecured <- readTVar $ qDeletedSecured s + _qSub <- readTVar $ qSub s + _qSubAuth <- readTVar $ qSubAuth s + _qSubDuplicate <- readTVar $ qSubDuplicate s + _qSubProhibited <- readTVar $ qSubProhibited s _msgSent <- readTVar $ msgSent s + _msgSentAuth <- readTVar $ msgSentAuth s + _msgSentQuota <- readTVar $ msgSentQuota s + _msgSentLarge <- readTVar $ msgSentLarge s _msgRecv <- readTVar $ msgRecv s _msgExpired <- readTVar $ msgExpired s _activeQueues <- getPeriodStatsData $ activeQueues s _msgSentNtf <- readTVar $ msgSentNtf s _msgRecvNtf <- readTVar $ msgRecvNtf s _activeQueuesNtf <- getPeriodStatsData $ activeQueuesNtf s + _pRelays <- getProxyStatsData $ pRelays s + _pRelaysOwn <- getProxyStatsData $ pRelaysOwn s + _pMsgFwds <- getProxyStatsData $ pMsgFwds s + _pMsgFwdsOwn <- getProxyStatsData $ pMsgFwdsOwn s + _pMsgFwdsRecv <- readTVar $ pMsgFwdsRecv s _qCount <- readTVar $ qCount s _msgCount <- readTVar $ msgCount s - pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _qCount, _msgCount} + pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _qSub, _qSubAuth, _qSubDuplicate, _qSubProhibited, _msgSent, _msgSentAuth, _msgSentQuota, _msgSentLarge, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount} setServerStats :: ServerStats -> ServerStatsData -> STM () setServerStats s d = do @@ -102,35 +150,63 @@ setServerStats s d = do writeTVar (qDeletedAll s) $! _qDeletedAll d writeTVar (qDeletedNew s) $! _qDeletedNew d writeTVar (qDeletedSecured s) $! _qDeletedSecured d + writeTVar (qSub s) $! _qSub d + writeTVar (qSubAuth s) $! _qSubAuth d + writeTVar (qSubDuplicate s) $! _qSubDuplicate d + writeTVar (qSubProhibited s) $! _qSubProhibited d writeTVar (msgSent s) $! _msgSent d + writeTVar (msgSentAuth s) $! _msgSentAuth d + writeTVar (msgSentQuota s) $! _msgSentQuota d + writeTVar (msgSentLarge s) $! _msgSentLarge d writeTVar (msgRecv s) $! _msgRecv d writeTVar (msgExpired s) $! _msgExpired d setPeriodStats (activeQueues s) (_activeQueues d) writeTVar (msgSentNtf s) $! _msgSentNtf d writeTVar (msgRecvNtf s) $! _msgRecvNtf d setPeriodStats (activeQueuesNtf s) (_activeQueuesNtf d) + setProxyStats (pRelays s) $! _pRelays d + setProxyStats (pRelaysOwn s) $! _pRelaysOwn d + setProxyStats (pMsgFwds s) $! _pMsgFwds d + setProxyStats (pMsgFwdsOwn s) $! _pMsgFwdsOwn d + writeTVar (pMsgFwdsRecv s) $! _pMsgFwdsRecv d writeTVar (qCount s) $! _qCount d writeTVar (msgCount s) $! _msgCount d instance StrEncoding ServerStatsData where - strEncode ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount} = + strEncode d = B.unlines - [ "fromTime=" <> strEncode _fromTime, - "qCreated=" <> strEncode _qCreated, - "qSecured=" <> strEncode _qSecured, - "qDeletedAll=" <> strEncode _qDeletedAll, - "qDeletedNew=" <> strEncode _qDeletedNew, - "qDeletedSecured=" <> strEncode _qDeletedSecured, - "qCount=" <> strEncode _qCount, - "msgSent=" <> strEncode _msgSent, - "msgRecv=" <> strEncode _msgRecv, - "msgExpired=" <> strEncode _msgExpired, - "msgSentNtf=" <> strEncode _msgSentNtf, - "msgRecvNtf=" <> strEncode _msgRecvNtf, + [ "fromTime=" <> strEncode (_fromTime d), + "qCreated=" <> strEncode (_qCreated d), + "qSecured=" <> strEncode (_qSecured d), + "qDeletedAll=" <> strEncode (_qDeletedAll d), + "qDeletedNew=" <> strEncode (_qDeletedNew d), + "qDeletedSecured=" <> strEncode (_qDeletedSecured d), + "qCount=" <> strEncode (_qCount d), + "qSub=" <> strEncode (_qSub d), + "qSubAuth=" <> strEncode (_qSubAuth d), + "qSubDuplicate=" <> strEncode (_qSubDuplicate d), + "qSubProhibited=" <> strEncode (_qSubProhibited d), + "msgSent=" <> strEncode (_msgSent d), + "msgSentAuth=" <> strEncode (_msgSentAuth d), + "msgSentQuota=" <> strEncode (_msgSentQuota d), + "msgSentLarge=" <> strEncode (_msgSentLarge d), + "msgRecv=" <> strEncode (_msgRecv d), + "msgExpired=" <> strEncode (_msgExpired d), + "msgSentNtf=" <> strEncode (_msgSentNtf d), + "msgRecvNtf=" <> strEncode (_msgRecvNtf d), "activeQueues:", - strEncode _activeQueues, + strEncode (_activeQueues d), "activeQueuesNtf:", - strEncode _activeQueuesNtf + strEncode (_activeQueuesNtf d), + "pRelays:", + strEncode (_pRelays d), + "pRelaysOwn:", + strEncode (_pRelaysOwn d), + "pMsgFwds:", + strEncode (_pMsgFwds d), + "pMsgFwdsOwn:", + strEncode (_pMsgFwdsOwn d), + "pMsgFwdsRecv=" <> strEncode (_pMsgFwdsRecv d) ] strP = do _fromTime <- "fromTime=" *> strP <* A.endOfLine @@ -139,12 +215,19 @@ instance StrEncoding ServerStatsData where (_qDeletedAll, _qDeletedNew, _qDeletedSecured) <- (,0,0) <$> ("qDeleted=" *> strP <* A.endOfLine) <|> ((,,) <$> ("qDeletedAll=" *> strP <* A.endOfLine) <*> ("qDeletedNew=" *> strP <* A.endOfLine) <*> ("qDeletedSecured=" *> strP <* A.endOfLine)) - _qCount <- "qCount=" *> strP <* A.endOfLine <|> pure 0 + _qCount <- opt "qCount=" + _qSub <- opt "qSub=" + _qSubAuth <- opt "qSubAuth=" + _qSubDuplicate <- opt "qSubDuplicate=" + _qSubProhibited <- opt "qSubProhibited=" _msgSent <- "msgSent=" *> strP <* A.endOfLine + _msgSentAuth <- opt "msgSentAuth=" + _msgSentQuota <- opt "msgSentQuota=" + _msgSentLarge <- opt "msgSentLarge=" _msgRecv <- "msgRecv=" *> strP <* A.endOfLine - _msgExpired <- "msgExpired=" *> strP <* A.endOfLine <|> pure 0 - _msgSentNtf <- "msgSentNtf=" *> strP <* A.endOfLine <|> pure 0 - _msgRecvNtf <- "msgRecvNtf=" *> strP <* A.endOfLine <|> pure 0 + _msgExpired <- opt "msgExpired=" + _msgSentNtf <- opt "msgSentNtf=" + _msgRecvNtf <- opt "msgRecvNtf=" _activeQueues <- optional ("activeQueues:" <* A.endOfLine) >>= \case Just _ -> strP <* optional A.endOfLine @@ -157,7 +240,18 @@ instance StrEncoding ServerStatsData where optional ("activeQueuesNtf:" <* A.endOfLine) >>= \case Just _ -> strP <* optional A.endOfLine _ -> pure newPeriodStatsData - pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount = 0} + _pRelays <- proxyStatsP "pRelays:" + _pRelaysOwn <- proxyStatsP "pRelaysOwn:" + _pMsgFwds <- proxyStatsP "pMsgFwds:" + _pMsgFwdsOwn <- proxyStatsP "pMsgFwdsOwn:" + _pMsgFwdsRecv <- opt "pMsgFwdsRecv=" + pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _qSub, _qSubAuth, _qSubDuplicate, _qSubProhibited, _msgSent, _msgSentAuth, _msgSentQuota, _msgSentLarge, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _pRelays, _pRelaysOwn, _pMsgFwds, _pMsgFwdsOwn, _pMsgFwdsRecv, _qCount, _msgCount = 0} + where + opt s = A.string s *> strP <* A.endOfLine <|> pure 0 + proxyStatsP key = + optional (A.string key >> A.endOfLine) >>= \case + Just _ -> strP <* optional A.endOfLine + _ -> pure newProxyStatsData data PeriodStats a = PeriodStats { day :: TVar (Set a), @@ -231,3 +325,78 @@ updatePeriodStats stats pId = do updatePeriod month where updatePeriod pSel = modifyTVar' (pSel stats) (S.insert pId) + +data ProxyStats = ProxyStats + { pRequests :: TVar Int, + pSuccesses :: TVar Int, -- includes destination server error responses that will be forwarded to the client + pErrorsConnect :: TVar Int, + pErrorsCompat :: TVar Int, + pErrorsOther :: TVar Int + } + +newProxyStats :: STM ProxyStats +newProxyStats = do + pRequests <- newTVar 0 + pSuccesses <- newTVar 0 + pErrorsConnect <- newTVar 0 + pErrorsCompat <- newTVar 0 + pErrorsOther <- newTVar 0 + pure ProxyStats {pRequests, pSuccesses, pErrorsConnect, pErrorsCompat, pErrorsOther} + +data ProxyStatsData = ProxyStatsData + { _pRequests :: Int, + _pSuccesses :: Int, + _pErrorsConnect :: Int, + _pErrorsCompat :: Int, + _pErrorsOther :: Int + } + deriving (Show) + +newProxyStatsData :: ProxyStatsData +newProxyStatsData = ProxyStatsData {_pRequests = 0, _pSuccesses = 0, _pErrorsConnect = 0, _pErrorsCompat = 0, _pErrorsOther = 0} + +getProxyStatsData :: ProxyStats -> STM ProxyStatsData +getProxyStatsData s = do + _pRequests <- readTVar $ pRequests s + _pSuccesses <- readTVar $ pSuccesses s + _pErrorsConnect <- readTVar $ pErrorsConnect s + _pErrorsCompat <- readTVar $ pErrorsCompat s + _pErrorsOther <- readTVar $ pErrorsOther s + pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} + +getResetProxyStatsData :: ProxyStats -> STM ProxyStatsData +getResetProxyStatsData s = do + _pRequests <- swapTVar (pRequests s) 0 + _pSuccesses <- swapTVar (pSuccesses s) 0 + _pErrorsConnect <- swapTVar (pErrorsConnect s) 0 + _pErrorsCompat <- swapTVar (pErrorsCompat s) 0 + _pErrorsOther <- swapTVar (pErrorsOther s) 0 + pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} + +setProxyStats :: ProxyStats -> ProxyStatsData -> STM () +setProxyStats s d = do + writeTVar (pRequests s) $! _pRequests d + writeTVar (pSuccesses s) $! _pSuccesses d + writeTVar (pErrorsConnect s) $! _pErrorsConnect d + writeTVar (pErrorsCompat s) $! _pErrorsCompat d + writeTVar (pErrorsOther s) $! _pErrorsOther d + +instance StrEncoding ProxyStatsData where + strEncode ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} = + "requests=" + <> strEncode _pRequests + <> "\nsuccesses=" + <> strEncode _pSuccesses + <> "\nerrorsConnect=" + <> strEncode _pErrorsConnect + <> "\nerrorsCompat=" + <> strEncode _pErrorsCompat + <> "\nerrorsOther=" + <> strEncode _pErrorsOther + strP = do + _pRequests <- "requests=" *> strP <* A.endOfLine + _pSuccesses <- "successes=" *> strP <* A.endOfLine + _pErrorsConnect <- "errorsConnect=" *> strP <* A.endOfLine + _pErrorsCompat <- "errorsCompat=" *> strP <* A.endOfLine + _pErrorsOther <- "errorsOther=" *> strP + pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} diff --git a/src/Simplex/Messaging/Session.hs b/src/Simplex/Messaging/Session.hs new file mode 100644 index 000000000..3ce5a35c8 --- /dev/null +++ b/src/Simplex/Messaging/Session.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Simplex.Messaging.Session where + +import Control.Concurrent.STM +import Control.Monad +import Data.Composition ((.:.)) +import Data.Functor (($>)) +import Data.Time (UTCTime) +import Simplex.Messaging.TMap (TMap) +import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Util (($>>=)) + +data SessionVar a = SessionVar + { sessionVar :: TMVar a, + sessionVarId :: Int, + sessionVarTs :: UTCTime + } + +getSessVar :: forall k a. Ord k => TVar Int -> k -> TMap k (SessionVar a) -> UTCTime -> STM (Either (SessionVar a) (SessionVar a)) +getSessVar sessSeq sessKey vs sessionVarTs = maybe (Left <$> newSessionVar) (pure . Right) =<< TM.lookup sessKey vs + where + newSessionVar :: STM (SessionVar a) + newSessionVar = do + sessionVar <- newEmptyTMVar + sessionVarId <- stateTVar sessSeq $ \next -> (next, next + 1) + let v = SessionVar {sessionVar, sessionVarId, sessionVarTs} + TM.insert sessKey v vs + pure v + +removeSessVar :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM () +removeSessVar = void .:. removeSessVar' +{-# INLINE removeSessVar #-} + +removeSessVar' :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM Bool +removeSessVar' v sessKey vs = + TM.lookup sessKey vs >>= \case + Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True + _ -> pure False + +tryReadSessVar :: Ord k => k -> TMap k (SessionVar a) -> STM (Maybe a) +tryReadSessVar sessKey vs = TM.lookup sessKey vs $>>= (tryReadTMVar . sessionVar) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6898af15d..6eddcabf8 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -12,6 +12,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} -- | @@ -32,14 +33,19 @@ module Simplex.Messaging.Transport VersionSMP, VersionRangeSMP, THandleSMP, + supportedSMPHandshakes, supportedClientSMPRelayVRange, supportedServerSMPRelayVRange, + proxiedSMPRelayVRange, + legacyServerSMPRelayVRange, currentClientSMPRelayVersion, + legacyServerSMPRelayVersion, currentServerSMPRelayVersion, batchCmdsSMPVersion, basicAuthSMPVersion, subModeSMPVersion, authCmdsSMPVersion, + sendingProxySMPVersion, simplexMQVersion, smpBlockSize, TransportConfig (..), @@ -70,14 +76,13 @@ module Simplex.Messaging.Transport smpClientHandshake, tPutBlock, tGetBlock, - serializeTransportError, - transportErrorP, sendHandshake, getHandshake, + smpTHParamsSetVersion, ) where -import Control.Applicative ((<|>)) +import Control.Applicative (optional) import Control.Monad (forM) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) @@ -123,7 +128,7 @@ smpBlockSize = 16384 -- 4 - support command batching (7/17/2022) -- 5 - basic auth for SMP servers (11/12/2022) -- 6 - allow creating queues without subscribing (9/10/2023) --- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (2/3/2024) +-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (4/30/2024) data SMPVersion @@ -148,20 +153,44 @@ subModeSMPVersion = VersionSMP 6 authCmdsSMPVersion :: VersionSMP authCmdsSMPVersion = VersionSMP 7 +sendingProxySMPVersion :: VersionSMP +sendingProxySMPVersion = VersionSMP 8 + currentClientSMPRelayVersion :: VersionSMP -currentClientSMPRelayVersion = VersionSMP 6 +currentClientSMPRelayVersion = VersionSMP 8 + +legacyServerSMPRelayVersion :: VersionSMP +legacyServerSMPRelayVersion = VersionSMP 6 currentServerSMPRelayVersion :: VersionSMP -currentServerSMPRelayVersion = VersionSMP 6 +currentServerSMPRelayVersion = VersionSMP 8 + +-- Max SMP protocol version to be used in e2e encrypted +-- connection between client and server, as defined by SMP proxy. +-- SMP proxy sets it to lower than its current version +-- to prevent client version fingerprinting by the +-- destination relays when clients upgrade at different times. +proxiedSMPRelayVersion :: VersionSMP +proxiedSMPRelayVersion = VersionSMP 8 -- minimal supported protocol version is 4 -- TODO remove code that supports sending commands without batching supportedClientSMPRelayVRange :: VersionRangeSMP supportedClientSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentClientSMPRelayVersion +legacyServerSMPRelayVRange :: VersionRangeSMP +legacyServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion legacyServerSMPRelayVersion + supportedServerSMPRelayVRange :: VersionRangeSMP supportedServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentServerSMPRelayVersion +-- This range initially allows only version 8 - see the comment above. +proxiedSMPRelayVRange :: VersionRangeSMP +proxiedSMPRelayVRange = mkVersionRange sendingProxySMPVersion proxiedSMPRelayVersion + +supportedSMPHandshakes :: [ALPN] +supportedSMPHandshakes = ["smp/1"] + simplexMQVersion :: String simplexMQVersion = showVersion SMQ.version @@ -193,6 +222,9 @@ class Transport c where -- | tls-unique channel binding per RFC5929 tlsUnique :: c -> SessionId + -- | ALPN value negotiated for the session + getSessionALPN :: c -> Maybe ALPN + -- | Close connection closeConnection :: c -> IO () @@ -287,6 +319,7 @@ instance Transport TLS where getServerConnection = getTLS TServer getClientConnection = getTLS TClient getServerCerts = tlsServerCerts + getSessionALPN = tlsALPN tlsUnique = tlsUniq closeConnection tls = closeTLS $ tlsContext tls @@ -311,20 +344,22 @@ instance Transport TLS where -- * SMP transport -- | The handle for SMP encrypted transport connection over Transport. -data THandle v c = THandle +data THandle v c p = THandle { connection :: c, - params :: THandleParams v + params :: THandleParams v p } -type THandleSMP c = THandle SMPVersion c +type THandleSMP c p = THandle SMPVersion c p -data THandleParams v = THandleParams +data THandleParams v p = THandleParams { sessionId :: SessionId, blockSize :: Int, + -- | server protocol version range + thServerVRange :: VersionRange v, -- | agreed server protocol version thVersion :: Version v, -- | peer public key for command authorization and shared secrets for entity ID encryption - thAuth :: Maybe THandleAuth, + thAuth :: Maybe (THandleAuth p), -- | do NOT send session ID in transmission, but include it into signed message -- based on protocol version implySessId :: Bool, @@ -333,10 +368,18 @@ data THandleParams v = THandleParams batch :: Bool } -data THandleAuth = THandleAuth - { peerPubKey :: C.PublicKeyX25519, -- used only in the client to combine with per-queue key - privKey :: C.PrivateKeyX25519 -- used to combine with peer's per-queue key (currently only in the server) - } +data THandleAuth (p :: TransportPeer) where + THAuthClient :: + { serverPeerPubKey :: C.PublicKeyX25519, -- used by the client to combine with client's private per-queue key + serverCertKey :: (X.CertificateChain, X.SignedExact X.PubKey), -- the key here is serverPeerPubKey signed with server certificate + sessSecret :: Maybe C.DhSecretX25519 -- session secret (will be used in SMP proxy only) + } -> + THandleAuth 'TClient + THAuthServer :: + { serverPrivKey :: C.PrivateKeyX25519, -- used by the server to combine with client's public per-queue key + sessSecret' :: Maybe C.DhSecretX25519 -- session secret (will be used in SMP proxy only) + } -> + THandleAuth 'TServer -- | TLS-unique channel binding type SessionId = ByteString @@ -345,6 +388,7 @@ data ServerHandshake = ServerHandshake { smpVersionRange :: VersionRangeSMP, sessionId :: SessionId, -- pub key to agree shared secrets for command authorization and entity ID encryption. + -- todo C.PublicKeyX25519 authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey) } @@ -390,12 +434,14 @@ encodeAuthEncryptCmds v k | otherwise = "" authEncryptCmdsP :: VersionSMP -> Parser a -> Parser (Maybe a) -authEncryptCmdsP v p = if v >= authCmdsSMPVersion then Just <$> p else pure Nothing +authEncryptCmdsP v p = if v >= authCmdsSMPVersion then optional p else pure Nothing -- | Error of SMP encrypted transport over TCP. data TransportError = -- | error parsing transport block TEBadBlock + | -- | incompatible client or server version + TEVersion | -- | message does not fit in transport block TELargeMsg | -- | incorrect session ID @@ -411,40 +457,38 @@ data TransportError data HandshakeError = -- | parsing error PARSE - | -- | incompatible peer version - VERSION | -- | incorrect server identity IDENTITY | -- | v7 authentication failed BAD_AUTH deriving (Eq, Read, Show, Exception) --- | SMP encrypted transport error parser. -transportErrorP :: Parser TransportError -transportErrorP = - "BLOCK" $> TEBadBlock - <|> "LARGE_MSG" $> TELargeMsg - <|> "SESSION" $> TEBadSession - <|> "NO_AUTH" $> TENoServerAuth - <|> "HANDSHAKE " *> (TEHandshake <$> parseRead1) - --- | Serialize SMP encrypted transport error. -serializeTransportError :: TransportError -> ByteString -serializeTransportError = \case - TEBadBlock -> "BLOCK" - TELargeMsg -> "LARGE_MSG" - TEBadSession -> "SESSION" - TENoServerAuth -> "NO_AUTH" - TEHandshake e -> "HANDSHAKE " <> bshow e +instance Encoding TransportError where + smpP = + A.takeTill (== ' ') >>= \case + "BLOCK" -> pure TEBadBlock + "VERSION" -> pure TEVersion + "LARGE_MSG" -> pure TELargeMsg + "SESSION" -> pure TEBadSession + "NO_AUTH" -> pure TENoServerAuth + "HANDSHAKE" -> TEHandshake <$> (A.space *> parseRead1) + _ -> fail "bad TransportError" + smpEncode = \case + TEBadBlock -> "BLOCK" + TEVersion -> "VERSION" + TELargeMsg -> "LARGE_MSG" + TEBadSession -> "SESSION" + TENoServerAuth -> "NO_AUTH" + TEHandshake e -> "HANDSHAKE " <> bshow e -- | Pad and send block to SMP transport. -tPutBlock :: Transport c => THandle v c -> ByteString -> IO (Either TransportError ()) +tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ()) tPutBlock THandle {connection = c, params = THandleParams {blockSize}} block = bimapM (const $ pure TELargeMsg) (cPut c) $ C.pad block blockSize -- | Receive block from SMP transport. -tGetBlock :: Transport c => THandle v c -> IO (Either TransportError ByteString) +tGetBlock :: Transport c => THandle v c p -> IO (Either TransportError ByteString) tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do msg <- cGet c blockSize if B.length msg == blockSize @@ -454,61 +498,89 @@ tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do -- | Server SMP transport handshake. -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c) +smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TServer) smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c sk = C.signX509 serverSignKey $ C.publicToX509 k certChain = getServerCerts c - sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = smpVRange, authPubKey = Just (certChain, sk)} + smpVersionRange = maybe legacyServerSMPRelayVRange (const smpVRange) $ getSessionALPN c + sendHandshake th $ ServerHandshake {sessionId, smpVersionRange, authPubKey = Just (certChain, sk)} getHandshake th >>= \case ClientHandshake {smpVersion = v, keyHash, authPubKey = k'} | keyHash /= kh -> throwE $ TEHandshake IDENTITY - | v `isCompatible` smpVRange -> - pure $ smpThHandle th v pk k' - | otherwise -> throwE $ TEHandshake VERSION + | otherwise -> + case compatibleVRange' smpVersionRange v of + Just (Compatible vr) -> pure $ smpTHandleServer th v vr pk k' + Nothing -> throwE TEVersion -- | Client SMP transport handshake. -- -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a -smpClientHandshake :: forall c. Transport c => c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c) -smpClientHandshake c (k, pk) keyHash@(C.KeyHash kh) smpVRange = do +smpClientHandshake :: forall c. Transport c => c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TClient) +smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do let th@THandle {params = THandleParams {sessionId}} = smpTHandle c ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th if sessionId /= sessId then throwE TEBadSession - else case smpVersionRange `compatibleVersion` smpVRange of - Just (Compatible v) -> do - sk_ <- forM authPubKey $ \(X.CertificateChain cert, exact) -> + else case smpVersionRange `compatibleVRange` smpVRange of + Just (Compatible vr) -> do + ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do case cert of [_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure () _ -> throwError "bad certificate" serverKey <- getServerVerifyKey c pubKey <- C.verifyX509 serverKey exact - C.x509ToPublic (pubKey, []) >>= C.pubKey - sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = Just k} - pure $ smpThHandle th v pk sk_ - Nothing -> throwE $ TEHandshake VERSION + (,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey) + let v = maxVersion vr + sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_} + pure $ smpTHandleClient th v vr (snd <$> ks_) ck_ + Nothing -> throwE TEVersion -smpThHandle :: forall c. THandleSMP c -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c -smpThHandle th@THandle {params} v privKey k_ = +smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer +smpTHandleServer th v vr pk k_ = + let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_} + in smpTHandle_ th v vr (Just thAuth) + +smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient +smpTHandleClient th v vr pk_ ck_ = + let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_ + in smpTHandle_ th v vr thAuth + +smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p +smpTHandle_ th@THandle {params} v vr thAuth = -- TODO drop SMP v6: make thAuth non-optional - let thAuth = (\k -> THandleAuth {peerPubKey = k, privKey}) <$> k_ - params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion} - in (th :: THandleSMP c) {params = params'} + let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion} + in (th :: THandleSMP c p) {params = params'} -sendHandshake :: (Transport c, Encoding smp) => THandle v c -> smp -> ExceptT TransportError IO () +-- This function is only used with v >= 8, so currently it's a simple record update. +-- It may require some parameters update in the future, to be consistent with smpTHandle_. +smpTHParamsSetVersion :: VersionSMP -> THandleParams SMPVersion p -> THandleParams SMPVersion p +smpTHParamsSetVersion v params = params {thVersion = v} +{-# INLINE smpTHParamsSetVersion #-} + +sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO () sendHandshake th = ExceptT . tPutBlock th . smpEncode -- ignores tail bytes to allow future extensions -getHandshake :: (Transport c, Encoding smp) => THandle v c -> ExceptT TransportError IO smp +getHandshake :: (Transport c, Encoding smp) => THandle v c p -> ExceptT TransportError IO smp getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP =<<) <$> tGetBlock th -smpTHandle :: Transport c => c -> THandleSMP c +smpTHandle :: Transport c => c -> THandleSMP c p smpTHandle c = THandle {connection = c, params} where - params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True} + v = VersionSMP 0 + params = + THandleParams + { sessionId = tlsUnique c, + blockSize = smpBlockSize, + thServerVRange = versionToRange v, + thVersion = v, + thAuth = Nothing, + implySessId = False, + batch = True + } $(J.deriveJSON (sumTypeJSON id) ''HandshakeError) diff --git a/src/Simplex/Messaging/Transport/Client.hs b/src/Simplex/Messaging/Transport/Client.hs index daea3982e..da2c6c253 100644 --- a/src/Simplex/Messaging/Transport/Client.hs +++ b/src/Simplex/Messaging/Transport/Client.hs @@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -10,6 +11,7 @@ module Simplex.Messaging.Transport.Client runTLSTransportClient, smpClientHandshake, defaultSMPPort, + defaultTcpConnectTimeout, defaultTransportClientConfig, defaultSocksProxy, TransportClientConfig (..), @@ -17,7 +19,7 @@ module Simplex.Messaging.Transport.Client TransportHost (..), TransportHosts (..), TransportHosts_ (..), - validateCertificateChain + validateCertificateChain, ) where @@ -50,7 +52,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll, parseString) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.KeepAlive -import Simplex.Messaging.Util (bshow, (<$?>), catchAll, tshow) +import Simplex.Messaging.Util (bshow, catchAll, tshow, (<$?>)) import System.IO.Error import Text.Read (readMaybe) import UnliftIO.Exception (IOException) @@ -112,6 +114,7 @@ instance IsString (NonEmpty TransportHost) where fromString = parseString strDec data TransportClientConfig = TransportClientConfig { socksProxy :: Maybe SocksProxy, + tcpConnectTimeout :: Int, tcpKeepAlive :: Maybe KeepAliveOpts, logTLSErrors :: Bool, clientCredentials :: Maybe (X.CertificateChain, T.PrivKey), @@ -119,8 +122,12 @@ data TransportClientConfig = TransportClientConfig } deriving (Eq, Show) +-- time to resolve host, connect socket, set up TLS +defaultTcpConnectTimeout :: Int +defaultTcpConnectTimeout = 25_000_000 + defaultTransportClientConfig :: TransportClientConfig -defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True Nothing Nothing +defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing clientTransportConfig :: TransportClientConfig -> TransportConfig clientTransportConfig TransportClientConfig {logTLSErrors} = @@ -136,19 +143,21 @@ runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, let hostName = B.unpack $ strEncode host clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert connectTCP = case socksProxy of - Just proxy -> connectSocksClient proxy proxyUsername $ hostAddr host + Just proxy -> connectSocksClient proxy proxyUsername (hostAddr host) _ -> connectTCPClient hostName c <- do sock <- connectTCP port mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e) let tCfg = clientTransportConfig cfg - connectTLS (Just hostName) tCfg clientParams sock >>= \tls -> do - chain <- atomically (tryTakeTMVar serverCert) >>= \case + -- No TLS timeout to avoid failing connections via SOCKS + tls <- connectTLS (Just hostName) tCfg clientParams sock + chain <- + atomically (tryTakeTMVar serverCert) >>= \case Nothing -> do logError "onServerCertificate didn't fire or failed to get cert chain" closeTLS tls >> error "onServerCertificate failed" Just c -> pure c - getClientConnection tCfg chain tls + getClientConnection tCfg chain tls client c `E.finally` closeConnection c where hostAddr = \case diff --git a/src/Simplex/Messaging/Transport/HTTP2/Client.hs b/src/Simplex/Messaging/Transport/HTTP2/Client.hs index b279c1805..71757ca6d 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/Client.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/Client.hs @@ -14,6 +14,7 @@ import Control.Monad import Data.ByteString.Char8 (ByteString) import Data.Functor (($>)) import Data.Time (UTCTime, getCurrentTime) +import qualified Data.X509 as X import qualified Data.X509.CertificateStore as XS import Network.HPACK (BufferSize) import Network.HTTP2.Client (ClientConfig (..), Request, Response) @@ -24,12 +25,11 @@ import Numeric.Natural (Natural) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Transport (ALPN, SessionId, TLS (tlsALPN), getServerCerts, getServerVerifyKey, tlsUniq) -import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), runTLSTransportClient) +import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTLSTransportClient) import Simplex.Messaging.Transport.HTTP2 import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM import UnliftIO.Timeout -import qualified Data.X509 as X data HTTP2Client = HTTP2Client { action :: Maybe (Async HTTP2Response), @@ -70,8 +70,16 @@ defaultHTTP2ClientConfig :: HTTP2ClientConfig defaultHTTP2ClientConfig = HTTP2ClientConfig { qSize = 64, - connTimeout = 10000000, - transportConfig = TransportClientConfig Nothing Nothing True Nothing Nothing, + connTimeout = defaultTcpConnectTimeout, + transportConfig = + TransportClientConfig + { socksProxy = Nothing, + tcpConnectTimeout = defaultTcpConnectTimeout, + tcpKeepAlive = Nothing, + logTLSErrors = True, + clientCredentials = Nothing, + alpn = Nothing + }, bufferSize = defaultHTTP2BufferSize, bodyHeadSize = 16384, suportedTLSParams = http2TLSParams diff --git a/src/Simplex/Messaging/Transport/HTTP2/File.hs b/src/Simplex/Messaging/Transport/HTTP2/File.hs index 10238f161..aef98acaa 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/File.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/File.hs @@ -23,7 +23,7 @@ hReceiveFile getBody h size = get $ fromIntegral size if | chSize > sz -> pure (chSize - sz) | chSize > 0 -> B.hPut h ch >> get (sz - chSize) - | otherwise -> pure (-fromIntegral sz) + | otherwise -> pure (-sz) hSendFile :: Handle -> (Builder -> IO ()) -> Word32 -> IO () hSendFile h send = go diff --git a/src/Simplex/Messaging/Transport/HTTP2/Server.hs b/src/Simplex/Messaging/Transport/HTTP2/Server.hs index c75d8fa31..f8ea1bd1d 100644 --- a/src/Simplex/Messaging/Transport/HTTP2/Server.hs +++ b/src/Simplex/Messaging/Transport/HTTP2/Server.hs @@ -51,7 +51,7 @@ data HTTP2Server = HTTP2Server -- This server is for testing only, it processes all requests in a single queue. getHTTP2Server :: HTTP2ServerConfig -> IO HTTP2Server getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, serverSupported, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do - tlsServerParams <- loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile + tlsServerParams <- loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile (alpn transportConfig) started <- newEmptyTMVarIO reqQ <- newTBQueueIO qSize action <- async $ diff --git a/src/Simplex/Messaging/Transport/Server.hs b/src/Simplex/Messaging/Transport/Server.hs index 542ebbb75..ffde39991 100644 --- a/src/Simplex/Messaging/Transport/Server.hs +++ b/src/Simplex/Messaging/Transport/Server.hs @@ -19,7 +19,7 @@ module Simplex.Messaging.Transport.Server loadTLSServerParams, loadFingerprint, smpServerHandshake, - tlsServerCredentials + tlsServerCredentials, ) where @@ -28,18 +28,21 @@ import Control.Logger.Simple import Control.Monad import qualified Crypto.Store.X509 as SX import Data.Default (def) -import Data.List (find) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IM -import Data.Maybe (fromJust) +import Data.List (find) +import Data.Maybe (fromJust, fromMaybe) import qualified Data.X509 as X import Data.X509.Validation (Fingerprint (..)) import qualified Data.X509.Validation as XV +import Foreign.C.Error +import GHC.IO.Exception (ioe_errno) import Network.Socket import qualified Network.TLS as T import Simplex.Messaging.Transport import Simplex.Messaging.Util (catchAll_, labelMyThread, tshow) import System.Exit (exitFailure) +import System.IO.Error (tryIOError) import System.Mem.Weak (Weak, deRefWeak) import UnliftIO (timeout) import UnliftIO.Concurrent @@ -49,7 +52,8 @@ import UnliftIO.STM data TransportServerConfig = TransportServerConfig { logTLSErrors :: Bool, tlsSetupTimeout :: Int, - transportTimeout :: Int + transportTimeout :: Int, + alpn :: Maybe [ALPN] } deriving (Eq, Show) @@ -58,7 +62,8 @@ defaultTransportServerConfig = TransportServerConfig { logTLSErrors = True, tlsSetupTimeout = 60000000, - transportTimeout = 40000000 + transportTimeout = 40000000, + alpn = Nothing } serverTransportConfig :: TransportServerConfig -> TransportConfig @@ -111,15 +116,36 @@ runTCPServer started port server = do runTCPServerSocket :: SocketState -> TMVar Bool -> IO Socket -> (Socket -> IO ()) -> IO () runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket server = E.bracket getSocket (closeServer started clients) $ \sock -> - forever . E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do + forever . E.bracketOnError (safeAccept sock) (close . fst) $ \(conn, _peer) -> do cId <- atomically $ stateTVar accepted $ \cId -> let cId' = cId + 1 in cId `seq` (cId', cId') let closeConn _ = do - atomically $ modifyTVar' clients $ IM.delete cId + atomically $ modifyTVar' clients $ IM.delete cId gracefulClose conn 5000 `catchAll_` pure () -- catchAll_ is needed here in case the connection was closed earlier - atomically $ modifyTVar' gracefullyClosed (+1) + atomically $ modifyTVar' gracefullyClosed (+ 1) tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn atomically $ modifyTVar' clients $ IM.insert cId tId +-- | Recover from errors in `accept` whenever it is safe. +-- Some errors are safe to ignore, while blindly restaring `accept` may trigger a busy loop. +-- +-- man accept says: +-- @ +-- For reliable operation the application should detect the network errors defined for the protocol after accept() and treat them like EAGAIN by retrying. +-- In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. +-- @ +safeAccept :: Socket -> IO (Socket, SockAddr) +safeAccept sock = + tryIOError (accept sock) >>= \case + Right r -> pure r + Left e + | retryAccept -> logWarn err >> safeAccept sock + | otherwise -> logError err >> E.throwIO e + where + retryAccept = maybe False ((`elem` again) . Errno) errno + again = [eAGAIN, eNETDOWN, ePROTO, eNOPROTOOPT, eHOSTDOWN, eNONET, eHOSTUNREACH, eOPNOTSUPP, eNETUNREACH] + err = "socket accept error: " <> tshow e <> maybe "" ((", errno=" <>) . tshow) errno + errno = ioe_errno e + type SocketState = (TVar Int, TVar Int, TVar (IntMap (Weak ThreadId))) newSocketState :: STM SocketState @@ -150,12 +176,13 @@ startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted pure sock setStarted sock = atomically (tryPutTMVar started True) >> pure sock -loadTLSServerParams :: FilePath -> FilePath -> FilePath -> IO T.ServerParams +loadTLSServerParams :: FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams loadTLSServerParams = loadSupportedTLSServerParams supportedParameters -loadSupportedTLSServerParams :: T.Supported -> FilePath -> FilePath -> FilePath -> IO T.ServerParams -loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile = - fromCredential <$> loadServerCredential +loadSupportedTLSServerParams :: T.Supported -> FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams +loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile alpn_ = do + tlsServerParams <- fromCredential <$> loadServerCredential + pure tlsServerParams {T.serverHooks = maybe def alpnHooks alpn_} where loadServerCredential :: IO T.Credential loadServerCredential = @@ -170,6 +197,7 @@ loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile p T.serverHooks = def, T.serverSupported = serverSupported } + alpnHooks supported = def {T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported)} loadFingerprint :: FilePath -> IO Fingerprint loadFingerprint certificateFile = do diff --git a/src/Simplex/Messaging/Transport/WebSockets.hs b/src/Simplex/Messaging/Transport/WebSockets.hs index 062f4f0f0..0883fcc28 100644 --- a/src/Simplex/Messaging/Transport/WebSockets.hs +++ b/src/Simplex/Messaging/Transport/WebSockets.hs @@ -14,7 +14,8 @@ import Network.WebSockets import Network.WebSockets.Stream (Stream) import qualified Network.WebSockets.Stream as S import Simplex.Messaging.Transport - ( TProxy, + ( ALPN, + TProxy, Transport (..), TransportConfig (..), TransportError (..), @@ -28,6 +29,7 @@ import Simplex.Messaging.Transport.Buffer (trimCR) data WS = WS { wsPeer :: TransportPeer, tlsUniq :: ByteString, + wsALPN :: Maybe ALPN, wsStream :: Stream, wsConnection :: Connection, wsTransportConfig :: TransportConfig, @@ -61,6 +63,9 @@ instance Transport WS where getServerCerts :: WS -> X.CertificateChain getServerCerts = wsServerCerts + getSessionALPN :: WS -> Maybe ALPN + getSessionALPN = wsALPN + tlsUnique :: WS -> ByteString tlsUnique = tlsUniq @@ -90,7 +95,8 @@ getWS wsPeer cfg wsServerCerts cxt = withTlsUnique wsPeer cxt connectWS connectWS tlsUniq = do s <- makeTLSContextStream cxt wsConnection <- connectPeer wsPeer s - pure $ WS {wsPeer, tlsUniq, wsStream = s, wsConnection, wsTransportConfig = cfg, wsServerCerts} + wsALPN <- T.getNegotiatedProtocol cxt + pure $ WS {wsPeer, tlsUniq, wsALPN, wsStream = s, wsConnection, wsTransportConfig = cfg, wsServerCerts} connectPeer :: TransportPeer -> Stream -> IO Connection connectPeer TServer = acceptClientRequest connectPeer TClient = sendClientRequest diff --git a/src/Simplex/Messaging/Util.hs b/src/Simplex/Messaging/Util.hs index a880cfaad..ef2cc6933 100644 --- a/src/Simplex/Messaging/Util.hs +++ b/src/Simplex/Messaging/Util.hs @@ -97,15 +97,15 @@ catchAll_ :: IO a -> IO a -> IO a catchAll_ a = catchAll a . const {-# INLINE catchAll_ #-} -tryAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> m (Either e a) -tryAllErrors err action = tryError action `UE.catch` (pure . Left . err) +tryAllErrors :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> ExceptT e m (Either e a) +tryAllErrors err action = ExceptT $ Right <$> runExceptT action `UE.catch` (pure . Left . err) {-# INLINE tryAllErrors #-} tryAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> m (Either e a) tryAllErrors' err action = runExceptT action `UE.catch` (pure . Left . err) {-# INLINE tryAllErrors' #-} -catchAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> (e -> m a) -> m a +catchAllErrors :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> (e -> ExceptT e m a) -> ExceptT e m a catchAllErrors err action handler = tryAllErrors err action >>= either handler pure {-# INLINE catchAllErrors #-} @@ -113,11 +113,11 @@ catchAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> catchAllErrors' err action handler = tryAllErrors' err action >>= either handler pure {-# INLINE catchAllErrors' #-} -catchThrow :: (MonadUnliftIO m, MonadError e m) => m a -> (E.SomeException -> e) -> m a +catchThrow :: MonadUnliftIO m => ExceptT e m a -> (E.SomeException -> e) -> ExceptT e m a catchThrow action err = catchAllErrors err action throwError {-# INLINE catchThrow #-} -allFinally :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> m b -> m a +allFinally :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> ExceptT e m b -> ExceptT e m a allFinally err action final = tryAllErrors err action >>= \r -> final >> either throwError pure r {-# INLINE allFinally #-} @@ -152,12 +152,14 @@ timeoutThrow :: MonadUnliftIO m => e -> Int -> ExceptT e m a -> ExceptT e m a timeoutThrow e ms action = ExceptT (sequence <$> (ms `timeout` runExceptT action)) >>= maybe (throwError e) pure threadDelay' :: Int64 -> IO () -threadDelay' time - | time <= 0 = pure () -threadDelay' time = do - let maxWait = min time $ fromIntegral (maxBound :: Int) - threadDelay $ fromIntegral maxWait - when (maxWait /= time) $ threadDelay' (time - maxWait) +threadDelay' = loop + where + loop time + | time <= 0 = pure () + | otherwise = do + let maxWait = min time $ fromIntegral (maxBound :: Int) + threadDelay $ fromIntegral maxWait + loop $ time - maxWait diffToMicroseconds :: NominalDiffTime -> Int64 diffToMicroseconds diff = fromIntegral ((truncate $ diff * 1000000) :: Integer) diff --git a/src/Simplex/Messaging/Version.hs b/src/Simplex/Messaging/Version.hs index 25f7368d1..5576cfa9f 100644 --- a/src/Simplex/Messaging/Version.hs +++ b/src/Simplex/Messaging/Version.hs @@ -23,6 +23,8 @@ module Simplex.Messaging.Version isCompatibleRange, proveCompatible, compatibleVersion, + compatibleVRange, + compatibleVRange', ) where @@ -98,6 +100,7 @@ class VersionScope v => VersionI v a | a -> v where class VersionScope v => VersionRangeI v a | a -> v where type VersionT v a versionRange :: a -> VersionRange v + toVersionRange :: a -> VersionRange v -> a toVersionT :: a -> Version v -> VersionT v a instance VersionScope v => VersionI v (Version v) where @@ -108,6 +111,7 @@ instance VersionScope v => VersionI v (Version v) where instance VersionScope v => VersionRangeI v (VersionRange v) where type VersionT v (VersionRange v) = Version v versionRange = id + toVersionRange _ vr = vr toVersionT _ v = v newtype Compatible a = Compatible_ a @@ -135,5 +139,24 @@ compatibleVersion x vr = max1 = maxVersion $ versionRange x max2 = maxVersion vr +-- | intersection of version ranges +compatibleVRange :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible a) +compatibleVRange x vr = + compatibleVRange_ x (max min1 min2) (min max1 max2) + where + VRange min1 max1 = versionRange x + VRange min2 max2 = vr + +-- | version range capped by compatible version +compatibleVRange' :: VersionRangeI v a => a -> Version v -> Maybe (Compatible a) +compatibleVRange' x v + | v <= max1 = compatibleVRange_ x min1 v + | otherwise = Nothing + where + VRange min1 max1 = versionRange x + +compatibleVRange_ :: VersionRangeI v a => a -> Version v -> Version v -> Maybe (Compatible a) +compatibleVRange_ x v1 v2 = Compatible_ . toVersionRange x <$> safeVersionRange v1 v2 + mkCompatibleIf :: a -> Bool -> Maybe (Compatible a) x `mkCompatibleIf` cond = if cond then Just $ Compatible_ x else Nothing diff --git a/src/Simplex/RemoteControl/Invitation.hs b/src/Simplex/RemoteControl/Invitation.hs index 712c41a9d..d606c4ff0 100644 --- a/src/Simplex/RemoteControl/Invitation.hs +++ b/src/Simplex/RemoteControl/Invitation.hs @@ -123,7 +123,7 @@ instance StrEncoding RCSignedInvitation where idsig <- requiredP sigs "idsig" $ parseAll strP pure RCSignedInvitation {invitation, ssig, idsig} -signInvitation :: C.PrivateKey C.Ed25519 -> C.PrivateKey C.Ed25519 -> RCInvitation -> RCSignedInvitation +signInvitation :: C.PrivateKey 'C.Ed25519 -> C.PrivateKey 'C.Ed25519 -> RCInvitation -> RCSignedInvitation signInvitation sKey idKey invitation = RCSignedInvitation {invitation, ssig, idsig} where uri = strEncode invitation diff --git a/tests/AgentTests.hs b/tests/AgentTests.hs index 8083ef988..b200c3933 100644 --- a/tests/AgentTests.hs +++ b/tests/AgentTests.hs @@ -7,13 +7,12 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests (agentTests) where import AgentTests.ConnectionRequestTests import AgentTests.DoubleRatchetTests (doubleRatchetTests) -import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg') +import AgentTests.FunctionalAPITests (functionalAPITests, inAnyOrder, pattern Msg, pattern Msg', pattern SENT) import AgentTests.MigrationTests (migrationTests) import AgentTests.NotificationTests (notificationTests) import AgentTests.SQLiteTests (storeTests) @@ -27,9 +26,9 @@ import GHC.Stack (withFrozenCallStack) import Network.HTTP.Types (urlEncode) import SMPAgentClient import SMPClient (testKeyHash, testPort, testPort2, testStoreLogFile, withSmpServer, withSmpServerStoreLogOn) -import Simplex.Messaging.Agent.Protocol hiding (MID, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (CONF, INFO, MID, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A -import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOn, pattern IKPQOff, pattern PQEncOn, pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ErrorType (..)) @@ -95,23 +94,24 @@ type AEntityTransmission p e = (ACorrId, ConnId, ACommand p e) type AEntityTransmissionOrError p e = (ACorrId, ConnId, Either AgentErrorType (ACommand p e)) tGetAgent :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AEConn) -tGetAgent = tGetAgent' +tGetAgent = tGetAgent' True -tGetAgent' :: forall c e. (Transport c, AEntityI e) => c -> IO (AEntityTransmissionOrError 'Agent e) -tGetAgent' h = do - (corrId, connId, cmdOrErr) <- pGetAgent h +tGetAgent' :: forall c e. (Transport c, AEntityI e) => Bool -> c -> IO (AEntityTransmissionOrError 'Agent e) +tGetAgent' skipErr h = do + (corrId, connId, cmdOrErr) <- pGetAgent skipErr h case cmdOrErr of Right (APC e cmd) -> case testEquality e (sAEntity @e) of Just Refl -> pure (corrId, connId, Right cmd) _ -> error $ "unexpected command " <> show cmd Left err -> pure (corrId, connId, Left err) -pGetAgent :: forall c. Transport c => c -> IO (ATransmissionOrError 'Agent) -pGetAgent h = do +pGetAgent :: forall c. Transport c => Bool -> c -> IO (ATransmissionOrError 'Agent) +pGetAgent skipErr h = do (corrId, connId, cmdOrErr) <- tGet SAgent h case cmdOrErr of - Right (APC _ CONNECT {}) -> pGetAgent h - Right (APC _ DISCONNECT {}) -> pGetAgent h + Right (APC _ CONNECT {}) -> pGetAgent skipErr h + Right (APC _ DISCONNECT {}) -> pGetAgent skipErr h + Right (APC _ (ERR (BROKER _ NETWORK))) | skipErr -> pGetAgent skipErr h cmd -> pure (corrId, connId, cmd) -- | receive message to handle `h` @@ -119,15 +119,18 @@ pGetAgent h = do (<#:) = tGetAgent (<#:?) :: Transport c => c -> IO (ATransmissionOrError 'Agent) -(<#:?) = pGetAgent +(<#:?) = pGetAgent True (<#:.) :: Transport c => c -> IO (AEntityTransmissionOrError 'Agent 'AENone) -(<#:.) = tGetAgent' +(<#:.) = tGetAgent' True -- | send transmission `t` to handle `h` and get response (#:) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (AEntityTransmissionOrError 'Agent 'AEConn) h #: t = tPutRaw h t >> (<#:) h +(#:!) :: Transport c => c -> (ByteString, ByteString, ByteString) -> IO (AEntityTransmissionOrError 'Agent 'AEConn) +h #:! t = tPutRaw h t >> tGetAgent' False h + -- | action and expected response -- `h #:t #> r` is the test that sends `t` to `h` and validates that the response is `r` (#>) :: IO (AEntityTransmissionOrError 'Agent 'AEConn) -> AEntityTransmission 'Agent 'AEConn -> Expectation @@ -214,14 +217,13 @@ testDuplexConnection _ alice bob = testDuplexConnection' (alice, IKPQOn) (bob, P testDuplexConnection' :: (HasCallStack, Transport c) => (c, InitialKeys) -> (c, PQSupport) -> IO () testDuplexConnection' (alice, aPQ) (bob, bPQ) = do let pq = pqConnectionMode aPQ bPQ - pqSup = CR.pqEncToSupport pq ("1", "bob", Right (INV cReq)) <- alice #: ("1", "bob", "NEW T INV" <> pqConnModeStr aPQ <> " subscribe") let cReq' = strEncode cReq bob #: ("11", "alice", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") #> ("11", "alice", OK) ("", "bob", Right (A.CONF confId pqSup' _ "bob's connInfo")) <- (alice <#:) - pqSup' `shouldBe` pqSup + pqSup' `shouldBe` CR.connPQEncryption aPQ alice #: ("2", "bob", "LET " <> confId <> " 16\nalice's connInfo") #> ("2", "bob", OK) - bob <# ("", "alice", A.INFO pqSup "alice's connInfo") + bob <# ("", "alice", A.INFO bPQ "alice's connInfo") bob <# ("", "alice", CON pq) alice <# ("", "bob", CON pq) -- message IDs 1 to 3 get assigned to control messages, so first MSG is assigned ID 4 @@ -243,7 +245,7 @@ testDuplexConnection' (alice, aPQ) (bob, bPQ) = do alice #: ("4a", "bob", "ACK 7") #> ("4a", "bob", OK) alice #: ("5", "bob", "OFF") #> ("5", "bob", OK) bob #: ("17", "alice", "SEND F 9\nmessage 3") #> ("17", "alice", A.MID 8 pq) - bob <# ("", "alice", MERR 8 (SMP AUTH)) + bob <#= \case ("", "alice", MERR 8 (SMP _ AUTH)) -> True; _ -> False alice #: ("6", "bob", "DEL") #> ("6", "bob", OK) alice #:# "nothing else should be delivered to alice" @@ -253,15 +255,14 @@ testDuplexConnRandomIds _ alice bob = testDuplexConnRandomIds' (alice, IKPQOn) ( testDuplexConnRandomIds' :: (HasCallStack, Transport c) => (c, InitialKeys) -> (c, PQSupport) -> IO () testDuplexConnRandomIds' (alice, aPQ) (bob, bPQ) = do let pq = pqConnectionMode aPQ bPQ - pqSup = CR.pqEncToSupport pq ("1", bobConn, Right (INV cReq)) <- alice #: ("1", "", "NEW T INV" <> pqConnModeStr aPQ <> " subscribe") let cReq' = strEncode cReq ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") ("", bobConn', Right (A.CONF confId pqSup' _ "bob's connInfo")) <- (alice <#:) - pqSup' `shouldBe` pqSup + pqSup' `shouldBe` CR.connPQEncryption aPQ bobConn' `shouldBe` bobConn alice #: ("2", bobConn, "LET " <> confId <> " 16\nalice's connInfo") =#> \case ("2", c, OK) -> c == bobConn; _ -> False - bob <# ("", aliceConn, A.INFO pqSup "alice's connInfo") + bob <# ("", aliceConn, A.INFO bPQ "alice's connInfo") bob <# ("", aliceConn, CON pq) alice <# ("", bobConn, CON pq) alice #: ("2", bobConn, "SEND F :hello") #> ("2", bobConn, A.MID 4 pq) @@ -282,7 +283,7 @@ testDuplexConnRandomIds' (alice, aPQ) (bob, bPQ) = do alice #: ("4a", bobConn, "ACK 7") #> ("4a", bobConn, OK) alice #: ("5", bobConn, "OFF") #> ("5", bobConn, OK) bob #: ("17", aliceConn, "SEND F 9\nmessage 3") #> ("17", aliceConn, A.MID 8 pq) - bob <# ("", aliceConn, MERR 8 (SMP AUTH)) + bob <#= \case ("", cId, MERR 8 (SMP _ AUTH)) -> cId == aliceConn; _ -> False alice #: ("6", bobConn, "DEL") #> ("6", bobConn, OK) alice #:# "nothing else should be delivered to alice" @@ -291,17 +292,15 @@ testContactConnection (alice, aPQ) (bob, bPQ) (tom, tPQ) = do ("1", "alice_contact", Right (INV cReq)) <- alice #: ("1", "alice_contact", "NEW T CON" <> pqConnModeStr aPQ <> " subscribe") let cReq' = strEncode cReq abPQ = pqConnectionMode aPQ bPQ - abPQSup = CR.pqEncToSupport abPQ aPQMode = CR.connPQEncryption aPQ bob #: ("11", "alice", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") #> ("11", "alice", OK) - ("", "alice_contact", Right (A.REQ aInvId pqSup' _ "bob's connInfo")) <- (alice <#:) - pqSup' `shouldBe` bPQ + ("", "alice_contact", Right (A.REQ aInvId PQSupportOn _ "bob's connInfo")) <- (alice <#:) alice #: ("2", "bob", "ACPT " <> aInvId <> enableKEMStr aPQMode <> " 16\nalice's connInfo") #> ("2", "bob", OK) ("", "alice", Right (A.CONF bConfId pqSup'' _ "alice's connInfo")) <- (bob <#:) - pqSup'' `shouldBe` abPQSup + pqSup'' `shouldBe` bPQ bob #: ("12", "alice", "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", "alice", OK) - alice <# ("", "bob", A.INFO abPQSup "bob's connInfo 2") + alice <# ("", "bob", A.INFO (CR.connPQEncryption aPQ) "bob's connInfo 2") alice <# ("", "bob", CON abPQ) bob <# ("", "alice", CON abPQ) alice #: ("3", "bob", "SEND F :hi") #> ("3", "bob", A.MID 4 abPQ) @@ -310,15 +309,13 @@ testContactConnection (alice, aPQ) (bob, bPQ) (tom, tPQ) = do bob #: ("13", "alice", "ACK 4") #> ("13", "alice", OK) let atPQ = pqConnectionMode aPQ tPQ - atPQSup = CR.pqEncToSupport atPQ tom #: ("21", "alice", "JOIN T " <> cReq' <> enableKEMStr tPQ <> " subscribe 14\ntom's connInfo") #> ("21", "alice", OK) - ("", "alice_contact", Right (A.REQ aInvId' pqSup3 _ "tom's connInfo")) <- (alice <#:) - pqSup3 `shouldBe` tPQ + ("", "alice_contact", Right (A.REQ aInvId' PQSupportOn _ "tom's connInfo")) <- (alice <#:) alice #: ("4", "tom", "ACPT " <> aInvId' <> enableKEMStr aPQMode <> " 16\nalice's connInfo") #> ("4", "tom", OK) ("", "alice", Right (A.CONF tConfId pqSup4 _ "alice's connInfo")) <- (tom <#:) - pqSup4 `shouldBe` atPQSup + pqSup4 `shouldBe` tPQ tom #: ("22", "alice", "LET " <> tConfId <> " 16\ntom's connInfo 2") #> ("22", "alice", OK) - alice <# ("", "tom", A.INFO atPQSup "tom's connInfo 2") + alice <# ("", "tom", A.INFO (CR.connPQEncryption aPQ) "tom's connInfo 2") alice <# ("", "tom", CON atPQ) tom <# ("", "alice", CON atPQ) alice #: ("5", "tom", "SEND F :hi there") #> ("5", "tom", A.MID 4 atPQ) @@ -329,22 +326,20 @@ testContactConnection (alice, aPQ) (bob, bPQ) (tom, tPQ) = do testContactConnRandomIds :: Transport c => (c, InitialKeys) -> (c, PQSupport) -> IO () testContactConnRandomIds (alice, aPQ) (bob, bPQ) = do let pq = pqConnectionMode aPQ bPQ - pqSup = CR.pqEncToSupport pq ("1", aliceContact, Right (INV cReq)) <- alice #: ("1", "", "NEW T CON" <> pqConnModeStr aPQ <> " subscribe") let cReq' = strEncode cReq ("11", aliceConn, Right OK) <- bob #: ("11", "", "JOIN T " <> cReq' <> enableKEMStr bPQ <> " subscribe 14\nbob's connInfo") - ("", aliceContact', Right (A.REQ aInvId pqSup' _ "bob's connInfo")) <- (alice <#:) - pqSup' `shouldBe` bPQ + ("", aliceContact', Right (A.REQ aInvId PQSupportOn _ "bob's connInfo")) <- (alice <#:) aliceContact' `shouldBe` aliceContact ("2", bobConn, Right OK) <- alice #: ("2", "", "ACPT " <> aInvId <> enableKEMStr (CR.connPQEncryption aPQ) <> " 16\nalice's connInfo") ("", aliceConn', Right (A.CONF bConfId pqSup'' _ "alice's connInfo")) <- (bob <#:) - pqSup'' `shouldBe` pqSup + pqSup'' `shouldBe` bPQ aliceConn' `shouldBe` aliceConn bob #: ("12", aliceConn, "LET " <> bConfId <> " 16\nbob's connInfo 2") #> ("12", aliceConn, OK) - alice <# ("", bobConn, A.INFO pqSup "bob's connInfo 2") + alice <# ("", bobConn, A.INFO (CR.connPQEncryption aPQ) "bob's connInfo 2") alice <# ("", bobConn, CON pq) bob <# ("", aliceConn, CON pq) @@ -358,11 +353,11 @@ testRejectContactRequest _ alice bob = do ("1", "a_contact", Right (INV cReq)) <- alice #: ("1", "a_contact", "NEW T CON subscribe") let cReq' = strEncode cReq bob #: ("11", "alice", "JOIN T " <> cReq' <> " subscribe 10\nbob's info") #> ("11", "alice", OK) - ("", "a_contact", Right (A.REQ aInvId PQSupportOff _ "bob's info")) <- (alice <#:) + ("", "a_contact", Right (A.REQ aInvId PQSupportOn _ "bob's info")) <- (alice <#:) -- RJCT must use correct contact connection alice #: ("2a", "bob", "RJCT " <> aInvId) #> ("2a", "bob", ERR $ CONN NOT_FOUND) alice #: ("2b", "a_contact", "RJCT " <> aInvId) #> ("2b", "a_contact", OK) - alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") #> ("3", "bob", ERR $ A.CMD PROHIBITED) + alice #: ("3", "bob", "ACPT " <> aInvId <> " 12\nalice's info") =#> \case ("3", "bob", ERR (A.CMD PROHIBITED _)) -> True; _ -> False bob #:# "nothing should be delivered to bob" testSubscription :: Transport c => TProxy c -> c -> c -> c -> IO () @@ -391,7 +386,7 @@ testSubscrNotification t (server, _) client = do killThread server client <#. ("", "", DOWN testSMPServer ["conn1"]) withSmpServer (ATransport t) $ - client <# ("", "conn1", ERR (SMP AUTH)) -- this new server does not have the queue + client <#= \case ("", "conn1", ERR (SMP _ AUTH)) -> True; _ -> False -- this new server does not have the queue testMsgDeliveryServerRestart :: forall c. Transport c => (c, InitialKeys) -> (c, PQSupport) -> IO () testMsgDeliveryServerRestart (alice, aPQ) (bob, bPQ) = do @@ -434,11 +429,11 @@ testServerConnectionAfterError t _ = do withAgent1 $ \bob -> do withAgent2 $ \alice -> do - bob #: ("1", "alice", "SUB") =#> \("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT - alice #: ("1", "bob", "SUB") =#> \("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT + bob #:! ("1", "alice", "SUB") =#> \case ("1", "alice", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT; _ -> False + alice #:! ("1", "bob", "SUB") =#> \case ("1", "bob", ERR (BROKER _ e)) -> e == NETWORK || e == TIMEOUT; _ -> False withServer $ do - alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False - alice <#=? \case ("", "bob", APC _ (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False + alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False + alice <#=? \case ("", "bob", APC SAEConn (SENT 4)) -> True; ("", "", APC _ (UP s ["bob"])) -> s == server; _ -> False bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False bob <#=? \case ("", "alice", APC _ (Msg "hello")) -> True; ("", "", APC _ (UP s ["alice"])) -> s == server; _ -> False bob #: ("2", "alice", "ACK 4") #> ("2", "alice", OK) @@ -528,6 +523,7 @@ testMsgDeliveryQuotaExceeded _ alice bob = do (_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND F :" <> msg) alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False (_, "bob", Right (MID _)) <- alice #: ("5", "bob", "SEND F :over quota") + alice <#= \case ("", "bob", MWARN _ (SMP _ QUOTA)) -> True; _ -> False alice #: ("1", "bob2", "SEND F :hello") #> ("1", "bob2", MID 4) -- if delivery is blocked it won't go further @@ -542,6 +538,7 @@ testResumeDeliveryQuotaExceeded _ alice bob = do (_, "bob", Right (MID mId)) <- alice #: (corrId, "bob", "SEND F :" <> msg) alice <#= \case ("", "bob", SENT m) -> m == mId; _ -> False ("5", "bob", Right (MID 8)) <- alice #: ("5", "bob", "SEND F :over quota") + alice <#= \case ("", "bob", MWARN 8 (SMP _ QUOTA)) -> True; _ -> False alice #:# "the last message not sent yet" bob <#= \case ("", "alice", Msg "message 1") -> True; _ -> False bob #: ("1", "alice", "ACK 4") #> ("1", "alice", OK) @@ -555,10 +552,10 @@ testResumeDeliveryQuotaExceeded _ alice bob = do bob <#= \case ("", "alice", Msg "message 4") -> True; _ -> False bob #: ("4", "alice", "ACK 7") #> ("4", "alice", OK) inAnyOrder - (tGetAgent alice) - [ \case ("", c, Right (SENT 8)) -> c == "bob"; _ -> False, - \case ("", c, Right QCONT) -> c == "bob"; _ -> False - ] + (tGetAgent alice) + [ \case ("", c, Right (SENT 8)) -> c == "bob"; _ -> False, + \case ("", c, Right QCONT) -> c == "bob"; _ -> False + ] bob <#= \case ("", "alice", Msg "over quota") -> True; _ -> False -- message 8 is skipped because of alice agent sending "QCONT" message bob #: ("5", "alice", "ACK 9") #> ("5", "alice", OK) @@ -571,12 +568,11 @@ connect' (h1, name1, pqMode1) (h2, name2, pqMode2) = do ("c1", _, Right (INV cReq)) <- h1 #: ("c1", name2, "NEW T INV" <> pqConnModeStr pqMode1 <> " subscribe") let cReq' = strEncode cReq pq = pqConnectionMode pqMode1 pqMode2 - pqSup = CR.pqEncToSupport pq h2 #: ("c2", name1, "JOIN T " <> cReq' <> enableKEMStr pqMode2 <> " subscribe 5\ninfo2") #> ("c2", name1, OK) ("", _, Right (A.CONF connId pqSup' _ "info2")) <- (h1 <#:) - pqSup' `shouldBe` pqSup + pqSup' `shouldBe` CR.connPQEncryption pqMode1 h1 #: ("c3", name2, "LET " <> connId <> " 5\ninfo1") #> ("c3", name2, OK) - h2 <# ("", name1, A.INFO pqSup "info1") + h2 <# ("", name1, A.INFO pqMode2 "info1") h2 <# ("", name1, CON pq) h1 <# ("", name2, CON pq) @@ -589,7 +585,7 @@ enableKEMStr _ = "" pqConnModeStr :: InitialKeys -> ByteString pqConnModeStr (IKNoPQ PQSupportOff) = "" -pqConnModeStr pq = " " <> strEncode pq +pqConnModeStr pq = " " <> strEncode pq sendMessage :: Transport c => (c, ConnId) -> (c, ConnId) -> ByteString -> IO () sendMessage (h1, name1) (h2, name2) msg = do @@ -618,12 +614,12 @@ sampleDhKey = "MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" syntaxTests :: forall c. Transport c => TProxy c -> Spec syntaxTests t = do - it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX") + it "unknown command" $ ("1", "5678", "HELLO") >#> ("1", "5678", "ERR CMD SYNTAX parseCommand") describe "NEW" $ do describe "valid" $ do it "with correct parameter" $ ("211", "", "NEW T INV subscribe") >#>= \case ("211", _, "INV" : _) -> True; _ -> False describe "invalid" $ do - it "with incorrect parameter" $ ("222", "", "NEW T hi subscribe") >#> ("222", "", "ERR CMD SYNTAX") + it "with incorrect parameter" $ ("222", "", "NEW T hi subscribe") >#> ("222", "", "ERR CMD SYNTAX parseCommand") describe "JOIN" $ do describe "valid" $ do @@ -639,9 +635,9 @@ syntaxTests t = do <> " subscribe " <> "14\nbob's connInfo" ) - >#> ("311", "a", "ERR SMP AUTH") + >#> ("311", "a", "ERR SMP smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001 AUTH") describe "invalid" $ do - it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX") + it "no parameters" $ ("321", "", "JOIN") >#> ("321", "", "ERR CMD SYNTAX parseCommand") where -- simple test for one command with the expected response (>#>) :: ARawTransmission -> ARawTransmission -> Expectation diff --git a/tests/AgentTests/ConnectionRequestTests.hs b/tests/AgentTests/ConnectionRequestTests.hs index 7ab234887..20480f84c 100644 --- a/tests/AgentTests/ConnectionRequestTests.hs +++ b/tests/AgentTests/ConnectionRequestTests.hs @@ -68,7 +68,7 @@ testE2ERatchetParams :: RcvE2ERatchetParamsUri 'C.X448 testE2ERatchetParams = E2ERatchetParamsUri (mkVersionRange (VersionE2E 1) (VersionE2E 1)) testDhPubKey testDhPubKey Nothing testE2ERatchetParams12 :: RcvE2ERatchetParamsUri 'C.X448 -testE2ERatchetParams12 = E2ERatchetParamsUri (supportedE2EEncryptVRange PQSupportOn) testDhPubKey testDhPubKey Nothing +testE2ERatchetParams12 = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey Nothing connectionRequest :: AConnectionRequestUri connectionRequest = @@ -82,7 +82,7 @@ connectionRequestCurrentRange :: AConnectionRequestUri connectionRequestCurrentRange = ACR SCMInvitation $ CRInvitationUri - connReqData {crAgentVRange = supportedSMPAgentVRange PQSupportOn, crSmpQueues = [queueV1, queueV1]} + connReqData {crAgentVRange = supportedSMPAgentVRange, crSmpQueues = [queueV1, queueV1]} testE2ERatchetParams12 connectionRequestClientDataEmpty :: AConnectionRequestUri diff --git a/tests/AgentTests/DoubleRatchetTests.hs b/tests/AgentTests/DoubleRatchetTests.hs index f95f07029..c3fbf01e8 100644 --- a/tests/AgentTests/DoubleRatchetTests.hs +++ b/tests/AgentTests/DoubleRatchetTests.hs @@ -93,9 +93,9 @@ fullMsgLen :: Ratchet a -> Int fullMsgLen Ratchet {rcSupportKEM, rcVersion} = headerLenLength + fullHeaderLen v rcSupportKEM + C.authTagSize + paddedMsgLen where v = current rcVersion - headerLenLength = case rcSupportKEM of - PQSupportOn | v >= pqRatchetE2EEncryptVersion -> 3 -- two bytes are added because of two Large used in new encoding - _ -> 1 + headerLenLength + | v >= pqRatchetE2EEncryptVersion = 3 -- two bytes are added because of two Large used in new encoding + | otherwise = 1 testMessageHeader :: forall a. AlgorithmI a => VersionE2E -> C.SAlgorithm a -> Expectation testMessageHeader v _ = do @@ -520,7 +520,7 @@ initRatchets = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let vs = testRatchetVersions PQSupportOff + let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob alice = initRcvRatchet vs pkAlice2 paramsAlice PQSupportOff pure (alice, bob, encrypt' noSndKEM, decrypt' noRcvKEM, (\#>)) @@ -537,7 +537,7 @@ initRatchetsKEMProposed = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let vs = testRatchetVersions PQSupportOn + let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob alice = initRcvRatchet vs pkAlice2 paramsAlice PQSupportOn pure (alice, bob, encrypt' hasSndKEM, decrypt' hasRcvKEM, (!#>)) @@ -555,7 +555,7 @@ initRatchetsKEMAccepted = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKem_ e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let vs = testRatchetVersions PQSupportOn + let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob alice = initRcvRatchet vs pkAlice2 paramsAlice PQSupportOn pure (alice, bob, encrypt' hasSndKEM, decrypt' hasRcvKEM, (!#>)) @@ -572,14 +572,14 @@ initRatchetsKEMProposedAgain = do Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKem_ e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g - let vs = testRatchetVersions PQSupportOn + let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob alice = initRcvRatchet vs pkAlice2 paramsAlice PQSupportOn pure (alice, bob, encrypt' hasSndKEM, decrypt' hasRcvKEM, (!#>)) -testRatchetVersions :: PQSupport -> RatchetVersions -testRatchetVersions pq = - let v = maxVersion $ supportedE2EEncryptVRange pq +testRatchetVersions :: RatchetVersions +testRatchetVersions = + let v = maxVersion supportedE2EEncryptVRange in RatchetVersions v v encrypt_ :: AlgorithmI a => Maybe PQEncryption -> (TVar ChaChaDRG, Ratchet a, SkippedMsgKeys) -> ByteString -> IO (Either CryptoError (ByteString, Ratchet a, SkippedMsgDiff)) diff --git a/tests/AgentTests/EqInstances.hs b/tests/AgentTests/EqInstances.hs index aaaa2de51..a810247fe 100644 --- a/tests/AgentTests/EqInstances.hs +++ b/tests/AgentTests/EqInstances.hs @@ -6,6 +6,7 @@ module AgentTests.EqInstances where import Data.Type.Equality import Simplex.Messaging.Agent.Store +import Simplex.Messaging.Client (ProxiedRelay (..)) instance Eq SomeConn where SomeConn d c == SomeConn d' c' = case testEquality d d' of @@ -23,3 +24,7 @@ deriving instance Eq (StoredSndQueue q) deriving instance Eq (DBQueueId q) deriving instance Eq ClientNtfCreds + +deriving instance Show ProxiedRelay + +deriving instance Eq ProxiedRelay diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 7d6884b27..c2badea63 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -1,7 +1,9 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE InstanceSigs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} @@ -12,7 +14,6 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module AgentTests.FunctionalAPITests ( functionalAPITests, @@ -45,42 +46,45 @@ module AgentTests.FunctionalAPITests pattern REQ, pattern Msg, pattern Msg', + pattern SENT, agentCfgV7, ) where import AgentTests.ConnectionRequestTests (connReqData, queueAddr, testE2ERatchetParams12) -import Control.Concurrent (killThread, threadDelay) +import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Monad import Control.Monad.Except import Control.Monad.Reader +import Data.Bifunctor (first) +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (isRight) import Data.Int (Int64) -import Data.List (nub) +import Data.List (find, nub) import Data.List.NonEmpty (NonEmpty) import qualified Data.Map as M -import Data.Maybe (isNothing) +import Data.Maybe (isJust, isNothing) import qualified Data.Set as S +import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.Clock.System (SystemTime (..), getSystemTime) -import Data.Type.Equality +import Data.Type.Equality (testEquality, (:~:) (Refl)) import Data.Word (Word16) import qualified Database.SQLite.Simple as SQL import GHC.Stack (withFrozenCallStack) import SMPAgentClient -import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) +import SMPClient (cfg, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerOn, withSmpServerProxy, withSmpServerStoreLogOn, withSmpServerStoreMsgLogOn, withSmpServerV7) import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import qualified Simplex.Messaging.Agent as A import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), UserNetworkInfo (..), UserNetworkType (..), waitForUserNetwork) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore) -import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ, SENT) import qualified Simplex.Messaging.Agent.Protocol as A -import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), TransportSessionMode (TSMEntity, TSMUser), defaultSMPClientConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SMPProxyFallback (..), SMPProxyMode (..), TransportSessionMode (TSMEntity, TSMUser), defaultClientConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -90,7 +94,8 @@ import Simplex.Messaging.Protocol (BasicAuth, ErrorType (..), MsgBody, ProtocolS import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) import Simplex.Messaging.Server.Expiration -import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion) +import Simplex.Messaging.Server.QueueStore.QueueInfo +import Simplex.Messaging.Transport (ATransport (..), SMPVersion, VersionSMP, authCmdsSMPVersion, basicAuthSMPVersion, batchCmdsSMPVersion, currentServerSMPRelayVersion, supportedSMPHandshakes) import Simplex.Messaging.Util (diffToMicroseconds) import Simplex.Messaging.Version (VersionRange (..)) import qualified Simplex.Messaging.Version as V @@ -105,8 +110,11 @@ type AEntityTransmission e = (ACorrId, ConnId, ACommand 'Agent e) -- deriving instance Eq (ValidFileDescription p) +shouldRespond :: (HasCallStack, MonadUnliftIO m, Eq a, Show a) => m a -> a -> m () +a `shouldRespond` r = withFrozenCallStack $ withTimeout a (`shouldBe` r) + (##>) :: (HasCallStack, MonadUnliftIO m) => m (AEntityTransmission e) -> AEntityTransmission e -> m () -a ##> t = withTimeout a (`shouldBe` t) +a ##> t = a `shouldRespond` t (=##>) :: (Show a, HasCallStack, MonadUnliftIO m) => m a -> (HasCallStack => a -> Bool) -> m () a =##> p = @@ -145,6 +153,8 @@ pGet c = do case cmd of CONNECT {} -> pGet c DISCONNECT {} -> pGet c + ERR (BROKER _ NETWORK) -> pGet c + MWARN {} -> pGet c _ -> pure t pattern CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> ACommand 'Agent e @@ -171,26 +181,33 @@ pattern MsgErr msgId err msgBody <- MSG MsgMeta {recipient = (msgId, _), integri pattern MsgErr' :: AgentMsgId -> MsgErrorType -> PQEncryption -> MsgBody -> ACommand 'Agent 'AEConn pattern MsgErr' msgId err pq msgBody <- MSG MsgMeta {recipient = (msgId, _), integrity = MsgError err, pqEncryption = pq} _ msgBody +pattern SENT :: AgentMsgId -> ACommand 'Agent 'AEConn +pattern SENT msgId = A.SENT msgId Nothing + pattern Rcvd :: AgentMsgId -> ACommand 'Agent 'AEConn pattern Rcvd agentMsgId <- RCVD MsgMeta {integrity = MsgOk} [MsgReceipt {agentMsgId, msgRcptStatus = MROk}] smpCfgVPrev :: ProtocolClientConfig SMPVersion -smpCfgVPrev = (smpCfg agentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg agentCfg} +smpCfgVPrev = (smpCfg agentCfg) {clientALPN = Nothing, serverVRange = prevRange $ serverVRange $ smpCfg agentCfg} smpCfgV7 :: ProtocolClientConfig SMPVersion smpCfgV7 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +ntfCfgVPrev :: ProtocolClientConfig NTFVersion +ntfCfgVPrev = (ntfCfg agentCfg) {clientALPN = Nothing, serverVRange = V.mkVersionRange (VersionNTF 1) (VersionNTF 1)} + ntfCfgV2 :: ProtocolClientConfig NTFVersion -ntfCfgV2 = (smpCfg agentCfg) {serverVRange = V.mkVersionRange (VersionNTF 1) authBatchCmdsNTFVersion} +ntfCfgV2 = (ntfCfg agentCfg) {serverVRange = V.mkVersionRange (VersionNTF 1) authBatchCmdsNTFVersion} agentCfgVPrev :: AgentConfig agentCfgVPrev = agentCfg { sndAuthAlg = C.AuthAlg C.SEd25519, - smpAgentVRange = \_ -> prevRange $ smpAgentVRange agentCfg PQSupportOff, + smpAgentVRange = prevRange $ smpAgentVRange agentCfg, smpClientVRange = prevRange $ smpClientVRange agentCfg, - e2eEncryptVRange = \_ -> prevRange $ e2eEncryptVRange agentCfg PQSupportOff, - smpCfg = smpCfgVPrev + e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg, + smpCfg = smpCfgVPrev, + ntfCfg = ntfCfgVPrev } -- agent config for the next client version @@ -198,14 +215,14 @@ agentCfgV7 :: AgentConfig agentCfgV7 = agentCfg { sndAuthAlg = C.AuthAlg C.SX25519, - smpAgentVRange = \_ -> V.mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, - e2eEncryptVRange = \_ -> V.mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, + smpAgentVRange = V.mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, + e2eEncryptVRange = V.mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, smpCfg = smpCfgV7, ntfCfg = ntfCfgV2 } agentCfgRatchetVPrev :: AgentConfig -agentCfgRatchetVPrev = agentCfg {e2eEncryptVRange = \_ -> prevRange $ e2eEncryptVRange agentCfg PQSupportOff} +agentCfgRatchetVPrev = agentCfg {e2eEncryptVRange = prevRange $ e2eEncryptVRange agentCfg} prevRange :: VersionRange v -> VersionRange v prevRange vr = vr {maxVersion = max (minVersion vr) (prevVersion $ maxVersion vr)} @@ -217,7 +234,7 @@ mkVersionRange :: Word16 -> Word16 -> VersionRange v mkVersionRange v1 v2 = V.mkVersionRange (Version v1) (Version v2) runRight_ :: (Eq e, Show e, HasCallStack) => ExceptT e IO () -> Expectation -runRight_ action = runExceptT action `shouldReturn` Right () +runRight_ action = withFrozenCallStack $ runExceptT action `shouldReturn` Right () runRight :: (Show e, HasCallStack) => ExceptT e IO a -> IO a runRight action = @@ -228,10 +245,10 @@ runRight action = getInAnyOrder :: HasCallStack => AgentClient -> [ATransmission 'Agent -> Bool] -> Expectation getInAnyOrder c ts = withFrozenCallStack $ inAnyOrder (pGet c) ts -inAnyOrder :: (Show a, MonadIO m, HasCallStack) => m a -> [a -> Bool] -> m () +inAnyOrder :: (Show a, MonadUnliftIO m, HasCallStack) => m a -> [a -> Bool] -> m () inAnyOrder _ [] = pure () inAnyOrder g rs = withFrozenCallStack $ do - r <- g + r <- 5000000 `timeout` g >>= maybe (error "inAnyOrder timeout") pure let rest = filter (not . expected r) rs if length rest < length rs then inAnyOrder g rest @@ -244,7 +261,7 @@ createConnection :: AgentClient -> UserId -> Bool -> SConnectionMode c -> Maybe createConnection c userId enableNtfs cMode clientData = A.createConnection c userId enableNtfs cMode clientData (IKNoPQ PQSupportOn) joinConnection :: AgentClient -> UserId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> AE ConnId -joinConnection c userId enableNtfs cReq connInfo = A.joinConnection c userId enableNtfs cReq connInfo PQSupportOn +joinConnection c userId enableNtfs cReq connInfo = A.joinConnection c userId Nothing enableNtfs cReq connInfo PQSupportOn sendMessage :: AgentClient -> ConnId -> SMP.MsgFlags -> MsgBody -> AE AgentMsgId sendMessage c connId msgFlags msgBody = do @@ -333,6 +350,9 @@ functionalAPITests t = do skip "faster version of the previous test (200 subscriptions gets very slow with test coverage)" $ it "should subscribe to multiple (6) subscriptions with batching" $ testBatchedSubscriptions 6 3 t + it "should subscribe to multiple connections with pending messages" $ + withSmpServer t $ + testBatchedPendingMessages 10 5 describe "Async agent commands" $ do it "should connect using async agent commands" $ withSmpServer t testAsyncCommands @@ -415,7 +435,7 @@ functionalAPITests t = do describe "server with password" $ do let auth = Just "abcd" srv = ProtoServerWithAuth testSMPServer2 - authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP AUTH) + authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP (B.unpack $ strEncode testSMPServer2) AUTH) it "should pass with correct password" $ testSMPServerConnectionTest t auth (srv auth) `shouldReturn` Nothing it "should fail without password" $ testSMPServerConnectionTest t auth (srv Nothing) `shouldReturn` authErr it "should fail with incorrect password" $ testSMPServerConnectionTest t auth (srv $ Just "wrong") `shouldReturn` authErr @@ -428,6 +448,11 @@ functionalAPITests t = do it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent t describe "user network info" $ do it "should wait for user network" testWaitForUserNetwork + it "should not reset online to offline if happens too quickly" testDoNotResetOnlineToOffline + it "should resume multiple threads" testResumeMultipleThreads + describe "SMP queue info" $ do + it "server should respond with queue and subscription information" $ + withSmpServer t testServerQueueInfo testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> (Maybe BasicAuth, VersionSMP) -> IO Int testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do @@ -447,26 +472,28 @@ canCreateQueue allowNew (srvAuth, srvVersion) (clntAuth, clntVersion) = let v = basicAuthSMPVersion in allowNew && (isNothing srvAuth || (srvVersion >= v && clntVersion >= v && srvAuth == clntAuth)) -testMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testMatrix2 t runTest = do - it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn - it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn - it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn - it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff - it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff - it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True + it "v7" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False + it "v7 to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False + it "current to v7" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False + it "current with v7 server" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "prev" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfgVPrev 3 $ runTest PQSupportOff False + it "prev to current" $ withSmpServer t $ runTestCfg2 agentCfgVPrev agentCfg 3 $ runTest PQSupportOff False + it "current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgVPrev 3 $ runTest PQSupportOff False -testRatchetMatrix2 :: ATransport -> (PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec +testRatchetMatrix2 :: ATransport -> (PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> Spec testRatchetMatrix2 t runTest = do - it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn - it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn - it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn - it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn - it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff - it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff - it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff + it "v8, via proxy" $ withSmpServerProxy t $ runTestCfgServers2 agentProxyCfg agentProxyCfg (initAgentServersProxy SPMAlways SPFProhibit) 3 $ runTest PQSupportOn True + it "ratchet next" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfgV7 3 $ runTest PQSupportOn False + it "ratchet next to current" $ withSmpServerV7 t $ runTestCfg2 agentCfgV7 agentCfg 3 $ runTest PQSupportOn False + it "ratchet current to next" $ withSmpServerV7 t $ runTestCfg2 agentCfg agentCfgV7 3 $ runTest PQSupportOn False + it "ratchet current" $ withSmpServer t $ runTestCfg2 agentCfg agentCfg 3 $ runTest PQSupportOn False + it "ratchet prev" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfgRatchetVPrev 3 $ runTest PQSupportOff False + it "ratchets prev to current" $ withSmpServer t $ runTestCfg2 agentCfgRatchetVPrev agentCfg 3 $ runTest PQSupportOff False + it "ratchets current to prev" $ withSmpServer t $ runTestCfg2 agentCfg agentCfgRatchetVPrev 3 $ runTest PQSupportOff False testServerMatrix2 :: ATransport -> (InitialAgentServers -> IO ()) -> Spec testServerMatrix2 t runTest = do @@ -474,10 +501,14 @@ testServerMatrix2 t runTest = do it "2 servers" $ withSmpServer t . withSmpServerOn t testPort2 $ runTest initAgentServers2 runTestCfg2 :: HasCallStack => AgentConfig -> AgentConfig -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () -runTestCfg2 aCfg bCfg baseMsgId runTest = - withAgentClientsCfg2 aCfg bCfg $ \a b -> runTest a b baseMsgId +runTestCfg2 aCfg bCfg = runTestCfgServers2 aCfg bCfg initAgentServers {-# INLINE runTestCfg2 #-} +runTestCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> AgentMsgId -> (HasCallStack => AgentClient -> AgentClient -> AgentMsgId -> IO ()) -> IO () +runTestCfgServers2 aCfg bCfg servers baseMsgId runTest = + withAgentClientsCfgServers2 aCfg bCfg servers $ \a b -> runTest a b baseMsgId +{-# INLINE runTestCfgServers2 #-} + withAgentClientsCfgServers2 :: HasCallStack => AgentConfig -> AgentConfig -> InitialAgentServers -> (HasCallStack => AgentClient -> AgentClient -> IO ()) -> IO () withAgentClientsCfgServers2 aCfg bCfg servers runTest = withAgent 1 aCfg servers testDB $ \a -> @@ -498,11 +529,11 @@ withAgentClients3 runTest = withAgent 3 agentCfg initAgentServers testDB3 $ \c -> runTest a b c -runAgentClientTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientTest pqSupport alice@AgentClient {} bob baseId = +runAgentClientTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientTest pqSupport viaProxy alice@AgentClient {} bob baseId = runRight_ $ do (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe + aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" pqSupport SMSubscribe ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport allowConnection alice bobId confId "alice's connInfo" @@ -511,25 +542,26 @@ runAgentClientTest pqSupport alice@AgentClient {} bob baseId = get bob ##> ("", aliceId, A.INFO pqSupport "alice's connInfo") get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let proxySrv = if viaProxy then Just testSMPServer else Nothing 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT $ baseId + 1) + get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv) 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" - get alice ##> ("", bobId, SENT $ baseId + 2) + get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv) get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" - get bob ##> ("", aliceId, SENT $ baseId + 3) + get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv) 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" - get bob ##> ("", aliceId, SENT $ baseId + 4) + get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv) get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId 5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where @@ -545,28 +577,27 @@ testEnablePQEncryption = (a, 4, "msg 1") \#>\ b (b, 5, "msg 2") \#>\ a -- 45 bytes is used by agent message envelope inside double ratchet message envelope - let largeMsg g' pqEnc = atomically $ C.randomBytes (e2eEncUserMsgLength pqdrSMPAgentVersion pqEnc - 45) g' + let largeMsg g' pqEnc = atomically $ C.randomBytes (e2eEncAgentMsgLength pqdrSMPAgentVersion pqEnc - 45) g' lrg <- largeMsg g PQSupportOff (a, 6, lrg) \#>\ b (b, 7, lrg) \#>\ a - -- enabling PQ encryption - (a, 8, lrg) \#>! b - (b, 9, lrg) \#>! a -- switched to smaller envelopes (before reporting PQ encryption enabled) sml <- largeMsg g PQSupportOn -- fail because of message size - Left (A.CMD LARGE) <- tryError $ A.sendMessage ca bId PQEncOn SMP.noMsgFlags lrg - (11, PQEncOff) <- A.sendMessage ca bId PQEncOn SMP.noMsgFlags sml - get ca =##> \case ("", connId, SENT 11) -> connId == bId; _ -> False - get cb =##> \case ("", connId, MsgErr' 10 MsgSkipped {} PQEncOff msg') -> connId == aId && msg' == sml; _ -> False - ackMessage cb aId 10 Nothing + Left (A.CMD LARGE _) <- tryError $ A.sendMessage ca bId PQEncOn SMP.noMsgFlags lrg + (9, PQEncOff) <- A.sendMessage ca bId PQEncOn SMP.noMsgFlags sml + get ca =##> \case ("", connId, SENT 9) -> connId == bId; _ -> False + get cb =##> \case ("", connId, MsgErr' 8 MsgSkipped {} PQEncOff msg') -> connId == aId && msg' == sml; _ -> False + ackMessage cb aId 8 Nothing -- -- fail in reply to sync IDss - Left (A.CMD LARGE) <- tryError $ A.sendMessage cb aId PQEncOn SMP.noMsgFlags lrg - (12, PQEncOn) <- A.sendMessage cb aId PQEncOn SMP.noMsgFlags sml - get cb =##> \case ("", connId, SENT 12) -> connId == aId; _ -> False - get ca =##> \case ("", connId, MsgErr' 12 MsgSkipped {} PQEncOn msg') -> connId == bId && msg' == sml; _ -> False - ackMessage ca bId 12 Nothing + Left (A.CMD LARGE _) <- tryError $ A.sendMessage cb aId PQEncOn SMP.noMsgFlags lrg + (10, PQEncOff) <- A.sendMessage cb aId PQEncOn SMP.noMsgFlags sml + get cb =##> \case ("", connId, SENT 10) -> connId == aId; _ -> False + get ca =##> \case ("", connId, MsgErr' 10 MsgSkipped {} PQEncOff msg') -> connId == bId && msg' == sml; _ -> False + ackMessage ca bId 10 Nothing + (a, 11, sml) \#>! b -- PQ encryption now enabled + (b, 12, sml) !#>! a (a, 13, sml) !#>! b (b, 14, sml) !#>! a -- disabling PQ encryption @@ -586,8 +617,8 @@ testEnablePQEncryption = (b, 26, sml) \#>\ a (a, 27, sml) \#>\ b -- PQ encryption is now disabled, but support remained enabled, so we still cannot send larger messages - Left (A.CMD LARGE) <- tryError $ A.sendMessage ca bId PQEncOff SMP.noMsgFlags (sml <> "123456") - Left (A.CMD LARGE) <- tryError $ A.sendMessage cb aId PQEncOff SMP.noMsgFlags (sml <> "123456") + Left (A.CMD LARGE _) <- tryError $ A.sendMessage ca bId PQEncOff SMP.noMsgFlags (sml <> "123456") + Left (A.CMD LARGE _) <- tryError $ A.sendMessage cb aId PQEncOff SMP.noMsgFlags (sml <> "123456") pure () where (\#>\) = PQEncOff `sndRcv` PQEncOff @@ -626,11 +657,13 @@ testAgentClient3 = get c =##> \case ("", connId, Msg "c5") -> connId == aIdForC; _ -> False ackMessage c aIdForC 5 Nothing -runAgentClientContactTest :: HasCallStack => PQSupport -> AgentClient -> AgentClient -> AgentMsgId -> IO () -runAgentClientContactTest pqSupport alice bob baseId = +runAgentClientContactTest :: HasCallStack => PQSupport -> Bool -> AgentClient -> AgentClient -> AgentMsgId -> IO () +runAgentClientContactTest pqSupport viaProxy alice bob baseId = runRight_ $ do (_, qInfo) <- A.createConnection alice 1 True SCMContact Nothing (IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob 1 True qInfo "bob's connInfo" pqSupport SMSubscribe + aliceId <- A.prepareConnectionToJoin bob 1 True qInfo pqSupport + aliceId' <- A.joinConnection bob 1 (Just aliceId) True qInfo "bob's connInfo" pqSupport SMSubscribe + liftIO $ aliceId' `shouldBe` aliceId ("", _, A.REQ invId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport bobId <- acceptContact alice True invId "alice's connInfo" PQSupportOn SMSubscribe @@ -642,25 +675,26 @@ runAgentClientContactTest pqSupport alice bob baseId = get alice ##> ("", bobId, A.CON pqEnc) get bob ##> ("", aliceId, A.CON pqEnc) -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let proxySrv = if viaProxy then Just testSMPServer else Nothing 1 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "hello" - get alice ##> ("", bobId, SENT $ baseId + 1) + get alice ##> ("", bobId, A.SENT (baseId + 1) proxySrv) 2 <- msgId <$> A.sendMessage alice bobId pqEnc SMP.noMsgFlags "how are you?" - get alice ##> ("", bobId, SENT $ baseId + 2) + get alice ##> ("", bobId, A.SENT (baseId + 2) proxySrv) get bob =##> \case ("", c, Msg' _ pq "hello") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 1) Nothing get bob =##> \case ("", c, Msg' _ pq "how are you?") -> c == aliceId && pq == pqEnc; _ -> False ackMessage bob aliceId (baseId + 2) Nothing 3 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "hello too" - get bob ##> ("", aliceId, SENT $ baseId + 3) + get bob ##> ("", aliceId, A.SENT (baseId + 3) proxySrv) 4 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 1" - get bob ##> ("", aliceId, SENT $ baseId + 4) + get bob ##> ("", aliceId, A.SENT (baseId + 4) proxySrv) get alice =##> \case ("", c, Msg' _ pq "hello too") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 3) Nothing get alice =##> \case ("", c, Msg' _ pq "message 1") -> c == bobId && pq == pqEnc; _ -> False ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId 5 <- msgId <$> A.sendMessage bob aliceId pqEnc SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where @@ -796,8 +830,8 @@ testAllowConnectionClientRestart t = do testIncreaseConnAgentVersion :: HasCallStack => ATransport -> IO () testIncreaseConnAgentVersion t = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob @@ -809,7 +843,7 @@ testIncreaseConnAgentVersion t = do -- version doesn't increase if incompatible disposeAgentClient alice - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB runRight_ $ do subscribeConnection alice2 bobId @@ -820,7 +854,7 @@ testIncreaseConnAgentVersion t = do -- version increases if compatible disposeAgentClient bob - bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB2 + bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 runRight_ $ do subscribeConnection bob2 aliceId @@ -831,7 +865,7 @@ testIncreaseConnAgentVersion t = do -- version doesn't decrease, even if incompatible disposeAgentClient alice2 - alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = \_ -> mkVersionRange 2 2} initAgentServers testDB + alice3 <- getSMPAgentClient' 5 agentCfg {smpAgentVRange = mkVersionRange 2 2} initAgentServers testDB runRight_ $ do subscribeConnection alice3 bobId @@ -840,7 +874,7 @@ testIncreaseConnAgentVersion t = do checkVersion bob2 aliceId 3 disposeAgentClient bob2 - bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 1} initAgentServers testDB2 + bob3 <- getSMPAgentClient' 6 agentCfg {smpAgentVRange = mkVersionRange 1 1} initAgentServers testDB2 runRight_ $ do subscribeConnection bob3 aliceId @@ -857,8 +891,8 @@ checkVersion c connId v = do testIncreaseConnAgentVersionMaxCompatible :: HasCallStack => ATransport -> IO () testIncreaseConnAgentVersionMaxCompatible t = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob @@ -870,7 +904,7 @@ testIncreaseConnAgentVersionMaxCompatible t = do -- version increases to max compatible disposeAgentClient alice - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB disposeAgentClient bob bob2 <- getSMPAgentClient' 4 agentCfg {smpAgentVRange = supportedSMPAgentVRange} initAgentServers testDB2 @@ -885,8 +919,8 @@ testIncreaseConnAgentVersionMaxCompatible t = do testIncreaseConnAgentVersionStartDifferentVersion :: HasCallStack => ATransport -> IO () testIncreaseConnAgentVersionStartDifferentVersion t = do - alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 2} initAgentServers testDB - bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB2 + alice <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 2} initAgentServers testDB + bob <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aliceId, bobId) <- runRight $ do (aliceId, bobId) <- makeConnection_ PQSupportOff alice bob @@ -898,7 +932,7 @@ testIncreaseConnAgentVersionStartDifferentVersion t = do -- version increases to max compatible disposeAgentClient alice - alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB + alice2 <- getSMPAgentClient' 3 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB runRight_ $ do subscribeConnection alice2 bobId @@ -1106,7 +1140,7 @@ testExpireMessageQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} testP 5 <- sendMessage a bId SMP.noMsgFlags "2" liftIO $ threadDelay 1000000 6 <- sendMessage a bId SMP.noMsgFlags "3" -- this won't expire - get a =##> \case ("", c, MERR 5 (SMP QUOTA)) -> bId == c; _ -> False + get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False pure (aId, bId) withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do subscribeConnection b' aId @@ -1134,15 +1168,15 @@ testExpireManyMessagesQuota t = withSmpServerConfigOn t cfg {msgQueueQuota = 1} 7 <- sendMessage a bId SMP.noMsgFlags "4" liftIO $ threadDelay 1000000 8 <- sendMessage a bId SMP.noMsgFlags "5" -- this won't expire - get a =##> \case ("", c, MERR 5 (SMP QUOTA)) -> bId == c; _ -> False + get a =##> \case ("", c, MERR 5 (SMP _ QUOTA)) -> bId == c; _ -> False get a >>= \case - ("", c, MERR 6 (SMP QUOTA)) -> do + ("", c, MERR 6 (SMP _ QUOTA)) -> do liftIO $ bId `shouldBe` c - get a =##> \case ("", c', MERR 7 (SMP QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP QUOTA)) -> bId == c'; _ -> False - ("", c, MERRS [6] (SMP QUOTA)) -> do + get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; ("", c', MERRS [7] (SMP _ QUOTA)) -> bId == c'; _ -> False + ("", c, MERRS [6] (SMP _ QUOTA)) -> do liftIO $ bId `shouldBe` c - get a =##> \case ("", c', MERR 7 (SMP QUOTA)) -> bId == c'; _ -> False - ("", c, MERRS [6, 7] (SMP QUOTA)) -> liftIO $ bId `shouldBe` c + get a =##> \case ("", c', MERR 7 (SMP _ QUOTA)) -> bId == c'; _ -> False + ("", c, MERRS [6, 7] (SMP _ QUOTA)) -> liftIO $ bId `shouldBe` c r -> error $ show r pure (aId, bId) withAgent 3 agentCfg initAgentServers testDB2 $ \b' -> runRight_ $ do @@ -1242,15 +1276,10 @@ testRatchetSyncServerOffline t = withAgentClients2 $ \alice bob -> do liftIO $ ratchetSyncState `shouldBe` RSStarted withSmpServerStoreMsgLogOn t testPort $ \_ -> do + concurrently_ + (getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP]) + (getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP]) runRight_ $ do - liftIO . getInAnyOrder alice $ - [ ratchetSyncP' bobId RSAgreed, - serverUpP - ] - liftIO . getInAnyOrder bob2 $ - [ ratchetSyncP' aliceId RSAgreed, - serverUpP - ] get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 @@ -1304,15 +1333,10 @@ testRatchetSyncSuspendForeground t = do foregroundAgent bob2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do + concurrently_ + (getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP]) + (getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP]) runRight_ $ do - liftIO . getInAnyOrder alice $ - [ ratchetSyncP' bobId RSAgreed, - serverUpP - ] - liftIO . getInAnyOrder bob2 $ - [ ratchetSyncP' aliceId RSAgreed, - serverUpP - ] get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 @@ -1337,15 +1361,10 @@ testRatchetSyncSimultaneous t = do liftIO $ aRSS `shouldBe` RSStarted withSmpServerStoreMsgLogOn t testPort $ \_ -> do + concurrently_ + (getInAnyOrder alice [ratchetSyncP' bobId RSAgreed, serverUpP]) + (getInAnyOrder bob2 [ratchetSyncP' aliceId RSAgreed, serverUpP]) runRight_ $ do - liftIO . getInAnyOrder alice $ - [ ratchetSyncP' bobId RSAgreed, - serverUpP - ] - liftIO . getInAnyOrder bob2 $ - [ ratchetSyncP' aliceId RSAgreed, - serverUpP - ] get alice =##> ratchetSyncP bobId RSOk get bob2 =##> ratchetSyncP aliceId RSOk exchangeGreetingsMsgIds alice bobId 12 bob2 aliceId 9 @@ -1393,13 +1412,15 @@ makeConnection = makeConnection_ PQSupportOn makeConnection_ :: PQSupport -> AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnection_ pqEnc alice bob = makeConnectionForUsers_ pqEnc alice 1 bob 1 -makeConnectionForUsers :: AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) +makeConnectionForUsers :: HasCallStack => AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers = makeConnectionForUsers_ PQSupportOn -makeConnectionForUsers_ :: PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) +makeConnectionForUsers_ :: HasCallStack => PQSupport -> AgentClient -> UserId -> AgentClient -> UserId -> ExceptT AgentErrorType IO (ConnId, ConnId) makeConnectionForUsers_ pqSupport alice aliceUserId bob bobUserId = do (bobId, qInfo) <- A.createConnection alice aliceUserId True SCMInvitation Nothing (CR.IKNoPQ pqSupport) SMSubscribe - aliceId <- A.joinConnection bob bobUserId True qInfo "bob's connInfo" pqSupport SMSubscribe + aliceId <- A.prepareConnectionToJoin bob bobUserId True qInfo pqSupport + aliceId' <- A.joinConnection bob bobUserId (Just aliceId) True qInfo "bob's connInfo" pqSupport SMSubscribe + liftIO $ aliceId' `shouldBe` aliceId ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice liftIO $ pqSup' `shouldBe` pqSupport allowConnection alice bobId confId "alice's connInfo" @@ -1492,9 +1513,9 @@ testSuspendingAgentCompleteSending t = withAgentClients2 $ \a b -> do liftIO $ suspendAgent b 5000000 withSmpServerStoreLogOn t testPort $ \_ -> runRight_ @AgentErrorType $ do - pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC _ (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False - pGet b =##> \case ("", c, APC _ (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 5)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False + pGet b =##> \case ("", c, APC SAEConn (SENT 6)) -> c == aId; ("", "", APC _ UP {}) -> True; _ -> False ("", "", SUSPENDED) <- nGet b pGet a =##> \case ("", c, APC _ (Msg "hello too")) -> c == bId; ("", "", APC _ UP {}) -> True; _ -> False @@ -1526,7 +1547,7 @@ testBatchedSubscriptions :: Int -> Int -> ATransport -> IO () testBatchedSubscriptions nCreate nDel t = withAgentClientsCfgServers2 agentCfg agentCfg initAgentServers2 $ \a b -> do conns <- runServers $ do - conns <- replicateM (nCreate :: Int) $ makeConnection_ PQSupportOff a b + conns <- replicateM nCreate $ makeConnection_ PQSupportOff a b forM_ conns $ \(aId, bId) -> exchangeGreetings_ PQEncOff a bId b aId let (aIds', bIds') = unzip $ take nDel conns delete a bIds' @@ -1585,6 +1606,25 @@ testBatchedSubscriptions nCreate nDel t = killThread t1 pure res +testBatchedPendingMessages :: Int -> Int -> IO () +testBatchedPendingMessages nCreate nMsgs = + withA $ \a -> do + conns <- withB $ \b -> runRight $ do + replicateM nCreate $ makeConnection a b + let msgConns = take nMsgs conns + runRight_ $ forM_ msgConns $ \(_, bId) -> sendMessage a bId SMP.noMsgFlags "hello" + replicateM_ nMsgs $ get a =##> \case ("", cId, SENT _) -> isJust $ find ((cId ==) . snd) msgConns; _ -> False + withB $ \b -> runRight_ $ do + r <- subscribeConnections b $ map fst conns + liftIO $ all isRight r `shouldBe` True + replicateM_ nMsgs $ do + ("", cId, Msg' msgId _ "hello") <- get b + liftIO $ isJust (find ((cId ==) . fst) msgConns) `shouldBe` True + ackMessage b cId msgId Nothing + where + withA = withAgent 1 agentCfg initAgentServers testDB + withB = withAgent 2 agentCfg initAgentServers testDB2 + testAsyncCommands :: IO () testAsyncCommands = withAgentClients2 $ \alice bob -> runRight_ $ do @@ -1679,7 +1719,7 @@ testAcceptContactAsync = ackMessage alice bobId (baseId + 4) Nothing suspendConnection alice bobId 5 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 5) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 5); _ -> False deleteConnection alice bobId liftIO $ noMessages alice "nothing else should be delivered to alice" where @@ -1725,7 +1765,7 @@ testWaitDeliveryNoPending t = withAgentClients2 $ \alice bob -> get alice =##> \case ("", cId, DEL_CONN) -> cId == bobId; _ -> False 3 <- msgId <$> sendMessage bob aliceId SMP.noMsgFlags "message 2" - get bob ##> ("", aliceId, MERR (baseId + 3) (SMP AUTH)) + get bob =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == aliceId && mId == (baseId + 3); _ -> False liftIO $ noMessages alice "nothing else should be delivered to alice" liftIO $ noMessages bob "nothing else should be delivered to bob" @@ -1820,8 +1860,8 @@ testWaitDeliveryAUTHErr t = liftIO $ noMessages bob "nothing else should be delivered to bob" withSmpServerStoreLogOn t testPort $ \_ -> do - get alice ##> ("", bobId, MERR (baseId + 3) (SMP AUTH)) - get alice ##> ("", bobId, MERR (baseId + 4) (SMP AUTH)) + get alice =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == bobId && mId == (baseId + 3); _ -> False + get alice =##> \case ("", cId, MERR mId (SMP _ AUTH)) -> cId == bobId && mId == (baseId + 4); _ -> False get alice =##> \case ("", cId, DEL_CONN) -> cId == bobId; _ -> False liftIO $ noMessages alice "nothing else should be delivered to alice" @@ -2392,11 +2432,11 @@ testCreateQueueAuth srvVersion clnt1 clnt2 = do b <- getClient 2 clnt2 testDB2 r <- runRight $ do tryError (createConnection a 1 True SCMInvitation Nothing SMSubscribe) >>= \case - Left (SMP AUTH) -> pure 0 + Left (SMP _ AUTH) -> pure 0 Left e -> throwError e Right (bId, qInfo) -> tryError (joinConnection b 1 True qInfo "bob's connInfo" SMSubscribe) >>= \case - Left (SMP AUTH) -> pure 1 + Left (SMP _ AUTH) -> pure 1 Left e -> throwError e Right aId -> do ("", _, CONF confId _ "bob's connInfo") <- get a @@ -2412,7 +2452,8 @@ testCreateQueueAuth srvVersion clnt1 clnt2 = do where getClient clientId (clntAuth, clntVersion) db = let servers = initAgentServers {smp = userServers [ProtoServerWithAuth testSMPServer clntAuth]} - smpCfg = (defaultSMPClientConfig :: ProtocolClientConfig SMPVersion) {serverVRange = V.mkVersionRange (prevVersion basicAuthSMPVersion) clntVersion} + alpn_ = if clntVersion >= authCmdsSMPVersion then Just supportedSMPHandshakes else Nothing + smpCfg = defaultClientConfig alpn_ $ 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 @@ -2448,13 +2489,13 @@ testDeliveryReceipts = get a =##> \case ("", c, Msg "hello too") -> c == bId; _ -> False ackMessage a bId 6 $ Just "" get b =##> \case ("", c, Rcvd 6) -> c == aId; _ -> False - ackMessage b aId 7 (Just "") `catchError` \e -> liftIO $ e `shouldBe` A.CMD PROHIBITED + ackMessage b aId 7 (Just "") `catchError` \case (A.CMD PROHIBITED _) -> pure (); e -> liftIO $ expectationFailure ("unexpected error " <> show e) ackMessage b aId 7 Nothing testDeliveryReceiptsVersion :: HasCallStack => ATransport -> IO () testDeliveryReceiptsVersion t = do - a <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB - b <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = \_ -> mkVersionRange 1 3} initAgentServers testDB2 + a <- getSMPAgentClient' 1 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB + b <- getSMPAgentClient' 2 agentCfg {smpAgentVRange = mkVersionRange 1 3} initAgentServers testDB2 withSmpServerStoreMsgLogOn t testPort $ \_ -> do (aId, bId) <- runRight $ do (aId, bId) <- makeConnection_ PQSupportOff a b @@ -2481,8 +2522,8 @@ testDeliveryReceiptsVersion t = do subscribeConnection a' bId subscribeConnection b' aId exchangeGreetingsMsgId_ PQEncOff 6 a' bId b' aId - checkVersion a' bId 4 - checkVersion b' aId 4 + checkVersion a' bId 5 + checkVersion b' aId 5 (8, PQEncOff) <- A.sendMessage a' bId PQEncOn SMP.noMsgFlags "hello" get a' ##> ("", bId, SENT 8) get b' =##> \case ("", c, Msg' 8 PQEncOff "hello") -> c == aId; _ -> False @@ -2661,32 +2702,169 @@ testServerMultipleIdentities = } testE2ERatchetParams12 -testWaitForUserNetwork :: HasCallStack => IO () +testWaitForUserNetwork :: IO () testWaitForUserNetwork = do a <- getSMPAgentClient' 1 aCfg initAgentServers testDB noNetworkDelay a - setUserNetworkInfo a $ UserNetworkInfo UNNone + setUserNetworkInfo a $ UserNetworkInfo UNNone False networkDelay a 100000 - networkDelay a 150000 - networkDelay a 200000 - networkDelay a 200000 - setUserNetworkInfo a $ UserNetworkInfo UNCellular + networkDelay a 100000 + setUserNetworkInfo a $ UserNetworkInfo UNCellular True noNetworkDelay a - setUserNetworkInfo a $ UserNetworkInfo UNNone + setUserNetworkInfo a $ UserNetworkInfo UNCellular False networkDelay a 100000 concurrently_ - (threadDelay 50000 >> setUserNetworkInfo a (UserNetworkInfo UNCellular)) + (threadDelay 50000 >> setUserNetworkInfo a (UserNetworkInfo UNCellular True)) (networkDelay a 50000) noNetworkDelay a where - aCfg = agentCfg {userNetworkInterval = RetryInterval {initialInterval = 100000, increaseAfter = 0, maxInterval = 200000}} - noNetworkDelay a = (10000 >) <$> waitNetwork a `shouldReturn` True - networkDelay a d' = (\d -> d' < d && d < d' + 15000) <$> waitNetwork a `shouldReturn` True - waitNetwork a = do - t <- getCurrentTime - waitForUserNetwork a `runReaderT` agentEnv a - t' <- getCurrentTime - pure $ diffToMicroseconds $ diffUTCTime t' t + aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0} + +testDoNotResetOnlineToOffline :: IO () +testDoNotResetOnlineToOffline = do + a <- getSMPAgentClient' 1 aCfg initAgentServers testDB + noNetworkDelay a + setUserNetworkInfo a $ UserNetworkInfo UNWifi False + networkDelay a 100000 + setUserNetworkInfo a $ UserNetworkInfo UNWifi False + setUserNetworkInfo a $ UserNetworkInfo UNWifi True + noNetworkDelay a + setUserNetworkInfo a $ UserNetworkInfo UNWifi False -- ingnored + noNetworkDelay a + threadDelay 100000 + setUserNetworkInfo a $ UserNetworkInfo UNWifi False + networkDelay a 100000 + setUserNetworkInfo a $ UserNetworkInfo UNNone False + networkDelay a 100000 + setUserNetworkInfo a $ UserNetworkInfo UNWifi True + setUserNetworkInfo a $ UserNetworkInfo UNNone False -- ingnored + noNetworkDelay a + where + aCfg = agentCfg {userNetworkInterval = 100000, userOfflineDelay = 0.1} + +testResumeMultipleThreads :: IO () +testResumeMultipleThreads = do + a <- getSMPAgentClient' 1 aCfg initAgentServers testDB + noNetworkDelay a + setUserNetworkInfo a $ UserNetworkInfo UNNone False + vs <- + replicateM 50000 $ do + v <- newEmptyTMVarIO + void . forkIO $ waitNetwork a >>= atomically . putTMVar v + pure v + threadDelay 1000000 + setUserNetworkInfo a $ UserNetworkInfo UNCellular True + ts <- mapM (atomically . readTMVar) vs + -- print $ minimum ts + -- print $ maximum ts + -- print $ sum ts `div` fromIntegral (length ts) + let average = sum ts `div` fromIntegral (length ts) + average < 3000000 `shouldBe` True + maximum ts < 4000000 `shouldBe` True + where + aCfg = agentCfg {userOfflineDelay = 0} + +testServerQueueInfo :: IO () +testServerQueueInfo = do + withAgentClients2 $ \alice bob -> runRight_ $ do + (bobId, cReq) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe + liftIO $ threadDelay 200000 + checkEmptyQ alice bobId False + aliceId <- joinConnection bob 1 True cReq "bob's connInfo" SMSubscribe + ("", _, CONF confId _ "bob's connInfo") <- get alice + liftIO $ threadDelay 200000 + checkEmptyQ alice bobId False + allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, CON) + get bob ##> ("", aliceId, INFO "alice's connInfo") + get bob ##> ("", aliceId, CON) + liftIO $ threadDelay 200000 + checkEmptyQ alice bobId True + checkEmptyQ bob aliceId True + let msgId = 4 + (msgId', PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello" + liftIO $ msgId' `shouldBe` msgId + get alice ##> ("", bobId, SENT msgId) + liftIO $ threadDelay 200000 + Just srvMsgId <- checkMsgQ bob aliceId 1 + get bob =##> \case + ("", c, MSG MsgMeta {integrity = MsgOk, broker = (smId, _), recipient = (mId, _), pqEncryption = PQEncOn} _ "hello") -> + c == aliceId && decodeLatin1 (B64.encode smId) == srvMsgId && mId == msgId + _ -> False + ackMessage bob aliceId msgId Nothing + liftIO $ threadDelay 200000 + checkEmptyQ bob aliceId True + (msgId1, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 1" + get alice ##> ("", bobId, SENT msgId1) + Just _ <- checkMsgQ bob aliceId 1 + (msgId2, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 2" + get alice ##> ("", bobId, SENT msgId2) + (msgId3, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 3" + get alice ##> ("", bobId, SENT msgId3) + (msgId4, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello 4" + get alice ##> ("", bobId, SENT msgId4) + Just _ <- checkMsgQ bob aliceId 4 + (msgId5, PQEncOn) <- A.sendMessage alice bobId PQEncOn SMP.noMsgFlags "hello: quota exceeded" + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 5 + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 1") -> c == aliceId && mId == msgId1; _ -> False + ackMessage bob aliceId msgId1 Nothing + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 4 + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 2") -> c == aliceId && mId == msgId2; _ -> False + ackMessage bob aliceId msgId2 Nothing + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 3") -> c == aliceId && mId == msgId3; _ -> False + ackMessage bob aliceId msgId3 Nothing + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 2 + get bob =##> \case ("", c, Msg' mId PQEncOn "hello 4") -> c == aliceId && mId == msgId4; _ -> False + ackMessage bob aliceId msgId4 Nothing + liftIO $ threadDelay 200000 + Just _ <- checkMsgQ bob aliceId 1 -- the one that did not fit now accepted + get alice ##> ("", bobId, QCONT) + get alice ##> ("", bobId, SENT msgId5) + liftIO $ threadDelay 200000 + Just _srvMsgId <- checkQ bob aliceId True (Just QNoSub) 1 (Just MTMessage) + get bob =##> \case ("", c, Msg' mId PQEncOn "hello: quota exceeded") -> c == aliceId && mId == msgId5 + 1; _ -> False + ackMessage bob aliceId (msgId5 + 1) Nothing + liftIO $ threadDelay 200000 + checkEmptyQ bob aliceId True + pure () + where + checkEmptyQ c cId qiSnd' = do + r <- checkQ c cId qiSnd' (Just QSubThread) 0 Nothing + liftIO $ r `shouldBe` Nothing + checkMsgQ c cId qiSize' = do + r <- checkQ c cId True (Just QNoSub) qiSize' (Just MTMessage) + liftIO $ isJust r `shouldBe` True + pure r + checkQ c cId qiSnd' qiSubThread_ qiSize' msgType_ = do + QueueInfo {qiSnd, qiNtf, qiSub, qiSize, qiMsg} <- getConnectionQueueInfo c cId + liftIO $ do + qiSnd `shouldBe` qiSnd' + qiNtf `shouldBe` False + qSubThread <$> qiSub `shouldBe` qiSubThread_ + qiSize `shouldBe` qiSize' + msgId_ <- forM qiMsg $ \MsgInfo {msgId, msgType} -> msgId <$ (Just msgType `shouldBe` msgType_) + qDelivered <$> qiSub `shouldBe` Just msgId_ + pure msgId_ + +noNetworkDelay :: AgentClient -> IO () +noNetworkDelay a = do + d <- waitNetwork a + unless (d < 10000) $ expectationFailure $ "expected no delay, d = " <> show d + +networkDelay :: AgentClient -> Int64 -> IO () +networkDelay a d' = do + d <- waitNetwork a + unless (d' < d && d < d' + 15000) $ expectationFailure $ "expected delay " <> show d' <> ", d = " <> show d + +waitNetwork :: AgentClient -> IO Int64 +waitNetwork a = do + t <- getCurrentTime + waitForUserNetwork a + t' <- getCurrentTime + pure $ diffToMicroseconds $ diffUTCTime t' t exchangeGreetings :: HasCallStack => AgentClient -> ConnId -> AgentClient -> ConnId -> ExceptT AgentErrorType IO () exchangeGreetings = exchangeGreetings_ PQEncOn @@ -2725,3 +2903,16 @@ exchangeGreetingsMsgIds alice bobId aliceMsgId bob aliceId bobMsgId = do get bob ##> ("", aliceId, SENT bobMsgId') get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False ackMessage alice bobId aliceMsgId' Nothing + +newtype InternalException e = InternalException {unInternalException :: e} + deriving (Eq, Show) + +instance Exception e => Exception (InternalException e) + +instance Exception e => MonadUnliftIO (ExceptT e IO) where + {-# INLINE withRunInIO #-} + withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b + withRunInIO inner = + ExceptT . fmap (first unInternalException) . try $ + withRunInIO $ \run -> + inner $ run . (either (throwIO . InternalException) pure <=< runExceptT) diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 2c1045791..a7c3fb25a 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -6,7 +6,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TypeApplications #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module AgentTests.NotificationTests where @@ -17,10 +16,6 @@ import AgentTests.FunctionalAPITests createConnection, exchangeGreetingsMsgId, get, - withAgent, - withAgentClients2, - withAgentClientsCfgServers2, - withAgentClients3, joinConnection, makeConnection, nGet, @@ -29,13 +24,18 @@ import AgentTests.FunctionalAPITests sendMessage, switchComplete, testServerMatrix2, + withAgent, + withAgentClients2, + withAgentClients3, withAgentClientsCfg2, + withAgentClientsCfgServers2, (##>), (=##>), pattern CON, pattern CONF, pattern INFO, pattern Msg, + pattern SENT, ) import Control.Concurrent (ThreadId, killThread, threadDelay) import Control.Monad @@ -55,12 +55,12 @@ import SMPClient (cfg, cfgV7, testPort, testPort2, testStoreLogFile2, withSmpSer import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore') import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers) -import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT) import Simplex.Messaging.Agent.Store.SQLite (getSavedNtfToken) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Protocol +import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..)) import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Types (NtfToken (..)) import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) @@ -151,7 +151,8 @@ testNtfMatrix t runTest = do it "next servers: SMP v7, NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfgV2 agentCfg agentCfg runTest it "curr servers: SMP v6, NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfg agentCfg agentCfg runTest skip "this case cannot be supported - see RFC" $ - it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest + it "servers: SMP v6, NTF v1; clients: v7/v2 (not supported)" $ + runNtfTestCfg t cfg ntfServerCfg agentCfgV7 agentCfgV7 runTest -- servers can be migrated in any order it "servers: next SMP v7, curr NTF v1; curr clients: v6/v1" $ runNtfTestCfg t cfgV7 ntfServerCfg agentCfg agentCfg runTest it "servers: curr SMP v6, next NTF v2; curr clients: v6/v1" $ runNtfTestCfg t cfg ntfServerCfgV2 agentCfg agentCfg runTest @@ -180,7 +181,7 @@ testNotificationToken APNSMockServer {apnsQ} = do NTActive <- checkNtfToken a tkn deleteNtfToken a tkn -- agent deleted this token - Left (CMD PROHIBITED) <- tryE $ checkNtfToken a tkn + Left (CMD PROHIBITED _) <- tryE $ checkNtfToken a tkn pure () (.->) :: J.Value -> J.Key -> ExceptT AgentErrorType IO ByteString @@ -243,7 +244,7 @@ testNtfTokenSecondRegistration APNSMockServer {apnsQ} = -- now the second token registration is verified verifyNtfToken a' tkn nonce' verification' -- the first registration is removed - Left (NTF AUTH) <- tryE $ checkNtfToken a tkn + Left (NTF _ AUTH) <- tryE $ checkNtfToken a tkn -- and the second is active NTActive <- checkNtfToken a' tkn pure () @@ -258,7 +259,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do atomically $ readTBQueue apnsQ liftIO $ sendApnsResponse APNSRespOk pure ntfData - -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server + -- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server threadDelay 1000000 withAgent 2 agentCfg initAgentServers testDB $ \a' -> -- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token, @@ -266,7 +267,7 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do withNtfServer t . runRight_ $ do verification <- ntfData .-> "verification" nonce <- C.cbNonce <$> ntfData .-> "nonce" - Left (NTF AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification + Left (NTF _ AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <- atomically $ readTBQueue apnsQ verification' <- ntfData' .-> "verification" @@ -333,6 +334,7 @@ testNtfTokenChangeServers t APNSMockServer {apnsQ} = getTestNtfTokenPort a >>= \port2 -> liftIO $ port2 `shouldBe` ntfTestPort2 -- but the token got updated killThread ntf withNtfServerOn t ntfTestPort2 $ runRight_ $ do + liftIO $ threadDelay 1000000 -- for notification server to reconnect tkn <- registerTestToken a "qwer" NMInstant apnsQ checkNtfToken a tkn >>= \r -> liftIO $ r `shouldBe` NTActive @@ -372,7 +374,7 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} alice@Agen pure (bobId, aliceId, nonce, message) -- alice client already has subscription for the connection - Left (CMD PROHIBITED) <- runExceptT $ getNotificationMessage alice nonce message + Left (CMD PROHIBITED _) <- runExceptT $ getNotificationMessage alice nonce message -- aliceNtf client doesn't have subscription and is allowed to get notification message withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 4bac4fb83..63466b9d7 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -5,8 +5,8 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -17,6 +17,7 @@ module AgentTests.SQLiteTests (storeTests) where import AgentTests.EqInstances () import Control.Concurrent.Async (concurrently_) +import Control.Concurrent.MVar import Control.Concurrent.STM import Control.Exception (SomeException) import Control.Monad (replicateM_) @@ -45,9 +46,9 @@ import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Protocol (SubscriptionMode (..), pattern VersionSMPC) import qualified Simplex.Messaging.Protocol as SMP @@ -88,7 +89,7 @@ removeStore db = do removeFile $ dbFilePath db where close :: SQLiteStore -> IO () - close st = mapM_ DB.close =<< atomically (tryTakeTMVar $ dbConnection st) + close st = mapM_ DB.close =<< tryTakeMVar (dbConnection st) storeTests :: Spec storeTests = do @@ -708,15 +709,15 @@ testGetNextRcvChunkToDownload st = do withTransaction st $ \db -> do Right Nothing <- getNextRcvChunkToDownload db xftpServer1 86400 - Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1" - Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True Left e <- getNextRcvChunkToDownload db xftpServer1 86400 show e `shouldContain` "ConversionFailed" DB.query_ db "SELECT rcv_file_id FROM rcv_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] - Right (Just RcvFileChunk {rcvFileEntityId}) <- getNextRcvChunkToDownload db xftpServer1 86400 + Right (Just (RcvFileChunk {rcvFileEntityId}, _)) <- getNextRcvChunkToDownload db xftpServer1 86400 rcvFileEntityId `shouldBe` fId2 testGetNextRcvFileToDecrypt :: SQLiteStore -> Expectation @@ -725,10 +726,10 @@ testGetNextRcvFileToDecrypt st = do withTransaction st $ \db -> do Right Nothing <- getNextRcvFileToDecrypt db 86400 - Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right _ <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 1" DB.execute_ db "UPDATE rcv_file_chunk_replicas SET replica_key = cast('bad' as blob) WHERE rcv_file_chunk_replica_id = 1" - Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) + Right fId2 <- createRcvFile db g 1 rcvFileDescr1 "filepath" "filepath" (CryptoFile "filepath" Nothing) True DB.execute_ db "UPDATE rcv_files SET status = 'received' WHERE rcv_file_id = 2" Left e <- getNextRcvFileToDecrypt db 86400 diff --git a/tests/CLITests.hs b/tests/CLITests.hs index 3bc48c5ce..1310665ee 100644 --- a/tests/CLITests.hs +++ b/tests/CLITests.hs @@ -57,7 +57,7 @@ smpServerTest :: Bool -> Bool -> IO () smpServerTest storeLog basicAuth = do -- init capture_ (withArgs (["init", "-y"] <> ["-l" | storeLog] <> ["--no-password" | not basicAuth]) $ smpServerCLI cfgPath logPath) - >>= (`shouldSatisfy` (("Server initialized, you can modify configuration in " <> cfgPath <> "/smp-server.ini") `isPrefixOf`)) + >>= (`shouldSatisfy` (("Server initialized, please provide additional server information in " <> cfgPath <> "/smp-server.ini") `isPrefixOf`)) 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" @@ -77,13 +77,13 @@ smpServerTest storeLog basicAuth = do let certPath = cfgPath "server.crt" oldCrt@X.Certificate {} <- XF.readSignedObject certPath >>= \case - [cert] -> pure . X.signedObject $ X.getSigned cert + [cert'] -> pure . X.signedObject $ X.getSigned cert' _ -> error "bad crt format" r' <- lines <$> capture_ (withArgs ["cert"] $ (100000 `timeout` smpServerCLI cfgPath logPath) `catchAll_` pure (Just ())) r' `shouldContain` ["Generated new server credentials"] newCrt <- XF.readSignedObject certPath >>= \case - [cert] -> pure . X.signedObject $ X.getSigned cert + [cert'] -> pure . X.signedObject $ X.getSigned cert' _ -> error "bad crt format after cert" X.certSignatureAlg oldCrt `shouldBe` X.certSignatureAlg newCrt X.certSubjectDN oldCrt `shouldBe` X.certSubjectDN newCrt diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index 996d2fed1..caab0637a 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} module CoreTests.BatchingTests (batchingTests) where @@ -11,6 +13,9 @@ import Crypto.Random (ChaChaDRG) import qualified Data.ByteString as B import Data.ByteString.Char8 (ByteString) import qualified Data.List.NonEmpty as L +import qualified Data.X509 as X +import qualified Data.X509.CertificateStore as XS +import qualified Data.X509.File as XF import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol @@ -276,12 +281,12 @@ randomSUB_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionSM randomSUB_ a v sessId = do g <- C.newRandom rId <- atomically $ C.randomBytes 24 g - corrId <- atomically $ CorrId <$> C.randomBytes 24 g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g (rKey, rpKey) <- atomically $ C.generateAuthKeyPair a g thAuth_ <- testTHandleAuth v g rKey let thParams = testTHandleParams v sessId - TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, rId, Cmd SRecipient SUB) - pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) corrId tForAuth + TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, rId, Cmd SRecipient SUB) + pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) nonce tForAuth randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg) randomSUBCmd = randomSUBCmd_ C.SEd25519 @@ -306,30 +311,36 @@ randomSEND_ :: (C.AlgorithmI a, C.AuthAlgorithm a) => C.SAlgorithm a -> VersionS randomSEND_ a v sessId len = do g <- C.newRandom sId <- atomically $ C.randomBytes 24 g - corrId <- atomically $ CorrId <$> C.randomBytes 3 g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g (sKey, spKey) <- atomically $ C.generateAuthKeyPair a g thAuth_ <- testTHandleAuth v g sKey msg <- atomically $ C.randomBytes len g let thParams = testTHandleParams v sessId - TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, sId, Cmd SSender $ SEND noMsgFlags msg) - pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) corrId tForAuth + TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, sId, Cmd SSender $ SEND noMsgFlags msg) + pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) nonce tForAuth -testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion +testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient testTHandleParams v sessionId = THandleParams { sessionId, blockSize = smpBlockSize, thVersion = v, + thServerVRange = supportedServerSMPRelayVRange, thAuth = Nothing, implySessId = v >= authCmdsSMPVersion, batch = True } -testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe THandleAuth) -testTHandleAuth v g (C.APublicAuthKey a k) = case a of +testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe (THandleAuth 'TClient)) +testTHandleAuth v g (C.APublicAuthKey a serverPeerPubKey) = case a of C.SX25519 | v >= authCmdsSMPVersion -> do - (_, privKey) <- atomically $ C.generateKeyPair g - pure $ Just THandleAuth {peerPubKey = k, privKey} + ca <- head <$> XS.readCertificates "tests/fixtures/ca.crt" + serverCert <- head <$> XS.readCertificates "tests/fixtures/server.crt" + serverKey <- head <$> XF.readKeyFile "tests/fixtures/server.key" + signKey <- either error pure $ C.x509ToPrivate (serverKey, []) >>= C.privKey @C.APrivateSignKey + (serverAuthPub, _) <- atomically $ C.generateKeyPair @'C.X25519 g + let serverCertKey = (X.CertificateChain [serverCert, ca], C.signX509 signKey $ C.toPubKey C.publicToX509 serverAuthPub) + pure $ Just THAuthClient {serverPeerPubKey, serverCertKey, sessSecret = Nothing} _ -> pure Nothing randomSENDCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> Int -> IO (PCTransmission ErrorType BrokerMsg) diff --git a/tests/CoreTests/ProtocolErrorTests.hs b/tests/CoreTests/ProtocolErrorTests.hs index 7b1a7b813..4466c4933 100644 --- a/tests/CoreTests/ProtocolErrorTests.hs +++ b/tests/CoreTests/ProtocolErrorTests.hs @@ -2,21 +2,20 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} module CoreTests.ProtocolErrorTests where -import qualified Data.ByteString.Char8 as B -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.FileTransfer.Transport (XFTPErrorType (..)) import Simplex.Messaging.Agent.Protocol +import qualified Simplex.Messaging.Agent.Protocol as Agent +import Simplex.Messaging.Client (ProxyClientError (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (CommandError (..), ErrorType (..)) +import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport (HandshakeError (..), TransportError (..)) import Simplex.RemoteControl.Types (RCErrorType (..)) import Test.Hspec @@ -26,17 +25,34 @@ import Test.QuickCheck protocolErrorTests :: Spec protocolErrorTests = modifyMaxSuccess (const 1000) $ do describe "errors parsing / serializing" $ do - it "should parse SMP protocol errors" . property $ \(err :: ErrorType) -> + it "should parse SMP protocol errors" . property . forAll possibleErrorType $ \err -> smpDecode (smpEncode err) == Right err - it "should parse SMP agent errors" . property $ \(err :: AgentErrorType) -> - errHasSpaces err - || strDecode (strEncode err) == Right err + it "should parse SMP agent errors" . property . forAll possibleAgentErrorType $ \err -> + strDecode (strEncode err) == Right err where - errHasSpaces = \case - BROKER srv (RESPONSE e) -> hasSpaces srv || hasSpaces e - BROKER srv _ -> hasSpaces srv + possibleErrorType :: Gen ErrorType + possibleErrorType = arbitrary >>= \e -> if skipErrorType e then discard else pure e + possibleAgentErrorType :: Gen AgentErrorType + possibleAgentErrorType = + arbitrary >>= \case + BROKER srv _ | hasSpaces srv -> discard + SMP srv e | hasSpaces srv || skipErrorType e -> discard + NTF srv e | hasSpaces srv || skipErrorType e -> discard + XFTP srv _ | hasSpaces srv -> discard + Agent.PROXY pxy srv _ | hasSpaces pxy || hasSpaces srv -> discard + Agent.PROXY _ _ (ProxyProtocolError e) | skipErrorType e -> discard + Agent.PROXY _ _ (ProxyUnexpectedResponse e) | hasUnicode e -> discard + Agent.PROXY _ _ (ProxyResponseError e) | skipErrorType e -> discard + ok -> pure ok + hasSpaces :: String -> Bool + hasSpaces = any (== ' ') + hasUnicode :: String -> Bool + hasUnicode = any (>= '\255') + skipErrorType = \case + SMP.PROXY (SMP.PROTOCOL e) -> skipErrorType e + SMP.PROXY (SMP.BROKER (UNEXPECTED s)) -> hasUnicode s + SMP.PROXY (SMP.BROKER (RESPONSE s)) -> hasUnicode s _ -> False - hasSpaces s = ' ' `B.elem` encodeUtf8 (T.pack s) deriving instance Generic AgentErrorType @@ -44,6 +60,8 @@ deriving instance Generic CommandErrorType deriving instance Generic ConnectionErrorType +deriving instance Generic ProxyClientError + deriving instance Generic BrokerErrorType deriving instance Generic SMPAgentError @@ -54,6 +72,8 @@ deriving instance Generic ErrorType deriving instance Generic CommandError +deriving instance Generic SMP.ProxyError + deriving instance Generic TransportError deriving instance Generic HandshakeError @@ -68,6 +88,8 @@ instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU +instance Arbitrary ProxyClientError where arbitrary = genericArbitraryU + instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU @@ -78,6 +100,8 @@ instance Arbitrary ErrorType where arbitrary = genericArbitraryU instance Arbitrary CommandError where arbitrary = genericArbitraryU +instance Arbitrary SMP.ProxyError where arbitrary = genericArbitraryU + instance Arbitrary TransportError where arbitrary = genericArbitraryU instance Arbitrary HandshakeError where arbitrary = genericArbitraryU diff --git a/tests/CoreTests/TRcvQueuesTests.hs b/tests/CoreTests/TRcvQueuesTests.hs index 91722228b..2b0009344 100644 --- a/tests/CoreTests/TRcvQueuesTests.hs +++ b/tests/CoreTests/TRcvQueuesTests.hs @@ -22,10 +22,13 @@ tRcvQueuesTests = do describe "connection API" $ do it "hasConn" hasConnTest it "hasConn, batch add" hasConnTestBatch + it "hasConn, batch idempotent" batchIdempotentTest it "deleteConn" deleteConnTest describe "session API" $ do it "getSessQueues" getSessQueuesTest it "getDelSessQueues" getDelSessQueuesTest + describe "queue transfer" $ do + it "getDelSessQueues-batchAddQueues preserves total length" removeSubsTest checkDataInvariant :: RQ.TRcvQueues -> IO Bool checkDataInvariant trq = atomically $ do @@ -62,6 +65,19 @@ hasConnTestBatch = do atomically (RQ.hasConn "c3" trq) `shouldReturn` True atomically (RQ.hasConn "nope" trq) `shouldReturn` False +batchIdempotentTest :: IO () +batchIdempotentTest = do + trq <- atomically RQ.empty + let qs = [dummyRQ 0 "smp://1234-w==@alpha" "c1", dummyRQ 0 "smp://1234-w==@alpha" "c2", dummyRQ 0 "smp://1234-w==@beta" "c3"] + atomically $ RQ.batchAddQueues trq qs + checkDataInvariant trq `shouldReturn` True + qs' <- readTVarIO $ RQ.getRcvQueues trq + cs' <- readTVarIO $ RQ.getConnections trq + atomically $ RQ.batchAddQueues trq qs + checkDataInvariant trq `shouldReturn` True + readTVarIO (RQ.getRcvQueues trq) `shouldReturn` qs' + fmap L.nub <$> readTVarIO (RQ.getConnections trq) `shouldReturn`cs' -- connections get duplicated, but that doesn't appear to affect anybody + deleteConnTest :: IO () deleteConnTest = do trq <- atomically RQ.empty @@ -121,6 +137,40 @@ getDelSessQueuesTest = do atomically (RQ.hasConn "c3" trq) `shouldReturn` True atomically (RQ.hasConn "c4" trq) `shouldReturn` True +removeSubsTest :: IO () +removeSubsTest = do + aq <- atomically RQ.empty + let qs = + [ dummyRQ 0 "smp://1234-w==@alpha" "c1", + dummyRQ 0 "smp://1234-w==@alpha" "c2", + dummyRQ 0 "smp://1234-w==@beta" "c3", + dummyRQ 1 "smp://1234-w==@beta" "c4" + ] + atomically $ RQ.batchAddQueues aq qs + + pq <- atomically RQ.empty + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@alpha", Nothing) aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "non-existent") aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@localhost", Nothing) aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + + atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "c3") aq >>= RQ.batchAddQueues pq . fst + atomically (totalSize aq pq) `shouldReturn` (4, 4) + +totalSize :: RQ.TRcvQueues -> RQ.TRcvQueues -> STM (Int, Int) +totalSize a b = do + qsizeA <- M.size <$> readTVar (RQ.getRcvQueues a) + qsizeB <- M.size <$> readTVar (RQ.getRcvQueues b) + csizeA <- M.size <$> readTVar (RQ.getConnections a) + csizeB <- M.size <$> readTVar (RQ.getConnections b) + pure (qsizeA + qsizeB, csizeA + csizeB) + dummyRQ :: UserId -> SMPServer -> ConnId -> RcvQueue dummyRQ userId server connId = RcvQueue diff --git a/tests/CoreTests/UtilTests.hs b/tests/CoreTests/UtilTests.hs index 9e413e838..2254ecafd 100644 --- a/tests/CoreTests/UtilTests.hs +++ b/tests/CoreTests/UtilTests.hs @@ -2,11 +2,11 @@ module CoreTests.UtilTests where +import AgentTests.FunctionalAPITests () import Control.Exception (Exception, SomeException, throwIO) import Control.Monad.Except import Control.Monad.IO.Class import Data.IORef -import Simplex.Messaging.Client.Agent () import Simplex.Messaging.Util import Test.Hspec import qualified UnliftIO.Exception as UE diff --git a/tests/CoreTests/VersionRangeTests.hs b/tests/CoreTests/VersionRangeTests.hs index cef556376..ff53cc6ca 100644 --- a/tests/CoreTests/VersionRangeTests.hs +++ b/tests/CoreTests/VersionRangeTests.hs @@ -6,6 +6,7 @@ module CoreTests.VersionRangeTests where +import Data.Word (Word16) import GHC.Generics (Generic) import Generic.Random (genericArbitraryU) import Simplex.Messaging.Version @@ -38,6 +39,28 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do (vr 1 3, vr 2 3) `compatible` Just (Version 3) (vr 1 3, vr 2 4) `compatible` Just (Version 3) (vr 1 2, vr 3 4) `compatible` Nothing + it "should choose mutually compatible version range (range intersection)" $ do + (vr 1 1, vr 1 1) `compatibleVR` Just (vr 1 1) + (vr 1 1, vr 1 2) `compatibleVR` Just (vr 1 1) + (vr 1 2, vr 1 2) `compatibleVR` Just (vr 1 2) + (vr 1 2, vr 2 3) `compatibleVR` Just (vr 2 2) + (vr 1 3, vr 2 3) `compatibleVR` Just (vr 2 3) + (vr 1 3, vr 2 4) `compatibleVR` Just (vr 2 3) + (vr 1 2, vr 3 4) `compatibleVR` Nothing + it "should choose compatible version range with changed max version (capped range)" $ do + (vr 1 1, 1) `compatibleVR'` Just (vr 1 1) + (vr 1 1, 2) `compatibleVR'` Nothing + (vr 1 2, 2) `compatibleVR'` Just (vr 1 2) + (vr 1 2, 3) `compatibleVR'` Nothing + (vr 1 3, 2) `compatibleVR'` Just (vr 1 2) + (vr 1 3, 3) `compatibleVR'` Just (vr 1 3) + (vr 1 3, 4) `compatibleVR'` Nothing + (vr 2 3, 1) `compatibleVR'` Nothing + (vr 2 3, 2) `compatibleVR'` Just (vr 2 2) + (vr 2 3, 3) `compatibleVR'` Just (vr 2 3) + (vr 2 4, 1) `compatibleVR'` Nothing + (vr 2 4, 3) `compatibleVR'` Just (vr 2 3) + (vr 2 4, 4) `compatibleVR'` Just (vr 2 4) it "should check if version is compatible" $ do isCompatible @T (Version 1) (vr 1 2) `shouldBe` True isCompatible @T (Version 2) (vr 1 2) `shouldBe` True @@ -63,3 +86,16 @@ versionRangeTests = modifyMaxSuccess (const 1000) $ do case compatibleVersion vr1 vr2 of Just (Compatible v') -> Just v' `shouldBe` v Nothing -> Nothing `shouldBe` v + compatibleVR :: (VersionRange T, VersionRange T) -> Maybe (VersionRange T) -> Expectation + (vr1, vr2) `compatibleVR` vr' = do + (vr1, vr2) `checkCompatibleVR` vr' + (vr2, vr1) `checkCompatibleVR` vr' + (vr1, vr2) `checkCompatibleVR` vr' = + case compatibleVRange vr1 vr2 of + Just (Compatible vr'') -> Just vr'' `shouldBe` vr' + Nothing -> Nothing `shouldBe` vr' + compatibleVR' :: (VersionRange T, Word16) -> Maybe (VersionRange T) -> Expectation + (vr1, v2) `compatibleVR'` vr' = + case compatibleVRange' vr1 (Version v2) of + Just (Compatible vr'') -> Just vr'' `shouldBe` vr' + Nothing -> Nothing `shouldBe` vr' diff --git a/tests/NtfClient.hs b/tests/NtfClient.hs index 26981628f..bd8cee771 100644 --- a/tests/NtfClient.hs +++ b/tests/NtfClient.hs @@ -36,6 +36,7 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Notifications.Protocol (NtfResponse) import Simplex.Messaging.Notifications.Server (runNtfServerBlocking) import Simplex.Messaging.Notifications.Server.Env +import qualified Simplex.Messaging.Notifications.Server.Env as Env import Simplex.Messaging.Notifications.Server.Push.APNS import Simplex.Messaging.Notifications.Server.Push.APNS.Internal import Simplex.Messaging.Notifications.Transport @@ -45,6 +46,7 @@ import Simplex.Messaging.Transport.Client import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), http2TLSParams) import Simplex.Messaging.Transport.HTTP2.Server import Simplex.Messaging.Transport.Server +import qualified Simplex.Messaging.Transport.Server as Server import Simplex.Messaging.Version (mkVersionRange) import Test.Hspec import UnliftIO.Async @@ -70,13 +72,11 @@ testKeyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=" ntfTestStoreLogFile :: FilePath ntfTestStoreLogFile = "tests/tmp/ntf-server-store.log" -testNtfClient :: Transport c => (THandleNTF c -> IO a) -> IO a +testNtfClient :: Transport c => (THandleNTF c 'TClient -> IO a) -> IO a testNtfClient client = do Right host <- pure $ chooseTransportHost defaultNetworkConfig testHost - runTransportClient defaultTransportClientConfig Nothing host ntfTestPort (Just testKeyHash) $ \h -> do - g <- C.newRandom - ks <- atomically $ C.generateKeyPair g - runExceptT (ntfClientHandshake h ks testKeyHash supportedClientNTFVRange) >>= \case + runTransportClient defaultTransportClientConfig Nothing host ntfTestPort (Just testKeyHash) $ \h -> + runExceptT (ntfClientHandshake h testKeyHash supportedClientNTFVRange) >>= \case Right th -> client th Left e -> error $ show e @@ -89,7 +89,7 @@ ntfServerCfg = clientQSize = 1, subQSize = 1, pushQSize = 1, - smpAgentCfg = defaultSMPClientAgentConfig, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 0}, apnsConfig = defaultAPNSPushClientConfig { apnsPort = apnsTestPort, @@ -115,8 +115,11 @@ ntfServerCfgV2 :: NtfServerConfig ntfServerCfgV2 = ntfServerCfg { ntfServerVRange = mkVersionRange initialNTFVersion authBatchCmdsNTFVersion, - smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}} + smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion}}, + Env.transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedNTFHandshakes} } + where + smpAgentCfg' = smpAgentCfg ntfServerCfg withNtfServerStoreLog :: ATransport -> (ThreadId -> IO a) -> IO a withNtfServerStoreLog t = withNtfServerCfg ntfServerCfg {storeLogFile = Just ntfTestStoreLogFile, transports = [(ntfTestPort, t)]} @@ -139,7 +142,7 @@ withNtfServerOn t port' = withNtfServerThreadOn t port' . const withNtfServer :: ATransport -> IO a -> IO a withNtfServer t = withNtfServerOn t ntfTestPort -runNtfTest :: forall c a. Transport c => (THandleNTF c -> IO a) -> IO a +runNtfTest :: forall c a. Transport c => (THandleNTF c 'TClient -> IO a) -> IO a runNtfTest test = withNtfServer (transport @c) $ testNtfClient test ntfServerTest :: @@ -150,7 +153,7 @@ ntfServerTest :: IO (Maybe TransmissionAuth, ByteString, ByteString, NtfResponse) ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h where - tPut' :: THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () + tPut' :: THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp) [Right ()] <- tPut h [Right (sig, t')] @@ -159,7 +162,7 @@ ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h [(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h pure (Nothing, corrId, qId, cmd) -ntfTest :: Transport c => TProxy c -> (THandleNTF c -> IO ()) -> Expectation +ntfTest :: Transport c => TProxy c -> (THandleNTF c 'TClient -> IO ()) -> Expectation ntfTest _ test' = runNtfTest test' `shouldReturn` () data APNSMockRequest = APNSMockRequest diff --git a/tests/NtfServerTests.hs b/tests/NtfServerTests.hs index e7e2018c2..e5f096eef 100644 --- a/tests/NtfServerTests.hs +++ b/tests/NtfServerTests.hs @@ -6,8 +6,8 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} module NtfServerTests where @@ -72,13 +72,13 @@ pattern RespNtf corrId queueId command <- (_, _, (corrId, queueId, Right command deriving instance Eq NtfResponse -sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) +sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) sendRecvNtf h@THandle {params} (sgn, corrId, qId, cmd) = do let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (sgn, tToSend) tGet1 h -signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) +signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse) signSendRecvNtf h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (authorize tForAuth, tToSend) diff --git a/tests/SMPAgentClient.hs b/tests/SMPAgentClient.hs index 59370e654..aee3c8cb0 100644 --- a/tests/SMPAgentClient.hs +++ b/tests/SMPAgentClient.hs @@ -20,7 +20,8 @@ import qualified Database.SQLite.Simple as SQL import Network.Socket (ServiceName) import NtfClient (ntfTestPort) import SMPClient - ( serverBracket, + ( proxyVRange, + serverBracket, testKeyHash, testPort, testPort2, @@ -34,7 +35,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Server (runSMPAgentBlocking) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), SQLiteStore (dbNew)) import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction') -import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultSMPClientConfig, defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, chooseTransportHost, defaultNetworkConfig, defaultSMPClientConfig) import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig) import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth) @@ -198,6 +199,10 @@ initAgentServers = initAgentServers2 :: InitialAgentServers initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]} +initAgentServersProxy :: SMPProxyMode -> SMPProxyFallback -> InitialAgentServers +initAgentServersProxy smpProxyMode smpProxyFallback = + initAgentServers {netCfg = (netCfg initAgentServers) {smpProxyMode, smpProxyFallback}} + agentCfg :: AgentConfig agentCfg = defaultAgentConfig @@ -207,6 +212,7 @@ agentCfg = smpCfg = defaultSMPClientConfig {qSize = 1, defaultTransport = (testPort, transport @TLS), networkConfig}, ntfCfg = defaultNTFClientConfig {qSize = 1, defaultTransport = (ntfTestPort, transport @TLS), networkConfig}, reconnectInterval = fastRetryInterval, + persistErrorInterval = 1, xftpNotifyErrsOnRetry = False, ntfWorkerDelay = 100, ntfSMPWorkerDelay = 100, @@ -215,7 +221,10 @@ agentCfg = certificateFile = "tests/fixtures/server.crt" } where - networkConfig = defaultNetworkConfig {tcpConnectTimeout = 3_000_000, tcpTimeout = 2_000_000} + networkConfig = defaultNetworkConfig {tcpConnectTimeout = 1_000_000, tcpTimeout = 2_000_000} + +agentProxyCfg :: AgentConfig +agentProxyCfg = agentCfg {smpCfg = (smpCfg agentCfg) {serverVRange = proxyVRange}} fastRetryInterval :: RetryInterval fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 3e5e9d2ce..6bc36c29a 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -16,7 +16,8 @@ import Control.Monad.Except (runExceptT) import Data.ByteString.Char8 (ByteString) import Data.List.NonEmpty (NonEmpty) import Network.Socket -import Simplex.Messaging.Client (chooseTransportHost, defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..), chooseTransportHost, defaultNetworkConfig) +import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Protocol @@ -24,8 +25,10 @@ import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client +import qualified Simplex.Messaging.Transport.Client as Client import Simplex.Messaging.Transport.Server -import Simplex.Messaging.Version (mkVersionRange) +import qualified Simplex.Messaging.Transport.Server as Server +import Simplex.Messaging.Version import System.Environment (lookupEnv) import System.Info (os) import Test.Hspec @@ -56,6 +59,9 @@ testStoreLogFile2 = "tests/tmp/smp-server-store.log.2" testStoreMsgsFile :: FilePath testStoreMsgsFile = "tests/tmp/smp-server-messages.log" +testStoreMsgsFile2 :: FilePath +testStoreMsgsFile2 = "tests/tmp/smp-server-messages.log.2" + testServerStatsBackupFile :: FilePath testServerStatsBackupFile = "tests/tmp/smp-server-stats.log" @@ -67,18 +73,25 @@ xit'' d t = do ci <- runIO $ lookupEnv "CI" (if ci == Just "true" then skip "skipped on CI" . it d else it d) t -testSMPClient :: Transport c => (THandleSMP c -> IO a) -> IO a +testSMPClient :: Transport c => (THandleSMP c 'TClient -> IO a) -> IO a testSMPClient = testSMPClientVR supportedClientSMPRelayVRange -testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c -> IO a) -> IO a +testSMPClientVR :: Transport c => VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a testSMPClientVR vr client = do Right useHost <- pure $ chooseTransportHost defaultNetworkConfig testHost - runTransportClient defaultTransportClientConfig Nothing useHost testPort (Just testKeyHash) $ \h -> do - g <- C.newRandom - ks <- atomically $ C.generateKeyPair g - runExceptT (smpClientHandshake h ks testKeyHash vr) >>= \case + testSMPClient_ useHost testPort vr client + +testSMPClient_ :: Transport c => TransportHost -> ServiceName -> VersionRangeSMP -> (THandleSMP c 'TClient -> IO a) -> IO a +testSMPClient_ host port vr client = do + let tcConfig = defaultTransportClientConfig {Client.alpn = clientALPN} + runTransportClient tcConfig Nothing host port (Just testKeyHash) $ \h -> + runExceptT (smpClientHandshake h Nothing testKeyHash vr) >>= \case Right th -> client th Left e -> error $ show e + where + clientALPN + | authCmdsSMPVersion `isCompatible` vr = Just supportedSMPHandshakes + | otherwise = Nothing cfg :: ServerConfig cfg = @@ -106,13 +119,33 @@ cfg = privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt", smpServerVRange = supportedServerSMPRelayVRange, - transportConfig = defaultTransportServerConfig, - controlPort = Nothing + transportConfig = defaultTransportServerConfig {Server.alpn = Just supportedSMPHandshakes}, + controlPort = Nothing, + smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 1}, -- seconds + allowSMPProxy = False, + serverClientConcurrency = 2, + information = Nothing } cfgV7 :: ServerConfig cfgV7 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} +cfgV8 :: ServerConfig +cfgV8 = cfg {smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} + +proxyCfg :: ServerConfig +proxyCfg = + cfgV7 + { allowSMPProxy = True, + smpServerVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, + smpAgentCfg = smpAgentCfg' {smpCfg = (smpCfg smpAgentCfg') {serverVRange = proxyVRange, agreeSecret = True}} + } + where + smpAgentCfg' = smpAgentCfg cfgV7 + +proxyVRange :: VersionRangeSMP +proxyVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion + withSmpServerStoreMsgLogOn :: HasCallStack => ATransport -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn t = withSmpServerConfigOn t cfg {storeLogFile = Just testStoreLogFile, storeMsgsFile = Just testStoreMsgsFile, serverStatsBackupFile = Just testServerStatsBackupFile} @@ -150,16 +183,19 @@ withSmpServer t = withSmpServerOn t testPort withSmpServerV7 :: HasCallStack => ATransport -> IO a -> IO a withSmpServerV7 t = withSmpServerConfigOn t cfgV7 testPort . const -runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c -> IO a) -> IO a +withSmpServerProxy :: HasCallStack => ATransport -> IO a -> IO a +withSmpServerProxy t = withSmpServerConfigOn t proxyCfg testPort . const + +runSmpTest :: forall c a. (HasCallStack, Transport c) => (HasCallStack => THandleSMP c 'TClient -> IO a) -> IO a runSmpTest test = withSmpServer (transport @c) $ testSMPClient test -runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a +runSmpTestN :: forall c a. (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a runSmpTestN = runSmpTestNCfg cfg supportedClientSMPRelayVRange -runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c] -> IO a) -> IO a +runSmpTestNCfg :: forall c a. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO a) -> IO a runSmpTestNCfg srvCfg clntVR nClients test = withSmpServerConfigOn (transport @c) srvCfg testPort $ \_ -> run nClients [] where - run :: Int -> [THandleSMP c] -> IO a + run :: Int -> [THandleSMP c 'TClient] -> IO a run 0 hs = test hs run n hs = testSMPClientVR clntVR $ \h -> run (n - 1) (h : hs) @@ -171,7 +207,7 @@ smpServerTest :: IO (Maybe TransmissionAuth, ByteString, ByteString, BrokerMsg) smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h where - tPut' :: THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () + tPut' :: THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, smp) -> IO () tPut' h@THandle {params = THandleParams {sessionId, implySessId}} (sig, corrId, queueId, smp) = do let t' = if implySessId then smpEncode (corrId, queueId, smp) else smpEncode (sessionId, corrId, queueId, smp) [Right ()] <- tPut h [Right (sig, t')] @@ -180,33 +216,33 @@ smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h [(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h pure (Nothing, corrId, qId, cmd) -smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> IO ()) -> Expectation +smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> IO ()) -> Expectation smpTest _ test' = runSmpTest test' `shouldReturn` () -smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c] -> IO ()) -> Expectation +smpTestN :: (HasCallStack, Transport c) => Int -> (HasCallStack => [THandleSMP c 'TClient] -> IO ()) -> Expectation smpTestN n test' = runSmpTestN n test' `shouldReturn` () -smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest2 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest2 = smpTest2Cfg cfg supportedClientSMPRelayVRange -smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest2Cfg :: forall c. (HasCallStack, Transport c) => ServerConfig -> VersionRangeSMP -> TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest2Cfg srvCfg clntVR _ test' = runSmpTestNCfg srvCfg clntVR 2 _test `shouldReturn` () where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2] = test' h1 h2 _test _ = error "expected 2 handles" -smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest3 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest3 _ test' = smpTestN 3 _test where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2, h3] = test' h1 h2 h3 _test _ = error "expected 3 handles" -smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c -> THandleSMP c -> THandleSMP c -> THandleSMP c -> IO ()) -> Expectation +smpTest4 :: forall c. (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> THandleSMP c 'TClient -> IO ()) -> Expectation smpTest4 _ test' = smpTestN 4 _test where - _test :: HasCallStack => [THandleSMP c] -> IO () + _test :: HasCallStack => [THandleSMP c 'TClient] -> IO () _test [h1, h2, h3, h4] = test' h1 h2 h3 h4 _test _ = error "expected 4 handles" diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs new file mode 100644 index 000000000..036c8b203 --- /dev/null +++ b/tests/SMPProxyTests.hs @@ -0,0 +1,386 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module SMPProxyTests where + +import AgentTests.EqInstances () +import AgentTests.FunctionalAPITests +import Control.Concurrent (ThreadId) +import Control.Logger.Simple +import Control.Monad (forM, forM_, forever) +import Control.Monad.Trans.Except (ExceptT, runExceptT) +import Data.ByteString.Char8 (ByteString) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import SMPAgentClient +import SMPClient +import ServerTests (decryptMsgV3, sendRecv) +import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMessage) +import qualified Simplex.Messaging.Agent as A +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..)) +import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, REQ) +import qualified Simplex.Messaging.Agent.Protocol as A +import Simplex.Messaging.Client +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOn) +import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Server.Env.STM (ServerConfig (..)) +import Simplex.Messaging.Transport +import Simplex.Messaging.Util (bshow, tshow) +import Simplex.Messaging.Version (mkVersionRange) +import System.FilePath (splitExtensions) +import Test.Hspec +import UnliftIO +import Util + +smpProxyTests :: Spec +smpProxyTests = do + describe "server configuration" $ do + it "refuses proxy handshake unless enabled" testNoProxy + it "checks basic auth in proxy requests" testProxyAuth + describe "proxy requests" $ do + describe "bad relay URIs" $ do + xit "host not resolved" todo + xit "when SMP port blackholed" todo + xit "no SMP service at host/port" todo + xit "bad SMP fingerprint" todo + xit "batching proxy requests" todo + describe "deliver message via SMP proxy" $ do + let srv1 = SMPServer testHost testPort testKeyHash + srv2 = SMPServer testHost testPort2 testKeyHash + describe "client API" $ do + let maxLen = maxMessageLength sendingProxySMPVersion + describe "one server" $ do + it "deliver via proxy" . oneServer $ do + deliverMessageViaProxy srv1 srv1 C.SEd448 "hello 1" "hello 2" + describe "two servers" $ do + let proxyServ = srv1 + relayServ = srv2 + (msg1, msg2) <- runIO $ do + g <- C.newRandom + atomically $ (,) <$> C.randomBytes maxLen g <*> C.randomBytes maxLen g + it "deliver via proxy" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd448 "hello 1" "hello 2" + it "max message size, Ed448 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd448 msg1 msg2 + it "max message size, Ed25519 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg1 msg2 + it "max message size, X25519 keys" . twoServersFirstProxy $ + deliverMessageViaProxy proxyServ relayServ C.SX25519 msg1 msg2 + describe "stress test 1k" $ do + let deliver n = deliverMessagesViaProxy srv1 srv2 C.SEd448 [] (map bshow [1 :: Int .. n]) + it "1x1000" . twoServersFirstProxy $ deliver 1000 + it "5x200" . twoServersFirstProxy $ 5 `inParrallel` deliver 200 + it "10x100" . twoServersFirstProxy $ 10 `inParrallel` deliver 100 + xdescribe "stress test 10k" $ do + let deliver n = deliverMessagesViaProxy srv1 srv2 C.SEd448 [] (map bshow [1 :: Int .. n]) + it "1x10000" . twoServersFirstProxy $ deliver 10000 + it "5x2000" . twoServersFirstProxy $ 5 `inParrallel` deliver 2000 + it "10x1000" . twoServersFirstProxy $ 10 `inParrallel` deliver 1000 + it "100x100 N1" . twoServersFirstProxy $ withNumCapabilities 1 $ 100 `inParrallel` deliver 100 + it "100x100 N4 C1" . twoServersNoConc $ withNumCapabilities 4 $ 100 `inParrallel` deliver 100 + it "100x100 N4 C2" . twoServersFirstProxy $ withNumCapabilities 4 $ 100 `inParrallel` deliver 100 + it "100x100 N4 C16" . twoServersMoreConc $ withNumCapabilities 4 $ 100 `inParrallel` deliver 100 + it "100x100 N" . twoServersFirstProxy $ withNCPUCapabilities $ 100 `inParrallel` deliver 100 + it "500x20" . twoServersFirstProxy $ 500 `inParrallel` deliver 20 + describe "agent API" $ do + describe "one server" $ do + it "always via proxy" . oneServer $ + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + it "without proxy" . oneServer $ + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" + describe "two servers" $ do + it "always via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" + it "both via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" + it "first via proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + it "without proxy" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" + it "first via proxy for unknown" . twoServers $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $ + agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" + it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $ + agentViaProxyVersionError + it "retries sending when destination or proxy relay is offline" $ + agentViaProxyRetryOffline + describe "stress test 1k" $ do + let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) + it "2 agents, 250 messages" . oneServer $ deliver 2 250 + it "5 agents, 10 pairs, 50 messages, N1" . oneServer . withNumCapabilities 1 $ deliver 5 50 + it "5 agents, 10 pairs, 50 messages. N4" . oneServer . withNumCapabilities 4 $ deliver 5 50 + xdescribe "stress test 10k" $ do + let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs]) + it "25 agents, 300 pairs, 17 messages" . oneServer . withNumCapabilities 4 $ deliver 25 17 + where + oneServer = withSmpServerConfigOn (transport @TLS) proxyCfg {msgQueueQuota = 128} testPort . const + twoServers = twoServers_ proxyCfg proxyCfg + twoServersFirstProxy = twoServers_ proxyCfg cfgV8 {msgQueueQuota = 128} + twoServersMoreConc = twoServers_ proxyCfg {serverClientConcurrency = 128} cfgV8 {msgQueueQuota = 128} + twoServersNoConc = twoServers_ proxyCfg {serverClientConcurrency = 1} cfgV8 {msgQueueQuota = 128} + twoServers_ cfg1 cfg2 runTest = + withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> + withSmpServerConfigOn (transport @TLS) cfg2 testPort2 $ const runTest + +deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> ByteString -> ByteString -> IO () +deliverMessageViaProxy proxyServ relayServ alg msg msg' = deliverMessagesViaProxy proxyServ relayServ alg [msg] [msg'] + +deliverMessagesViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> [ByteString] -> [ByteString] -> IO () +deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do + g <- C.newRandom + -- set up proxy + pc' <- getProtocolClient g (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} Nothing (\_ -> pure ()) + pc <- either (fail . show) pure pc' + THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc + -- set up relay + msgQ <- newTBQueueIO 1024 + rc' <- getProtocolClient g (2, relayServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange batchCmdsSMPVersion authCmdsSMPVersion} (Just msgQ) (\_ -> pure ()) + rc <- either (fail . show) pure rc' + -- prepare receiving queue + (rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g + (rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g + QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe + let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv + -- get proxy session + sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") + sess <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct") + sess0 `shouldBe` sess + -- send via proxy to unsecured queue + forM_ unsecuredMsgs $ \msg -> do + runExceptT' (proxySMPMessage pc sess Nothing sndId noMsgFlags msg) `shouldReturn` Right () + runExceptT' (proxySMPMessage pc sess {prSessionId = "bad session"} Nothing sndId noMsgFlags msg) `shouldReturn` Left (ProxyProtocolError $ SMP.PROXY SMP.NO_SESSION) + -- receive 1 + (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId, msgBody = EncRcvMsgBody encBody})))]) <- atomically $ readTBQueue msgQ + dec msgId encBody `shouldBe` Right msg + runExceptT' $ ackSMPMessage rc rPriv rcvId msgId + -- secure queue + (sPub, sPriv) <- atomically $ C.generateAuthKeyPair alg g + runExceptT' $ secureSMPQueue rc rPriv rcvId sPub + -- send via proxy to secured queue + waitSendRecv + ( forM_ securedMsgs $ \msg' -> + runExceptT' (proxySMPMessage pc sess (Just sPriv) sndId noMsgFlags msg') `shouldReturn` Right () + ) + ( forM_ securedMsgs $ \msg' -> do + (_tSess, _v, _sid, [(_entId, STEvent (Right (SMP.MSG RcvMessage {msgId = msgId', msgBody = EncRcvMsgBody encBody'})))]) <- atomically $ readTBQueue msgQ + dec msgId' encBody' `shouldBe` Right msg' + runExceptT' $ ackSMPMessage rc rPriv rcvId msgId' + ) + +agentDeliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => (NonEmpty SMPServer, SMPProxyMode, Bool) -> (NonEmpty SMPServer, SMPProxyMode, Bool) -> C.SAlgorithm a -> ByteString -> ByteString -> IO () +agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, bViaProxy) alg msg1 msg2 = + withAgent 1 aCfg (servers aTestCfg) testDB $ \alice -> + withAgent 2 aCfg (servers bTestCfg) testDB2 $ \bob -> runRight_ $ do + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice + liftIO $ pqSup' `shouldBe` PQSupportOn + allowConnection alice bobId confId "alice's connInfo" + let pqEnc = CR.PQEncOn + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + -- message IDs 1 to 3 (or 1 to 4 in v1) get assigned to control messages, so first MSG is assigned ID 4 + let aProxySrv = if aViaProxy then Just $ L.head aSrvs else Nothing + 1 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + get alice ##> ("", bobId, A.SENT (baseId + 1) aProxySrv) + 2 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg2 + get alice ##> ("", bobId, A.SENT (baseId + 2) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 1) Nothing + get bob =##> \case ("", c, Msg' _ pq msg2') -> c == aliceId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage bob aliceId (baseId + 2) Nothing + let bProxySrv = if bViaProxy then Just $ L.head bSrvs else Nothing + 3 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg1 + get bob ##> ("", aliceId, A.SENT (baseId + 3) bProxySrv) + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg1') -> c == bobId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage alice bobId (baseId + 3) Nothing + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 4) Nothing + where + baseId = 3 + msgId = subtract baseId . fst + aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg} + servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + +agentDeliverMessagesViaProxyConc :: [NonEmpty SMPServer] -> [MsgBody] -> IO () +agentDeliverMessagesViaProxyConc agentServers msgs = + withAgents $ \agents -> do + let pairs = combinations 2 agents + logNote $ "Pairing " <> tshow (length agents) <> " agents into " <> tshow (length pairs) <> " connections" + connections <- forM pairs $ \case + [a, b] -> prePair a b + _ -> error "agents must be paired" + logNote "Running..." + mapConcurrently_ run connections + where + withAgents :: ([AgentClient] -> IO ()) -> IO () + withAgents action = go [] (zip [1 :: Int ..] agentServers) + where + go agents = \case + [] -> action agents + (aId, aSrvs) : next -> withAgent aId aCfg (servers aSrvs) (dbPrefix <> show aId <> dbSuffix) $ \a -> (a : agents) `go` next + (dbPrefix, dbSuffix) = splitExtensions testDB + -- agent connections have to be set up in advance + -- otherwise the CONF messages would get mixed with MSG + prePair alice bob = do + (bobId, qInfo) <- runExceptT' $ A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- runExceptT' $ A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + confId <- + get alice >>= \case + ("", _, A.CONF confId pqSup' _ "bob's connInfo") -> do + pqSup' `shouldBe` PQSupportOn + pure confId + huh -> fail $ show huh + runExceptT' $ allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + pure (alice, bobId, bob, aliceId) + -- stream messages in opposite directions, while getting deliveries and sending ACKs + run (alice, bobId, bob, aliceId) = do + aSender <- async $ forM_ msgs $ runExceptT' . A.sendMessage alice bobId pqEnc noMsgFlags + bRecipient <- + async $ + forever $ + get bob >>= \case + ("", _, A.SENT _ _) -> pure () + ("", _, Msg' mId' _ _) -> runExceptT' $ ackMessage alice bobId mId' Nothing + huh -> fail (show huh) + bSender <- async $ forM_ msgs $ runExceptT' . A.sendMessage bob aliceId pqEnc noMsgFlags + aRecipient <- + async $ + forever $ + get alice >>= \case + ("", _, A.SENT _ _) -> pure () + ("", _, Msg' mId' _ _) -> runExceptT' $ ackMessage alice bobId mId' Nothing + huh -> fail (show huh) + logDebug "run waiting..." + a2b <- async $ (waitCatch aSender >>= either throwIO pure) `finally` cancel bRecipient -- stopped sender cancels paired recipient loop + b2a <- async $ (waitCatch bSender >>= either throwIO pure) `finally` cancel aRecipient + waitEitherCatch a2b b2a >>= \case + Right (Right ()) -> wait b2a + Right (Left e) -> cancel bSender >> throwIO e + Left (Right ()) -> wait a2b + Left (Left e) -> cancel aSender >> throwIO e + logDebug "run finished" + pqEnc = CR.PQEncOn + aCfg = agentProxyCfg {sndAuthAlg = C.AuthAlg C.SEd448, rcvAuthAlg = C.AuthAlg C.SEd448} + servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers $ L.map noAuthSrv srvs} + +agentViaProxyVersionError :: IO () +agentViaProxyVersionError = + withAgent 1 agentProxyCfg (servers [SMPServer testHost testPort testKeyHash]) testDB $ \alice -> do + Left (A.BROKER _ (TRANSPORT TEVersion)) <- + withAgent 2 agentProxyCfg (servers [SMPServer testHost testPort2 testKeyHash]) testDB2 $ \bob -> runExceptT $ do + (_bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + pure () + where + servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs} + +agentViaProxyRetryOffline :: IO () +agentViaProxyRetryOffline = do + let srv1 = SMPServer testHost testPort testKeyHash + srv2 = SMPServer testHost testPort2 testKeyHash + msg1 = "hello 1" + msg2 = "hello 2" + aProxySrv = Just srv1 + bProxySrv = Just srv2 + withAgent 1 aCfg (servers srv1) testDB $ \alice -> + withAgent 2 aCfg (servers srv2) testDB2 $ \bob -> do + let pqEnc = CR.PQEncOn + withServer $ \_ -> do + (aliceId, bobId) <- withServer2 $ \_ -> runRight $ do + (bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe + aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + ("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice + liftIO $ pqSup' `shouldBe` PQSupportOn + allowConnection alice bobId confId "alice's connInfo" + get alice ##> ("", bobId, A.CON pqEnc) + get bob ##> ("", aliceId, A.INFO PQSupportOn "alice's connInfo") + get bob ##> ("", aliceId, A.CON pqEnc) + 1 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + get alice ##> ("", bobId, A.SENT (baseId + 1) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 1) Nothing + 2 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + get bob ##> ("", aliceId, A.SENT (baseId + 2) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 2) Nothing + pure (aliceId, bobId) + runRight_ $ do + -- destination relay down + 3 <- msgId <$> A.sendMessage alice bobId pqEnc noMsgFlags msg1 + bob `down` aliceId + withServer2 $ \_ -> runRight_ $ do + bob `up` aliceId + get alice ##> ("", bobId, A.SENT (baseId + 3) aProxySrv) + get bob =##> \case ("", c, Msg' _ pq msg1') -> c == aliceId && pq == pqEnc && msg1 == msg1'; _ -> False + ackMessage bob aliceId (baseId + 3) Nothing + runRight_ $ do + -- proxy relay down + 4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2 + bob `down` aliceId + withServer2 $ \_ -> runRight_ $ do + bob `up` aliceId + get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv) + get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False + ackMessage alice bobId (baseId + 4) Nothing + where + withServer :: (ThreadId -> IO a) -> IO a + withServer = withServer_ testStoreLogFile testStoreMsgsFile testPort + withServer2 :: (ThreadId -> IO a) -> IO a + withServer2 = withServer_ testStoreLogFile2 testStoreMsgsFile2 testPort2 + withServer_ storeLog storeMsgs port = + withSmpServerConfigOn (transport @TLS) proxyCfg {storeLogFile = Just storeLog, storeMsgsFile = Just storeMsgs} port + a `up` cId = nGet a =##> \case ("", "", UP _ [c]) -> c == cId; _ -> False + a `down` cId = nGet a =##> \case ("", "", DOWN _ [c]) -> c == cId; _ -> False + aCfg = agentProxyCfg {messageRetryInterval = fastMessageRetryInterval} + baseId = 3 + msgId = subtract baseId . fst + servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]} + +testNoProxy :: IO () +testNoProxy = do + withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do + testSMPClient_ "127.0.0.1" testPort2 proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do + (_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing) + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) + +testProxyAuth :: IO () +testProxyAuth = do + withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do + testSMPClient_ "127.0.0.1" testPort proxyVRange $ \(th :: THandleSMP TLS 'TClient) -> do + (_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong") + reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH) + where + proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"} + +todo :: IO () +todo = do + fail "TODO" + +runExceptT' :: Exception e => ExceptT e IO a -> IO a +runExceptT' a = runExceptT a >>= either throwIO pure + +waitSendRecv :: IO () -> IO () -> IO () +waitSendRecv s r = do + s' <- async s + r' <- async r + waitCatch s' >>= either (\e -> cancel r' >> fail (show e)) pure + waitCatch r' >>= either (\e -> cancel s' >> fail (show e)) pure diff --git a/tests/ServerTests.hs b/tests/ServerTests.hs index 09cf975c1..e2ca278d9 100644 --- a/tests/ServerTests.hs +++ b/tests/ServerTests.hs @@ -78,13 +78,13 @@ pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh) pattern Msg :: MsgId -> MsgBody -> BrokerMsg pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body} -sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) +sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) sendRecv h@THandle {params} (sgn, corrId, qId, cmd) = do let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (sgn, tToSend) tGet1 h -signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) +signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg) signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd) Right () <- tPut1 h (authorize tForAuth, tToSend) @@ -93,17 +93,17 @@ signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do authorize t = case a of C.SEd25519 -> Just . TASignature . C.ASignature C.SEd25519 $ C.sign' pk t C.SEd448 -> Just . TASignature . C.ASignature C.SEd448 $ C.sign' pk t - C.SX25519 -> (\THandleAuth {peerPubKey} -> TAAuthenticator $ C.cbAuthenticate peerPubKey pk (C.cbNonce corrId) t) <$> thAuth params + C.SX25519 -> (\THAuthClient {serverPeerPubKey = k} -> TAAuthenticator $ C.cbAuthenticate k pk (C.cbNonce corrId) t) <$> thAuth params #if !MIN_VERSION_base(4,18,0) _sx448 -> undefined -- ghc8107 fails to the branch excluded by types #endif -tPut1 :: Transport c => THandle v c -> SentRawTransmission -> IO (Either TransportError ()) +tPut1 :: Transport c => THandle v c 'TClient -> SentRawTransmission -> IO (Either TransportError ()) tPut1 h t = do [r] <- tPut h [Right t] pure r -tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c -> IO (SignedTransmission err cmd) +tGet1 :: (ProtocolEncoding v err cmd, Transport c) => THandle v c 'TClient -> IO (SignedTransmission err cmd) tGet1 h = do [r] <- liftIO $ tGet h pure r @@ -183,12 +183,12 @@ testCreateSecure (ATransport t) = Resp "dabc" _ err5 <- sendRecv s ("", "dabc", sId, _SEND "hello") (err5, ERR AUTH) #== "rejects unsigned SEND" - let maxAllowedMessage = B.replicate maxMessageLength '-' + let maxAllowedMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion) '-' Resp "bcda" _ OK <- signSendRecv s sKey ("bcda", sId, _SEND maxAllowedMessage) Resp "" _ (Msg mId3 msg3) <- tGet1 r (dec mId3 msg3, Right maxAllowedMessage) #== "delivers message of max size" - let biggerMessage = B.replicate (maxMessageLength + 1) '-' + let biggerMessage = B.replicate (maxMessageLength currentClientSMPRelayVersion + 1) '-' Resp "bcda" _ (ERR LARGE_MSG) <- signSendRecv s sKey ("bcda", sId, _SEND biggerMessage) pure () @@ -555,12 +555,12 @@ testWithStoreLog at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 removeFile testStoreLogFile where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () logSize :: FilePath -> IO Int @@ -608,7 +608,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 2 logSize testStoreMsgsFile `shouldReturn` 5 - logSize testServerStatsBackupFile `shouldReturn` 20 + logSize testServerStatsBackupFile `shouldReturn` 52 Right stats1 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats1 [rId] 5 1 @@ -626,7 +626,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 -- the last message is not removed because it was not ACK'd logSize testStoreMsgsFile `shouldReturn` 3 - logSize testServerStatsBackupFile `shouldReturn` 20 + logSize testServerStatsBackupFile `shouldReturn` 52 Right stats2 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats2 [rId] 5 3 @@ -645,7 +645,7 @@ testRestoreMessages at@(ATransport t) = logSize testStoreLogFile `shouldReturn` 1 logSize testStoreMsgsFile `shouldReturn` 0 - logSize testServerStatsBackupFile `shouldReturn` 20 + logSize testServerStatsBackupFile `shouldReturn` 52 Right stats3 <- strDecode <$> B.readFile testServerStatsBackupFile checkStats stats3 [rId] 5 5 @@ -653,12 +653,12 @@ testRestoreMessages at@(ATransport t) = removeFile testStoreMsgsFile removeFile testServerStatsBackupFile where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () checkStats :: ServerStatsData -> [RecipientId] -> Int -> Int -> Expectation @@ -727,15 +727,15 @@ testRestoreExpireMessages at@(ATransport t) = Right ServerStatsData {_msgExpired} <- strDecode <$> B.readFile testServerStatsBackupFile _msgExpired `shouldBe` 2 where - runTest :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> ThreadId -> Expectation + runTest :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> ThreadId -> Expectation runTest _ test' server = do testSMPClient test' `shouldReturn` () killThread server - runClient :: Transport c => TProxy c -> (THandleSMP c -> IO ()) -> Expectation + runClient :: Transport c => TProxy c -> (THandleSMP c 'TClient -> IO ()) -> Expectation runClient _ test' = testSMPClient test' `shouldReturn` () -createAndSecureQueue :: Transport c => THandleSMP c -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret) +createAndSecureQueue :: Transport c => THandleSMP c 'TClient -> SndPublicAuthKey -> IO (SenderId, RecipientId, RcvPrivateAuthKey, RcvDhSecret) createAndSecureQueue h sPub = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g @@ -759,8 +759,8 @@ testTiming (ATransport t) = timingTests :: [(C.AuthAlg, C.AuthAlg, Int)] timingTests = [ (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd25519, 200), -- correct key type - -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150), - -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200), + -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SEd448, 150), + -- (C.AuthAlg C.SEd25519, C.AuthAlg C.SX25519, 200), (C.AuthAlg C.SEd448, C.AuthAlg C.SEd25519, 200), (C.AuthAlg C.SEd448, C.AuthAlg C.SEd448, 150), -- correct key type (C.AuthAlg C.SEd448, C.AuthAlg C.SX25519, 200), @@ -770,7 +770,7 @@ testTiming (ATransport t) = ] timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const similarTime t1 t2 = abs (t2 / t1 - 1) < 0.2 -- normally the difference between "no queue" and "wrong key" is less than 5% - testSameTiming :: forall c. Transport c => THandleSMP c -> THandleSMP c -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation + testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, n) = do g <- C.newRandom (rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g @@ -791,10 +791,11 @@ testTiming (ATransport t) = runTimingTest sh badKey sId $ _SEND "hello" where - runTimingTest :: PartyI p => THandleSMP c -> C.APrivateAuthKey -> ByteString -> Command p -> IO () + runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> ByteString -> Command p -> IO () runTimingTest h badKey qId cmd = do threadDelay 100000 - _ <- timeRepeat n $ do -- "warm up" the server + _ <- timeRepeat n $ do + -- "warm up" the server Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd) return () threadDelay 100000 @@ -930,8 +931,6 @@ instance Eq C.ASignature where Just Refl -> s == s' _ -> False -deriving instance Eq (C.Signature a) - syntaxTests :: ATransport -> Spec syntaxTests (ATransport t) = do it "unknown command" $ ("", "abcd", "1234", ('H', 'E', 'L', 'L', 'O')) >#> ("", "abcd", "1234", ERR $ CMD UNKNOWN) diff --git a/tests/Test.hs b/tests/Test.hs index aebceb22d..f9fb2a2c0 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -21,6 +21,7 @@ import GHC.IO.Exception (IOException (..)) import qualified GHC.IO.Exception as IOException import NtfServerTests (ntfServerTests) import RemoteControl (remoteControlTests) +import SMPProxyTests (smpProxyTests) import ServerTests import Simplex.Messaging.Transport (TLS, Transport (..)) import Simplex.Messaging.Transport.WebSockets (WS) @@ -59,6 +60,7 @@ main = do describe "SMP server via WebSockets" $ serverTests (transport @WS) describe "Notifications server" $ ntfServerTests (transport @TLS) describe "SMP client agent" $ agentTests (transport @TLS) + describe "SMP proxy" smpProxyTests describe "XFTP" $ do describe "XFTP server" xftpServerTests describe "XFTP file description" fileDescriptionTests diff --git a/tests/Util.hs b/tests/Util.hs index a52fee32c..6ad6d054f 100644 --- a/tests/Util.hs +++ b/tests/Util.hs @@ -1,6 +1,28 @@ module Util where +import Control.Monad (replicateM) +import Data.Either (partitionEithers) +import Data.List (tails) +import GHC.Conc (getNumCapabilities, getNumProcessors, setNumCapabilities) import Test.Hspec +import UnliftIO skip :: String -> SpecWith a -> SpecWith a skip = before_ . pendingWith + +withNumCapabilities :: Int -> IO a -> IO a +withNumCapabilities new a = getNumCapabilities >>= \old -> bracket_ (setNumCapabilities new) (setNumCapabilities old) a + +withNCPUCapabilities :: IO a -> IO a +withNCPUCapabilities a = getNumProcessors >>= \p -> withNumCapabilities p a + +inParrallel :: Int -> IO () -> IO () +inParrallel n action = do + streams <- replicateM n $ async action + (es, rs) <- partitionEithers <$> mapM waitCatch streams + map show es `shouldBe` [] + length rs `shouldBe` n + +combinations :: Int -> [a] -> [[a]] +combinations 0 _ = [[]] +combinations k xs = [y : ys | y : xs' <- tails xs, ys <- combinations (k - 1) xs'] diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index 2d865f140..e4cf3d704 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -20,12 +20,14 @@ import Data.Int (Int64) import Data.List (find, isSuffixOf) import Data.Maybe (fromJust) import SMPAgentClient (agentCfg, initAgentServers, testDB, testDB2, testDB3) -import Simplex.FileTransfer.Description (FileChunk (..), FileDescription (..), FileDescriptionURI (..), ValidFileDescription, fileDescriptionURI, mb, qrSizeLimit, pattern ValidFileDescription) +import Simplex.FileTransfer.Client (XFTPClientConfig (..)) +import Simplex.FileTransfer.Description (FileChunk (..), FileDescription (..), FileDescriptionURI (..), ValidFileDescription, fileDescriptionURI, kb, mb, qrSizeLimit, pattern ValidFileDescription) import Simplex.FileTransfer.Protocol (FileParty (..)) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..)) import Simplex.FileTransfer.Transport (XFTPErrorType (AUTH)) import Simplex.Messaging.Agent (AgentClient, testProtocolServer, xftpDeleteRcvFile, xftpDeleteSndFileInternal, xftpDeleteSndFileRemote, xftpReceiveFile, xftpSendDescription, xftpSendFile, xftpStartWorkers) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, xftpCfg) import Simplex.Messaging.Agent.Protocol (ACommand (..), AgentErrorType (..), BrokerErrorType (..), RcvFileId, SndFileId, noAuthSrv) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs) @@ -44,14 +46,19 @@ import XFTPClient xftpAgentTests :: Spec xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do - it "should send and receive file" testXFTPAgentSendReceive + it "should send and receive file" $ withXFTPServer testXFTPAgentSendReceive + -- uncomment CPP option slow_servers and run hpack to run this test + xit "should send and receive file with slow server responses" $ + withXFTPServerCfg testXFTPServerConfig {responseDelay = 500000} $ + \_ -> testXFTPAgentSendReceive it "should send and receive with encrypted local files" testXFTPAgentSendReceiveEncrypted it "should send and receive large file with a redirect" testXFTPAgentSendReceiveRedirect it "should send and receive small file without a redirect" testXFTPAgentSendReceiveNoRedirect + describe "sending and receiving with version negotiation" testXFTPAgentSendReceiveMatrix it "should resume receiving file after restart" testXFTPAgentReceiveRestore it "should cleanup rcv tmp path after permanent error" testXFTPAgentReceiveCleanup it "should resume sending file after restart" testXFTPAgentSendRestore - it "should cleanup snd prefix path after permanent error" testXFTPAgentSendCleanup + xit "should cleanup snd prefix path after permanent error" testXFTPAgentSendCleanup it "should delete sent file on server" testXFTPAgentDelete it "should resume deleting file after restart" testXFTPAgentDeleteRestore -- TODO when server is fixed to correctly send AUTH error, this test has to be modified to expect AUTH error @@ -66,7 +73,7 @@ xftpAgentTests = around_ testBracket . describe "agent XFTP API" $ do describe "server with password" $ do let auth = Just "abcd" srv = ProtoServerWithAuth testXFTPServer2 - authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP AUTH) + authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP (B.unpack $ strEncode testXFTPServer2) AUTH) it "should pass with correct password" $ testXFTPServerTest auth (srv auth) `shouldReturn` Nothing it "should fail without password" $ testXFTPServerTest auth (srv Nothing) `shouldReturn` authErr it "should fail with incorrect password" $ testXFTPServerTest auth (srv $ Just "wrong") `shouldReturn` authErr @@ -90,14 +97,14 @@ sfProgress c expected = loop 0 -- checks that progress increases till it reaches total checkProgress :: (HasCallStack, MonadIO m) => (Int64, Int64) -> (Int64, Int64) -> (Int64 -> m ()) -> m () checkProgress (prev, expected) (progress, total) loop - | total /= expected = error "total /= expected" + | total /= expected = liftIO (print total) >> error "total /= expected" | progress <= prev = error "progress <= prev" | progress > total = error "progress > total" | progress < total = loop progress | otherwise = pure () testXFTPAgentSendReceive :: HasCallStack => IO () -testXFTPAgentSendReceive = withXFTPServer $ do +testXFTPAgentSendReceive = do filePath <- createRandomFile -- send file, delete snd file internally (rfd1, rfd2) <- withAgent 1 agentCfg initAgentServers testDB $ \sndr -> runRight $ do @@ -176,7 +183,7 @@ testXFTPAgentSendReceiveRedirect = withXFTPServer $ do withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> do FileDescriptionURI {description} <- either fail pure $ strDecode uri - rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing + rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing True rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 65536 totalSize) -- extra RFPROG before switching to real file rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize) rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 8388608 totalSize) @@ -220,7 +227,7 @@ testXFTPAgentSendReceiveNoRedirect = withXFTPServer $ do FileDescriptionURI {description} <- either fail pure $ strDecode uri let ValidFileDescription FileDescription {redirect} = description redirect `shouldBe` Nothing - rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing + rcvFileId <- runRight $ xftpReceiveFile rcp 1 description Nothing True -- NO extra "RFPROG 65k 65k" before switching to real file rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 4194304 totalSize) rfGet rcp `shouldReturn` ("", rcvFileId, RFPROG 5242880 totalSize) @@ -233,6 +240,34 @@ testXFTPAgentSendReceiveNoRedirect = withXFTPServer $ do inBytes <- B.readFile filePathIn B.readFile out `shouldReturn` inBytes +testXFTPAgentSendReceiveMatrix :: Spec +testXFTPAgentSendReceiveMatrix = do + describe "old server" $ do + it "new clients" $ run oldServer newClient newClient + it "new sender, old recipient" $ run oldServer newClient newClient + it "old sender, new recipient" $ run oldServer oldClient newClient + it "old clients" $ run oldServer oldClient oldClient + describe "new server" $ do + it "new clients" $ run newServer newClient newClient + it "new sender, old recipient" $ run newServer newClient newClient + it "old sender, new recipient" $ run newServer oldClient newClient + it "old clients" $ run newServer oldClient oldClient + where + oldClient = agentCfg {xftpCfg = (xftpCfg agentCfg) {clientALPN = Nothing}} + newClient = agentCfg + oldServer = testXFTPServerConfig_ Nothing + newServer = testXFTPServerConfig + run :: HasCallStack => XFTPServerConfig -> AgentConfig -> AgentConfig -> IO () + run server sender receiver = + withXFTPServerCfg server $ \_t -> do + filePath <- createRandomFile_ (kb 319 :: Integer) "testfile" + rfd <- withAgent 1 sender initAgentServers testDB $ \sndr -> do + (sfId, _, rfd1, _) <- runRight $ testSendCF' sndr (CF.plain filePath) (kb 320) + rfd1 <$ xftpDeleteSndFileInternal sndr sfId + withAgent 2 receiver initAgentServers testDB2 $ \rcp -> do + rfId <- runRight $ testReceiveCF' rcp rfd Nothing filePath (kb 320) + xftpDeleteRcvFile rcp rfId + createRandomFile :: HasCallStack => IO FilePath createRandomFile = createRandomFile' "testfile" @@ -250,10 +285,13 @@ testSend :: HasCallStack => AgentClient -> FilePath -> ExceptT AgentErrorType IO testSend sndr = testSendCF sndr . CF.plain testSendCF :: HasCallStack => AgentClient -> CryptoFile -> ExceptT AgentErrorType IO (SndFileId, ValidFileDescription 'FSender, ValidFileDescription 'FRecipient, ValidFileDescription 'FRecipient) -testSendCF sndr file = do +testSendCF sndr file = testSendCF' sndr file $ mb 18 + +testSendCF' :: HasCallStack => AgentClient -> CryptoFile -> Int64 -> ExceptT AgentErrorType IO (SndFileId, ValidFileDescription 'FSender, ValidFileDescription 'FRecipient, ValidFileDescription 'FRecipient) +testSendCF' sndr file size = do xftpStartWorkers sndr (Just senderFiles) sfId <- xftpSendFile sndr 1 file 2 - sfProgress sndr $ mb 18 + sfProgress sndr size ("", sfId', SFDONE sndDescr [rfd1, rfd2]) <- sfGet sndr liftIO $ testNoRedundancy rfd1 liftIO $ testNoRedundancy rfd2 @@ -270,15 +308,15 @@ testReceive rcp rfd = testReceiveCF rcp rfd Nothing testReceiveCF :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> ExceptT AgentErrorType IO RcvFileId testReceiveCF rcp rfd cfArgs originalFilePath = do xftpStartWorkers rcp (Just recipientFiles) - testReceiveCF' rcp rfd cfArgs originalFilePath + testReceiveCF' rcp rfd cfArgs originalFilePath $ mb 18 testReceive' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> FilePath -> ExceptT AgentErrorType IO RcvFileId -testReceive' rcp rfd = testReceiveCF' rcp rfd Nothing +testReceive' rcp rfd originalFilePath = testReceiveCF' rcp rfd Nothing originalFilePath $ mb 18 -testReceiveCF' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> ExceptT AgentErrorType IO RcvFileId -testReceiveCF' rcp rfd cfArgs originalFilePath = do - rfId <- xftpReceiveFile rcp 1 rfd cfArgs - rfProgress rcp $ mb 18 +testReceiveCF' :: HasCallStack => AgentClient -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> FilePath -> Int64 -> ExceptT AgentErrorType IO RcvFileId +testReceiveCF' rcp rfd cfArgs originalFilePath size = do + rfId <- xftpReceiveFile rcp 1 rfd cfArgs True + rfProgress rcp size ("", rfId', RFDONE path) <- rfGet rcp liftIO $ do rfId' `shouldBe` rfId @@ -302,7 +340,7 @@ testXFTPAgentReceiveRestore = do -- receive file - should not succeed with server down rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do xftpStartWorkers rcp (Just recipientFiles) - rfId <- xftpReceiveFile rcp 1 rfd Nothing + rfId <- xftpReceiveFile rcp 1 rfd Nothing True liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt pure rfId @@ -346,7 +384,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do -- receive file - should not succeed with server down rfId <- withAgent 2 agentCfg initAgentServers testDB2 $ \rcp -> runRight $ do xftpStartWorkers rcp (Just recipientFiles) - rfId <- xftpReceiveFile rcp 1 rfd Nothing + rfId <- xftpReceiveFile rcp 1 rfd Nothing True liftIO $ timeout 300000 (get rcp) `shouldReturn` Nothing -- wait for worker attempt pure rfId @@ -358,7 +396,7 @@ testXFTPAgentReceiveCleanup = withGlobalLogging logCfgNoLogs $ do -- receive file - should fail with AUTH error withAgent 3 agentCfg initAgentServers testDB2 $ \rcp' -> do runRight_ $ xftpStartWorkers rcp' (Just recipientFiles) - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp' + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- rfGet rcp' rfId' `shouldBe` rfId -- tmp path should be removed after permanent error @@ -437,7 +475,8 @@ testXFTPAgentSendCleanup = withGlobalLogging logCfgNoLogs $ do -- send file - should fail with AUTH error withAgent 2 agentCfg initAgentServers testDB $ \sndr' -> do runRight_ $ xftpStartWorkers sndr' (Just senderFiles) - ("", sfId', SFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- sfGet sndr' + ("", sfId', SFERR (INTERNAL "XFTP {serverAddress = \"xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000\", xftpErr = AUTH}")) <- + sfGet sndr' sfId' `shouldBe` sfId -- prefix path should be removed after permanent error @@ -471,8 +510,9 @@ testXFTPAgentDelete = withGlobalLogging logCfgNoLogs $ -- receive file - should fail with AUTH error withAgent 3 agentCfg initAgentServers testDB2 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) - rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2 + rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + rfGet rcp2 liftIO $ rfId' `shouldBe` rfId testXFTPAgentDeleteRestore :: HasCallStack => IO () @@ -494,7 +534,6 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do runRight_ $ xftpStartWorkers sndr (Just senderFiles) xftpDeleteSndFileRemote sndr 1 sfId sndDescr timeout 300000 (get sndr) `shouldReturn` Nothing -- wait for worker attempt - threadDelay 300000 length <$> listDirectory xftpServerFiles `shouldReturn` 6 @@ -509,8 +548,9 @@ testXFTPAgentDeleteRestore = withGlobalLogging logCfgNoLogs $ do -- receive file - should fail with AUTH error withAgent 5 agentCfg initAgentServers testDB3 $ \rcp2 -> runRight $ do xftpStartWorkers rcp2 (Just recipientFiles) - rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp2 + rfId <- xftpReceiveFile rcp2 1 rfd2 Nothing True + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + rfGet rcp2 liftIO $ rfId' `shouldBe` rfId testXFTPAgentDeleteOnServer :: HasCallStack => IO () @@ -543,9 +583,10 @@ testXFTPAgentDeleteOnServer = withGlobalLogging logCfgNoLogs $ runRight_ . void $ do -- receive file 1 again - rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing - ("", rfId1', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp - liftIO $ rfId1 `shouldBe` rfId1' + rfId1 <- xftpReceiveFile rcp 1 rfd1_2 Nothing True + ("", rfId1', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + rfGet rcp + liftIO $ rfId1 `shouldBe` rfId1' -- receive file 2 testReceive' rcp rfd2 filePath2 @@ -575,8 +616,9 @@ testXFTPAgentExpiredOnServer = withGlobalLogging logCfgNoLogs $ do -- receive file 1 again - should fail with AUTH error runRight $ do - rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing - ("", rfId', RFERR (INTERNAL "XFTP {xftpErr = AUTH}")) <- rfGet rcp + rfId <- xftpReceiveFile rcp 1 rfd1_2 Nothing True + ("", rfId', RFERR (XFTP "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7000" AUTH)) <- + rfGet rcp liftIO $ rfId' `shouldBe` rfId -- create and send file 2 diff --git a/tests/XFTPClient.hs b/tests/XFTPClient.hs index d42ee7d06..208b54dc5 100644 --- a/tests/XFTPClient.hs +++ b/tests/XFTPClient.hs @@ -1,4 +1,5 @@ {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -12,9 +13,10 @@ import SMPClient (serverBracket) import Simplex.FileTransfer.Client import Simplex.FileTransfer.Description import Simplex.FileTransfer.Server (runXFTPServerBlocking) -import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration) -import qualified Simplex.Messaging.Crypto as C +import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import Simplex.Messaging.Protocol (XFTPServer) +import Simplex.Messaging.Transport (ALPN) import Simplex.Messaging.Transport.Server import Test.Hspec @@ -95,7 +97,10 @@ testXFTPStatsBackupFile :: FilePath testXFTPStatsBackupFile = "tests/tmp/xftp-server-stats.log" testXFTPServerConfig :: XFTPServerConfig -testXFTPServerConfig = +testXFTPServerConfig = testXFTPServerConfig_ (Just supportedXFTPhandshakes) + +testXFTPServerConfig_ :: Maybe [ALPN] -> XFTPServerConfig +testXFTPServerConfig_ alpn = XFTPServerConfig { xftpPort = xftpTestPort, controlPort = Nothing, @@ -114,19 +119,23 @@ testXFTPServerConfig = caCertificateFile = "tests/fixtures/ca.crt", privateKeyFile = "tests/fixtures/server.key", certificateFile = "tests/fixtures/server.crt", + xftpServerVRange = supportedFileServerVRange, logStatsInterval = Nothing, logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, - transportConfig = defaultTransportServerConfig + transportConfig = defaultTransportServerConfig {alpn}, + responseDelay = 0 } testXFTPClientConfig :: XFTPClientConfig testXFTPClientConfig = defaultXFTPClientConfig testXFTPClient :: HasCallStack => (HasCallStack => XFTPClient -> IO a) -> IO a -testXFTPClient client = do - g <- C.newRandom - getXFTPClient g (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> pure ()) >>= \case +testXFTPClient = testXFTPClientWith testXFTPClientConfig + +testXFTPClientWith :: HasCallStack => XFTPClientConfig -> (HasCallStack => XFTPClient -> IO a) -> IO a +testXFTPClientWith cfg client = + getXFTPClient (1, testXFTPServer, Nothing) cfg (\_ -> pure ()) >>= \case Right c -> client c Left e -> error $ show e diff --git a/tests/XFTPServerTests.hs b/tests/XFTPServerTests.hs index 494f624fd..19713d8b1 100644 --- a/tests/XFTPServerTests.hs +++ b/tests/XFTPServerTests.hs @@ -219,8 +219,7 @@ testFileChunkExpiration = withXFTPServerCfg testXFTPServerConfig {fileExpiration testInactiveClientExpiration :: Expectation testInactiveClientExpiration = withXFTPServerCfg testXFTPServerConfig {inactiveClientExpiration} $ \_ -> runRight_ $ do disconnected <- newEmptyTMVarIO - g <- liftIO C.newRandom - c <- ExceptT $ getXFTPClient g (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> atomically $ putTMVar disconnected ()) + c <- ExceptT $ getXFTPClient (1, testXFTPServer, Nothing) testXFTPClientConfig (\_ -> atomically $ putTMVar disconnected ()) pingXFTP c liftIO $ do threadDelay 100000