From 547595041eb661b9038245c082f65e5fb19a6f73 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:36:28 +0000 Subject: [PATCH 1/3] ios: open SimpleX links in chat messages via in-app connect flow (#7101) * ios: open SimpleX links in chat messages via in-app connect flow Tapping an inline SimpleX connection link in message text was dispatched through UIApplication.shared.open. iOS drops an open() of a URL owned by the same app while it is in the foreground (the simplex: scheme and the simplex.chat universal links both belong to this app), so the tap was ignored and never reached the connection flow. Web links (Safari) and mailto:/tel: (other apps) were unaffected, which is why only SimpleX links appeared dead. Route SimpleX links to ChatModel.appOpenUrl instead - the same sink onOpenURL feeds, leading to connectViaUrl/planAndConnect. This matches the connection-link card and the multiplatform clients, which connect in-process rather than via an OS round-trip. Also fixes the same problem for the "Send questions and ideas" and "connect to SimpleX Chat developers" buttons, which open simplexTeamURL (a simplex: link) the same broken way. * docs: plan - justify iOS in-app dispatch for SimpleX links in messages Root cause and justification for opening inline SimpleX links via the in-app connect flow instead of UIApplication.shared.open (undefined re-entry of the same foreground app for a self-owned simplex: URL). --- .../Views/Chat/ChatItem/MsgContentView.swift | 16 ++++- apps/ios/Shared/Views/ChatList/ChatHelp.swift | 4 +- .../Views/UserSettings/SettingsView.swift | 4 +- ...6-19-ios-open-simplex-links-in-messages.md | 65 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 plans/2026-06-19-ios-open-simplex-links-in-messages.md diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 9aaff57cc5..11c3c4c3f4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -191,9 +191,14 @@ private func handleTextTaps( } } } - if let index, let (uri, browser) = attributedStringLink(s, for: index) { + if let index, let (uri, browser, simplex) = attributedStringLink(s, for: index) { if browser { openBrowserAlert(uri: uri) + } else if simplex, let url = URL(string: uri) { + // SimpleX links target this same app (simplex: scheme / simplex.chat universal link), + // so UIApplication.shared.open is dropped by iOS while the app is in the foreground. + // Route to the in-app connect flow instead (same sink onOpenURL feeds). + ChatModel.shared.appOpenUrl = url } else if let url = URL(string: uri) { UIApplication.shared.open(url) } else { @@ -203,9 +208,10 @@ private func handleTextTaps( }) } - func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool)? { + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool, Bool)? { var linkURL: String? var browser: Bool = false + var simplex: Bool = false s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in if index >= range.location && index < range.location + range.length { if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo { @@ -213,6 +219,7 @@ private func handleTextTaps( } else if let url = attrs[linkAttrKey] as? String { linkURL = url browser = attrs[webLinkAttrKey] != nil + simplex = attrs[simplexLinkAttrKey] != nil } else if let showSecrets, let i = attrs[secretAttrKey] as? Int { if showSecrets.wrappedValue.contains(i) { showSecrets.wrappedValue.remove(i) @@ -225,7 +232,7 @@ private func handleTextTaps( stop.pointee = true } } - return if let linkURL { (linkURL, browser) } else { nil } + return if let linkURL { (linkURL, browser, simplex) } else { nil } } } @@ -250,6 +257,8 @@ private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link") private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink") +private let simplexLinkAttrKey = NSAttributedString.Key("chat.simplex.app.simplexLink") + private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command") @@ -392,6 +401,7 @@ func messageText( attrs = linkAttrs() if !preview { attrs[linkAttrKey] = simplexUri + attrs[simplexLinkAttrKey] = true handleTaps = true } if let s = text ?? (privacySimplexLinkModeDefault.get() == .description ? linkType.description : nil) { diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index 3047572236..5844fd3ff9 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -26,7 +26,9 @@ struct ChatHelp: View { Button("connect to SimpleX Chat developers.") { dismissSettingsSheet() DispatchQueue.main.async { - UIApplication.shared.open(simplexTeamURL) + // simplexTeamURL targets this same app; route to the in-app connect flow + // (UIApplication.shared.open is dropped for self-owned URLs in the foreground) + ChatModel.shared.appOpenUrl = simplexTeamURL } } .padding(.top, 2) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index c1bc699261..483ca6aea8 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -390,7 +390,9 @@ struct SettingsView: View { Button("Send questions and ideas") { dismiss() DispatchQueue.main.async { - UIApplication.shared.open(simplexTeamURL) + // simplexTeamURL targets this same app; route to the in-app connect flow + // (UIApplication.shared.open is dropped for self-owned URLs in the foreground) + ChatModel.shared.appOpenUrl = simplexTeamURL } } } diff --git a/plans/2026-06-19-ios-open-simplex-links-in-messages.md b/plans/2026-06-19-ios-open-simplex-links-in-messages.md new file mode 100644 index 0000000000..f7c89b303a --- /dev/null +++ b/plans/2026-06-19-ios-open-simplex-links-in-messages.md @@ -0,0 +1,65 @@ +# iOS: open SimpleX links in chat messages via in-app connect flow + +## Problem + +On iOS, tapping a **SimpleX connection/invitation link inside message text** does nothing — it never reaches the connection flow. Reproduced on iPhone 17 (v6.5.2 and v6.5.5). On the same screens, tapping a web link (opens browser), a `mailto:`/`tel:` link, and the connection-link **card** all work. Notably it was **device-specific**: dead on an iPhone 17 but working on an iPhone 12 running the **same iOS version**, with only **one** SimpleX app installed. + +## Root cause + +Inline links are dispatched in `MsgContentView.handleTextTaps` (`apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift`): + +- web links (`webLinkAttrKey`) → `openBrowserAlert` → `UIApplication.shared.open` (Safari) +- everything else → `UIApplication.shared.open(url)` + +SimpleX links fell into the second branch. Two facts make this the bug: + +1. **The URI is always the `simplex:` custom scheme.** The core markdown parser normalizes every connection link to the `simplex:` scheme via `simplexConnReqUri` / `simplexShortLink` (`src/Simplex/Chat/Markdown.hs:344,353`), regardless of whether the message contained `https://simplex.chat/…` or `simplex:/…` (see `tests/MarkdownTests.hs`). So the tap always calls `UIApplication.shared.open("simplex:/contact#…")`. + +2. **`simplex:` is registered to this app, and the app is in the foreground.** `UIApplication.shared.open` is an OS app-launch API: it asks iOS (LaunchServices) to resolve the scheme to its registered app and activate it. Here the registered app is SimpleX itself, already foregrounded. **Re-entering the same foreground app through `open()` is not a supported operation** — `open()` exists to hand a URL to a *different* app or the system. When the resolved target is the calling foreground app, the outcome is undefined: on some devices iOS still delivers the URL to `onOpenURL`, on others it is a silent no-op (`open` returns `false`, no error, no UI). + +That undefined outcome is decided by device-local OS state (scheme resolution / launch services), which is why identical code + identical OS + identical single app behaved differently on the iPhone 12 (delivered → connected) and the iPhone 17 (no-op → dead). It is **not** an OS-version rule and **not** a multiple-handler conflict — both were ruled out (same OS; single install). + +This also explains the full symptom matrix — only the path that re-enters the same app via `open()` is affected: + +| Tapped | Dispatch | Target | Result | +|---|---|---|---| +| Web link | `openBrowserAlert` → `open()` | Safari (other app) | works | +| `mailto:` / `tel:` | `open()` | Mail / Phone (other apps) | works | +| Invite card | `planAndConnect` in-process | this app, no `open()` | works | +| Inline SimpleX link | `open("simplex:…")` | this app (self), foreground | undefined → dead | + +The underlying cause is using the **wrong mechanism**: an OS hand-off API to perform an **in-app** action. Every other connect path handles the connection in-process and never leaves the app: + +- the card: `planAndConnect` directly (`FramedItemView.swift`) +- the share extension: `ShareSheet.openExternalLink` sets `ChatModel.appOpenUrl` +- multiplatform: `openVerifiedSimplexUri` → `connectIfOpenedViaUri` → `planAndConnect` + +Inline links were the lone exception delegating to the OS, making them hostage to undefined self-open behavior. + +## Fix + +Restore the three-way dispatch the multiplatform clients use (`WEB_URL` / `OTHER_URL` / `SIMPLEX_URL`): + +- web → `openBrowserAlert` (unchanged) +- `mailto:` / `tel:` → `UIApplication.shared.open` (unchanged — these target other apps) +- **SimpleX → `ChatModel.appOpenUrl`** — the same sink `onOpenURL` feeds, leading to `connectViaUrl` → `planAndConnect`, entirely **in-process** with no OS round-trip + +SimpleX links are identified by a dedicated attribute key (`simplexLinkAttrKey`) set on the `.simplexLink` format, mirroring the multiplatform `SIMPLEX_URL` annotation tag, rather than sniffing the URL string — so all link types (contact, invitation, group, channel, relay) are covered. + +This is correct regardless of the exact device-local trigger, because it removes the dependency on iOS re-delivering a self-owned URL. The invite card already proves the in-process path works on the affected device. + +Also fixes the same issue for the **"Send questions and ideas"** (Settings) and **"connect to SimpleX Chat developers"** (chat help) buttons, which opened `simplexTeamURL` (a `simplex:` link) the same broken way. + +## Scope + +- `apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift` — three-way tap dispatch + `simplexLinkAttrKey` +- `apps/ios/Shared/Views/UserSettings/SettingsView.swift`, `apps/ios/Shared/Views/ChatList/ChatHelp.swift` — route `simplexTeamURL` in-process + +No behavior change for web / `mailto:` / `tel:` links. + +## Verification + +- Tap an inline SimpleX invitation/contact link in a received message → the connection sheet opens (on iPhone 17, where it was previously dead). +- The two developer-contact buttons open the connect flow. +- Web links still open the browser; `mailto:`/`tel:` still open Mail/Phone. +- Optional, to confirm the device-local nature: open a `simplex:/contact#…` link from another app (e.g. Notes) on the affected device — if that is also dead there but works on a second device, it confirms the difference is device-local scheme resolution rather than app code. From 974fbae298dd82f31b6a5e3df6f61ffd3241fd23 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:56:55 +0400 Subject: [PATCH 2/3] ci: clean simplexmq submodule source dir in windows lib build (#7099) --- scripts/desktop/build-lib-windows.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index af408d4054..fdf7154491 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -38,8 +38,9 @@ scripts/desktop/prepare-openssl-windows.sh openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') rm -rf $BUILD_DIR 2>/dev/null || true -# Existence of this directory produces build error: cabal's bug -rm -rf dist-newstyle/src/direct-sq* 2>/dev/null || true +# Existence of these directories produces build error: cabal's bug +# (simplexmq is removed because cabal cannot delete its read-only git submodule pack files - blst, libbbs - on Windows) +rm -rf dist-newstyle/src/direct-sq* dist-newstyle/src/simplexmq* 2>/dev/null || true rm cabal.project.local 2>/dev/null || true echo "ignore-project: False" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local From 8c4580ee00b1239d6781dccb278534a1284eca47 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 20 Jun 2026 20:54:34 +0100 Subject: [PATCH 3/3] core: block obfuscated simplex links if the group does not allow them (#7107) * core: block obfuscated simplex links if the group does not allow them * remove newlines * remove renames * name * more efficient parser * remove comment --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- src/Simplex/Chat/Library/Commands.hs | 4 ++-- src/Simplex/Chat/Library/Internal.hs | 9 +++++---- src/Simplex/Chat/Library/Subscriber.hs | 6 +++--- src/Simplex/Chat/Markdown.hs | 11 +++++++++++ src/Simplex/Chat/Types.hs | 6 ++++++ tests/ChatTests/Profiles.hs | 6 ++++++ tests/MarkdownTests.hs | 19 +++++++++++++++++++ 7 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 9fe2342dd6..e11a8bd523 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -3675,7 +3675,7 @@ processChatCommand cxt nm = \case profileToSend <- presentUserBadge user incognitoProfile $ case gInfo_ of Just gInfo_' -> - let allowSimplexLinks = maybe True (groupFeatureUserAllowed SGFSimplexLinks) gInfo_' + let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_' in userProfileInGroup' user allowSimplexLinks incognitoProfile Nothing -> userProfileDirect user incognitoProfile Nothing True chatEvent <- case gInfo_ of @@ -3988,7 +3988,7 @@ processChatCommand cxt nm = \case conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode pure (relayMember, conn, groupRelay) let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + allowSimplexLinks = groupUserAllowSimplexLinks gInfo GroupMember {memberId = relayMemberId} = relayMember membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership let relayInv = GroupRelayInvitation { diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 68e870a7c5..79fff87e5b 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -367,7 +367,7 @@ prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Bool prohibitedSimplexLinks gInfo m mc ft = not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) - && (isChatLink mc || maybe False (any ftIsSimplexLink) ft) + && (isChatLink mc || maybe False (any ftIsSimplexLink) ft || hasObfuscatedSimplexLink (msgContentText mc)) where isChatLink = \case MCChat {} -> True @@ -1177,7 +1177,7 @@ introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn sendGroupMemberMessages user gInfo conn introEvts' userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile -userProfileInGroup user = userProfileInGroup' user . groupFeatureUserAllowed SGFSimplexLinks +userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks {-# INLINE userProfileInGroup #-} userProfileInGroup' :: User -> Bool -> Maybe Profile -> Profile @@ -1195,7 +1195,7 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a memberKey = MemberKey <$> memberPubKey } where - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g redactedMemberProfile :: Bool -> Profile -> Profile redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDescr, image, peerType, badge} = @@ -1203,6 +1203,7 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes where removeSimplexLink s | allowSimplexLinks = Just s + | hasObfuscatedSimplexLink s = Nothing | otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s sendHistory :: User -> GroupInfo -> GroupMember -> CM () @@ -2129,7 +2130,7 @@ sendGroupMessages user gInfo scope asGroup members events = do _ -> False sendProfileUpdate = do let members' = filter (`supportsVersion` memberProfileUpdateVersion) members - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + allowSimplexLinks = groupUserAllowSimplexLinks gInfo -- shouldSendProfileUpdate excludes incognito membership, so the badge is presented profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index f8cb2b861c..79ca91918f 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -813,7 +813,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemInfo memId _memProfile | sameMemberId memId m -> do let GroupMember {memberId = membershipMemId} = membership - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + allowSimplexLinks = groupUserAllowSimplexLinks gInfo membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership -- TODO update member profile -- [async agent commands] no continuation needed, but command should be asynchronous for stability @@ -2701,7 +2701,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure m where contentChanged = not (sameProfileContent (redactedMemberProfile allowSimplexLinks (fromLocalProfile p)) (redactedMemberProfile allowSimplexLinks p')) - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo && groupFeatureMemberAllowed SGFDirectMessages m gInfo updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' @@ -3090,7 +3090,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure toMember subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito - let allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + let allowSimplexLinks = groupUserAllowSimplexLinks gInfo membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile -- [async agent commands] no continuation needed, but commands should be asynchronous for stability diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 9507375527..e8cd381941 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -18,6 +18,7 @@ import Control.Monad import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ +import qualified Data.Attoparsec.ByteString.Char8 as AB import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A import Data.ByteString.Char8 (ByteString) @@ -191,6 +192,16 @@ isLink = \case hasLinks :: MarkdownList -> Bool hasLinks = any $ \(FormattedText f _) -> maybe False isLink f +hasObfuscatedSimplexLink :: Text -> Bool +hasObfuscatedSimplexLink t = + fromRight False $ AB.parseOnly findLinkP $ encodeUtf8 $ T.filter (not . isSpace) t + where + findLinkP = do + AB.skipWhile (\c -> c /= 's' && c /= 'h') -- links start only with "simplex:" or "https://" + (True <$ (strP :: AB.Parser AConnectionLink)) + <|> (AB.anyChar *> findLinkP) + <|> pure False + markdownP :: Parser Markdown markdownP = mconcat <$> A.many' fragmentP where diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 7155d407e8..10f492c328 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -638,6 +638,12 @@ groupFeatureUserAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupInfo - groupFeatureUserAllowed feature GroupInfo {membership = GroupMember {memberRole}, fullGroupPreferences} = groupFeatureMemberAllowed' feature memberRole fullGroupPreferences +-- A connection link in a profile description enables a direct connection, so a description +-- keeps its links only when both SimpleX links and direct messages are allowed. +groupUserAllowSimplexLinks :: GroupInfo -> Bool +groupUserAllowSimplexLinks g = + groupFeatureUserAllowed SGFSimplexLinks g && groupFeatureUserAllowed SGFDirectMessages g + mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 0e2052b259..0efdd6baa2 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2903,6 +2903,12 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil bob <## "bad chat command: feature not allowed SimpleX links" bob ##> ("/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}]") bob <## "bad chat command: feature not allowed SimpleX links" + -- a link split with a space or a newline is still blocked + let (lnk1, lnk2) = splitAt 12 inv + bob ##> ("#team \"" <> lnk1 <> " " <> lnk2 <> "\"") + bob <## "bad chat command: feature not allowed SimpleX links" + bob ##> ("#team \"" <> lnk1 <> "\\n" <> lnk2 <> "\"") + bob <## "bad chat command: feature not allowed SimpleX links" (alice inv <> "\\ntest\"") diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index efa010ceb1..2a5328ff26 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -25,6 +25,7 @@ markdownTests = do textColor textWithUri textWithHyperlink + obfuscatedSimplexLinks textWithEmail textWithPhone textWithMentions @@ -284,6 +285,24 @@ textWithHyperlink = describe "text with HyperLink without link text" do "[click here](example.com)" <==> "[click here](example.com)" "[click here](https://example.com )" <==> "[click here](https://example.com )" +obfuscatedSimplexLinks :: Spec +obfuscatedSimplexLinks = describe "SimpleX links obfuscated with whitespace" do + let addr = "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw" + inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + let spaced s = T.replace "://" ":// " s -- insert a space right after the scheme + it "detects links split with spaces or newlines" do + hasObfuscatedSimplexLink addr `shouldBe` True + hasObfuscatedSimplexLink (spaced addr) `shouldBe` True + hasObfuscatedSimplexLink (T.intercalate "\n" $ T.chunksOf 8 addr) `shouldBe` True + hasObfuscatedSimplexLink ("connect with me: " <> spaced addr) `shouldBe` True + hasObfuscatedSimplexLink (T.intercalate " " $ T.chunksOf 8 $ "https://simplex.chat" <> inv) `shouldBe` True + it "detects a split link followed by other text" do + hasObfuscatedSimplexLink (spaced addr <> "\nplease connect") `shouldBe` True + it "ignores text without a SimpleX link" do + hasObfuscatedSimplexLink "" `shouldBe` False + hasObfuscatedSimplexLink "hello there, this is a normal message" `shouldBe` False + hasObfuscatedSimplexLink "see https://example.com/page?ref=123 for details" `shouldBe` False + email :: Text -> Markdown email = Markdown $ Just Email