mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-31 05:25:49 +00:00
parse short connection links
This commit is contained in:
@@ -214,7 +214,11 @@ Each step produces working, tested code. Steps 1-11 work without block encryptio
|
||||
- `base64UrlDecode(s)`: pad to multiple of 4, replace `-`→`+`, `_`→`/`, decode
|
||||
- Returns `{scheme, connType, server: {hosts, port, keyHash}, linkKey}`
|
||||
|
||||
**Tests**: Haskell `strEncode` a `ConnShortLink` → TypeScript `parseShortLink` parses. All fields match. Test multiple formats: with/without query params, different type chars.
|
||||
**Tests**: Haskell `strEncode` a `ConnShortLink` → TypeScript `connShortLinkStrP` parses. All fields match. Test multiple formats: with/without query params, different type chars.
|
||||
|
||||
**Done**. Function: `connShortLinkStrP` in `agent/protocol.ts`. Uses `base64urlDecode` from xftp-web `description.ts`.
|
||||
|
||||
**Future**: add long link parsing (`ConnectionRequestUri`) and an either-parser that handles both short and long links.
|
||||
|
||||
### Step 7: HKDF Key Derivation
|
||||
|
||||
|
||||
87
smp-web/src/agent/protocol.ts
Normal file
87
smp-web/src/agent/protocol.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// Agent protocol types and short link parsing.
|
||||
// Mirrors: Simplex.Messaging.Agent.Protocol
|
||||
|
||||
import {base64urlDecode} from "@simplex-chat/xftp-web/dist/protocol/description.js"
|
||||
|
||||
// -- Short link types (Agent/Protocol.hs:1462-1470)
|
||||
|
||||
export type ShortLinkScheme = "simplex" | "https"
|
||||
|
||||
export type ContactConnType = "contact" | "channel" | "group" | "relay"
|
||||
|
||||
export interface ShortLinkServer {
|
||||
hosts: string[]
|
||||
port: string
|
||||
keyHash: Uint8Array
|
||||
}
|
||||
|
||||
export type ConnShortLink =
|
||||
| {mode: "invitation", scheme: ShortLinkScheme, server: ShortLinkServer, linkId: Uint8Array, linkKey: Uint8Array}
|
||||
| {mode: "contact", scheme: ShortLinkScheme, connType: ContactConnType, server: ShortLinkServer, linkKey: Uint8Array}
|
||||
|
||||
// -- Contact type char mapping (Agent/Protocol.hs:1651-1666)
|
||||
|
||||
const ctTypeFromChar: Record<string, ContactConnType> = {
|
||||
a: "contact",
|
||||
c: "channel",
|
||||
g: "group",
|
||||
r: "relay",
|
||||
}
|
||||
|
||||
// -- Short link parser (Agent/Protocol.hs:1596-1629)
|
||||
// Mirrors strP for AConnShortLink
|
||||
|
||||
export function connShortLinkStrP(uri: string): ConnShortLink {
|
||||
let scheme: ShortLinkScheme
|
||||
let firstHost: string | null = null
|
||||
let rest: string
|
||||
|
||||
if (uri.startsWith("simplex:")) {
|
||||
scheme = "simplex"
|
||||
rest = uri.slice("simplex:".length)
|
||||
} else if (uri.startsWith("https://")) {
|
||||
scheme = "https"
|
||||
const afterScheme = uri.slice("https://".length)
|
||||
const slashIdx = afterScheme.indexOf("/")
|
||||
if (slashIdx < 0) throw new Error("bad short link: no path")
|
||||
firstHost = afterScheme.slice(0, slashIdx)
|
||||
rest = afterScheme.slice(slashIdx)
|
||||
} else {
|
||||
throw new Error("bad short link scheme")
|
||||
}
|
||||
|
||||
// rest: /typeChar#fragment?query
|
||||
if (rest[0] !== "/") throw new Error("bad short link: expected /")
|
||||
const typeChar = rest[1]
|
||||
const hashIdx = rest.indexOf("#")
|
||||
if (hashIdx < 0) throw new Error("bad short link: no #")
|
||||
const afterHash = rest.slice(hashIdx + 1)
|
||||
|
||||
const qIdx = afterHash.indexOf("?")
|
||||
const fragment = qIdx >= 0 ? afterHash.slice(0, qIdx) : afterHash
|
||||
const queryStr = qIdx >= 0 ? afterHash.slice(qIdx + 1) : ""
|
||||
const params = new URLSearchParams(queryStr)
|
||||
|
||||
// Build server
|
||||
const hParam = params.get("h")
|
||||
const additionalHosts = hParam ? hParam.split(",") : []
|
||||
const allHosts = firstHost ? [firstHost, ...additionalHosts] : additionalHosts
|
||||
if (allHosts.length === 0) throw new Error("short link without server")
|
||||
|
||||
const port = params.get("p") ?? ""
|
||||
const keyHash = params.has("c") ? base64urlDecode(params.get("c")!) : new Uint8Array(0)
|
||||
const server: ShortLinkServer = {hosts: allHosts, port, keyHash}
|
||||
|
||||
if (typeChar === "i") {
|
||||
const slashIdx = fragment.indexOf("/")
|
||||
if (slashIdx < 0) throw new Error("invitation link must have linkId/linkKey")
|
||||
const linkId = base64urlDecode(fragment.slice(0, slashIdx))
|
||||
const linkKey = base64urlDecode(fragment.slice(slashIdx + 1))
|
||||
return {mode: "invitation", scheme, server, linkId, linkKey}
|
||||
} else {
|
||||
const connType = ctTypeFromChar[typeChar]
|
||||
if (!connType) throw new Error("unknown contact type: " + typeChar)
|
||||
const linkKey = base64urlDecode(fragment)
|
||||
return {mode: "contact", scheme, connType, server, linkKey}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- | Per-function tests for the smp-web TypeScript SMP client library.
|
||||
@@ -10,13 +13,19 @@
|
||||
module SMPWebTests (smpWebTests) where
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BC
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Word (Word16)
|
||||
import qualified Simplex.Messaging.Agent.Protocol as AP
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Protocol (SMPServer, pattern SMPServer)
|
||||
import Simplex.Messaging.Server.Env.STM (AStoreType (..))
|
||||
import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..))
|
||||
import Simplex.Messaging.Server.Web (attachStaticAndWS)
|
||||
import Simplex.Messaging.Transport (TLS)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import SMPClient (cfgWebOn, testKeyHash, testPort, withSmpServerConfig)
|
||||
import Test.Hspec hiding (it)
|
||||
import Util
|
||||
@@ -40,6 +49,12 @@ impWS :: String
|
||||
impWS = "import { connectSMP, sendBlock, receiveBlock } from './dist/transport/websockets.js';"
|
||||
<> "import { blockPad, blockUnpad } from '@simplex-chat/xftp-web/dist/protocol/transmission.js';"
|
||||
|
||||
impAgentProto :: String
|
||||
impAgentProto = "import { connShortLinkStrP } from './dist/agent/protocol.js';"
|
||||
|
||||
jsStr :: B.ByteString -> String
|
||||
jsStr bs = "'" <> BC.unpack bs <> "'"
|
||||
|
||||
smpWebTests :: SpecWith ()
|
||||
smpWebTests = describe "SMP Web Client" $ do
|
||||
describe "protocol" $ do
|
||||
@@ -139,6 +154,70 @@ smpWebTests = describe "SMP Web Client" $ do
|
||||
<> "})")
|
||||
tsEncoded `shouldBe` hsEncoded
|
||||
|
||||
describe "agent/protocol" $ do
|
||||
describe "ConnShortLink" $ do
|
||||
it "parses simplex: contact link" $ do
|
||||
let srv = SMPServer ("smp1.example.com" :| []) "" (C.KeyHash $ B.pack [1..32])
|
||||
linkKey = AP.LinkKey $ B.pack [100..131]
|
||||
link = AP.CSLContact AP.SLSSimplex AP.CCTContact srv linkKey
|
||||
uri = strEncode link
|
||||
tsResult <- callNode $ impAgentProto
|
||||
<> "const r = connShortLinkStrP(" <> jsStr uri <> ");"
|
||||
<> jsOut ("new Uint8Array([...r.linkKey, ...r.server.keyHash])")
|
||||
tsResult `shouldBe` (B.pack [100..131] <> B.pack [1..32])
|
||||
|
||||
it "parses https: contact link with port" $ do
|
||||
let srv = SMPServer ("smp2.example.com" :| []) "5223" (C.KeyHash $ B.pack [50..81])
|
||||
linkKey = AP.LinkKey $ B.pack [200..231]
|
||||
link = AP.CSLContact AP.SLSServer AP.CCTContact srv linkKey
|
||||
uri = strEncode link
|
||||
tsResult <- callNode $ impAgentProto
|
||||
<> "const r = connShortLinkStrP(" <> jsStr uri <> ");"
|
||||
<> "const enc = new TextEncoder();"
|
||||
<> jsOut ("new Uint8Array([...r.linkKey, ...r.server.keyHash, ...enc.encode(r.server.port), 0, ...enc.encode(r.server.hosts.join(','))])")
|
||||
let expected = B.pack [200..231] <> B.pack [50..81] <> "5223" <> B.singleton 0 <> "smp2.example.com"
|
||||
tsResult `shouldBe` expected
|
||||
|
||||
it "parses simplex: contact link with multiple hosts" $ do
|
||||
let srv = SMPServer ("host1.example.com" :| ["host2.example.com"]) "" (C.KeyHash $ B.pack [1..32])
|
||||
linkKey = AP.LinkKey $ B.pack [10..41]
|
||||
link = AP.CSLContact AP.SLSSimplex AP.CCTContact srv linkKey
|
||||
uri = strEncode link
|
||||
tsResult <- callNode $ impAgentProto
|
||||
<> "const r = connShortLinkStrP(" <> jsStr uri <> ");"
|
||||
<> "const enc = new TextEncoder();"
|
||||
<> jsOut ("new Uint8Array([...r.linkKey, ...enc.encode(r.server.hosts.join(','))])")
|
||||
tsResult `shouldBe` (B.pack [10..41] <> "host1.example.com,host2.example.com")
|
||||
|
||||
it "parses group link type" $ do
|
||||
let srv = SMPServer ("smp.example.com" :| []) "" (C.KeyHash $ B.pack [1..32])
|
||||
linkKey = AP.LinkKey $ B.pack [10..41]
|
||||
link = AP.CSLContact AP.SLSSimplex AP.CCTGroup srv linkKey
|
||||
uri = strEncode link
|
||||
tsResult <- callNode $ impAgentProto
|
||||
<> "const r = connShortLinkStrP(" <> jsStr uri <> ");"
|
||||
<> "const enc = new TextEncoder();"
|
||||
<> jsOut ("enc.encode(r.connType)")
|
||||
tsResult `shouldBe` "group"
|
||||
|
||||
it "round-trips: Haskell encode -> TypeScript parse -> fields match" $ do
|
||||
let srv = SMPServer ("server1.simplex.im" :| ["server2.simplex.im"]) "443" (C.KeyHash $ B.pack [1..32])
|
||||
linkKey = AP.LinkKey $ B.pack [200..231]
|
||||
link = AP.CSLContact AP.SLSServer AP.CCTContact srv linkKey
|
||||
uri = strEncode link
|
||||
-- TypeScript returns: mode, scheme, connType, host count, port, linkKey
|
||||
tsResult <- callNode $ impAgentProto
|
||||
<> "const r = connShortLinkStrP(" <> jsStr uri <> ");"
|
||||
<> "const enc = new TextEncoder();"
|
||||
<> jsOut ("new Uint8Array(["
|
||||
<> "r.mode === 'contact' ? 1 : 0,"
|
||||
<> "r.scheme === 'https' ? 1 : 0,"
|
||||
<> "r.connType === 'contact' ? 1 : 0,"
|
||||
<> "r.server.hosts.length,"
|
||||
<> "...r.linkKey"
|
||||
<> "])")
|
||||
tsResult `shouldBe` B.pack ([1, 1, 1, 2] ++ [200..231])
|
||||
|
||||
describe "WebSocket handshake" $ do
|
||||
it "TypeScript connects and completes SMP handshake" $ do
|
||||
let msType = ASType SQSMemory SMSJournal
|
||||
|
||||
Reference in New Issue
Block a user