Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/Simplex/Messaging/ServiceScheme.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import Control.Applicative ((<|>))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Network.Socket (HostName, ServiceName)
import Network.Socket (ServiceName)
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Transport.Client (TransportHost)

data ServiceScheme = SSSimplex | SSAppServer SrvLoc
deriving (Eq, Show)
Expand All @@ -25,14 +26,13 @@ instance StrEncoding ServiceScheme where
"simplex:" $> SSSimplex
<|> "https://" *> (SSAppServer <$> strP)

data SrvLoc = SrvLoc HostName ServiceName
data SrvLoc = SrvLoc TransportHost ServiceName
deriving (Eq, Ord, Show)

instance StrEncoding SrvLoc where
strEncode (SrvLoc host port) = B.pack $ host <> if null port then "" else ':' : port
strP = SrvLoc <$> host <*> (port <|> pure "")
strEncode (SrvLoc host port) = strEncode host <> B.pack (if null port then "" else ':' : port)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
strEncode (SrvLoc host port) = strEncode host <> B.pack (if null port then "" else ':' : port)
strEncode (SrvLoc host port) = case host of
THIPv6 _ | not (null port) -> strEncode ('[', host, ']') <> B.pack (':' : port)
_ -> strEncode host

strP = SrvLoc <$> strP <*> (port <|> pure "")
where
host = B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ")
port = show <$> (A.char ':' *> (A.decimal :: A.Parser Int))

simplexChat :: ServiceScheme
Expand Down
2 changes: 1 addition & 1 deletion src/Simplex/Messaging/Transport/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ instance StrEncoding TransportHost where
[ THIPv4 <$> ((,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal),
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> ipv6StrP,
THOnionHost <$> ((<>) <$> A.takeWhile (\c -> isAsciiLower c || isDigit c) <*> A.string ".onion"),
THDomainName . B.unpack <$> (notOnion <$?> A.takeWhile1 (A.notInClass ":#,;/ \n\r\t"))
THDomainName . B.unpack <$> (notOnion <$?> A.takeWhile1 (A.notInClass ":#,;/ \n\r\t[]"))
]
where
ipNum = validIP <$?> (A.decimal <* A.char '.')
Expand Down
2 changes: 1 addition & 1 deletion src/Simplex/RemoteControl/Invitation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ instance StrEncoding RCInvitation where
_ <- A.string "xrcp:/"
ca <- strP
_ <- A.char '@'
host <- A.takeWhile (/= ':') >>= either fail pure . strDecode . urlDecode True
host <- strP
_ <- A.char ':'
port <- strP
_ <- A.string "#/?"
Expand Down
27 changes: 27 additions & 0 deletions tests/CoreTests/EncodingTests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Internal (w2c)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Time.Clock.System (SystemTime (..), getSystemTime, utcToSystemTime)
import Data.Time.ISO8601 (parseISO8601)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtocolServer (..), XFTPServer)
import Simplex.Messaging.ServiceScheme (ServiceScheme (..), SrvLoc (..))
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Test.Hspec hiding (fit, it)
import Test.Hspec.QuickCheck (modifyMaxSuccess)
Expand Down Expand Up @@ -67,7 +70,27 @@ encodingTests = modifyMaxSuccess (const 1000) $ do
THDomainName "192.256.0.1" #==# "192.256.0.1"
THDomainName "192.168.0.-1" #==# "192.168.0.-1"
shouldNotParse @TransportHost "192.168.0.0.1" "endOfInput"
-- brackets are reserved for IPv6 literals
shouldReject @TransportHost "[simplex.chat]"
shouldReject @TransportHost "[smp.simplex.im]"
describe "Encoding service locations" $ do
it "should parse bracketed IPv6 host with port" $
strDecode @ServiceScheme "https://[2001:db8::1]:8443"
`shouldBe` Right (SSAppServer $ SrvLoc "2001:db8::1" "8443")
it "should reject bracketed non-IPv6 host" $
shouldReject @ServiceScheme "https://[simplex.chat]:8443"
describe "Encoding protocol servers" $ do
it "should parse bracketed IPv6 server host with port" $
case strDecode @XFTPServer "xftp://1234-w==@[2001:db8::1]:443" of
Left err -> expectationFailure err
Right (ProtocolServer _ parsedHost parsedPort _) -> do
parsedHost `shouldBe` (ipv6Host :| [])
parsedPort `shouldBe` "443"
it "should reject bracketed non-IPv6 server host" $
shouldReject @XFTPServer "xftp://1234-w==@[simplex.chat]:443"
where
ipv6Host :: TransportHost
ipv6Host = either error id $ strDecode "2001:db8::1"
testSystemTime :: SystemTime -> Expectation
testSystemTime t = do
smpEncode t `shouldBe` smpEncode (systemSeconds t)
Expand All @@ -78,3 +101,7 @@ encodingTests = modifyMaxSuccess (const 1000) $ do
strDecode s `shouldBe` Right x
shouldNotParse :: forall s. (StrEncoding s, Eq s, Show s) => ByteString -> String -> Expectation
shouldNotParse s err = strDecode s `shouldBe` (Left err :: Either String s)
shouldReject :: forall s. (StrEncoding s, Show s) => ByteString -> Expectation
shouldReject s = case strDecode s :: Either String s of
Left _ -> pure ()
Right a -> expectationFailure $ "expected parse failure, got " <> show a
66 changes: 65 additions & 1 deletion tests/RemoteControl.hs
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module RemoteControl where

import AgentTests.FunctionalAPITests (runRight)
import Control.Logger.Simple
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.List (stripPrefix)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Time.Clock.System (SystemTime (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Transport (TSbChainKeys (..))
import Simplex.Messaging.Transport.Client (TransportHost)
import qualified Simplex.RemoteControl.Client as HC (RCHostClient (action))
import qualified Simplex.RemoteControl.Client as RC
import Simplex.RemoteControl.Discovery (mkLastLocalHost, preferAddress)
import Simplex.RemoteControl.Invitation (RCSignedInvitation, verifySignedInvitation)
import Simplex.RemoteControl.Invitation
( RCInvitation (..),
RCSignedInvitation,
verifySignedInvitation,
)
import Simplex.RemoteControl.Types
import Test.Hspec hiding (fit, it)
import UnliftIO
Expand All @@ -27,6 +37,9 @@ import Util
remoteControlTests :: Spec
remoteControlTests = do
describe "preferred bindings should go first" testPreferAddress
describe "Invitation parsing" $ do
it "should parse bracketed IPv6 host with port" testInvitationBracketedIPv6Host
it "should reject bracketed non-IPv6 host" testInvitationBracketedNonIPv6HostRejected
describe "New controller/host pairing" $ do
it "should connect to new pairing" testNewPairing
it "should connect to existing pairing" testExistingPairing
Expand Down Expand Up @@ -65,6 +78,57 @@ testPreferAddress = do
addrsDups = "10.20.30.40" `on` "eth1" : addrs'
ifaceDups = "10.20.30.41" `on` "eth0" : addrs'

testInvitationBracketedIPv6Host :: IO ()
testInvitationBracketedIPv6Host = do
invitation <- testIPv6Invitation
let bracketedUri =
B.pack . replaceFirst "@2001:db8::1:" "@[2001:db8::1]:" . B.unpack $
strEncode invitation
expectedHost = either error id (strDecode "2001:db8::1") :: TransportHost
case strDecode bracketedUri of
Left err -> expectationFailure err
Right RCInvitation {host, port} -> do
host `shouldBe` expectedHost
port `shouldBe` 5223

testInvitationBracketedNonIPv6HostRejected :: IO ()
testInvitationBracketedNonIPv6HostRejected = do
invitation <- testIPv6Invitation
let bracketedUri =
B.pack . replaceFirst "@2001:db8::1:" "@[simplex.chat]:" . B.unpack $
strEncode invitation
case strDecode bracketedUri :: Either String RCInvitation of
Left _ -> pure ()
Right _ -> expectationFailure "expected parse failure for bracketed non-IPv6 host"

replaceFirst :: String -> String -> String -> String
replaceFirst needle replacement = go
where
go [] = []
go input@(c : cs) =
case stripPrefix needle input of
Just rest -> replacement <> rest
Nothing -> c : go cs

testIPv6Invitation :: IO RCInvitation
testIPv6Invitation = do
drg <- C.newRandom
(skey, _) <- atomically $ C.generateKeyPair @'C.Ed25519 drg
(idkey, _) <- atomically $ C.generateKeyPair @'C.Ed25519 drg
(dh, _) <- atomically $ C.generateKeyPair @'C.X25519 drg
pure
RCInvitation
{ ca = C.KeyHash "test-ca",
host = either error id $ strDecode "2001:db8::1",
port = 5223,
v = supportedRCPVRange,
app = J.String "app",
ts = MkSystemTime 0 0,
skey,
idkey,
dh
}

testNewPairing :: IO ()
testNewPairing = do
drg <- C.newRandom
Expand Down
18 changes: 17 additions & 1 deletion xftp-web/src/protocol/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ export function parseXFTPServer(address: string): XFTPServer {
const hostPart = m[2]
// Take the first host (before any comma), then split port from that
const firstHost = hostPart.split(',')[0]
return {keyHash, ...parseHostPort(firstHost)}
}

function parseHostPort(firstHost: string): Pick<XFTPServer, "host" | "port"> {
if (firstHost.length === 0) throw new Error("parseXFTPServer: missing host")
if (firstHost.startsWith('[')) {
const bracketEnd = firstHost.indexOf(']')
if (bracketEnd < 0) throw new Error("parseXFTPServer: invalid bracketed host")
const host = firstHost.substring(0, bracketEnd + 1)
const rest = firstHost.substring(bracketEnd + 1)
if (rest.length === 0) return {host, port: "443"}
if (!rest.startsWith(':')) throw new Error("parseXFTPServer: invalid bracketed host")
const port = rest.substring(1)
if (port.length === 0) throw new Error("parseXFTPServer: missing port")
return {host, port}
}
const colonIdx = firstHost.lastIndexOf(':')
let host: string
let port: string
Expand All @@ -45,7 +61,7 @@ export function parseXFTPServer(address: string): XFTPServer {
host = firstHost
port = "443"
}
return {keyHash, host, port}
return {host, port}
}

// Format an XFTPServer back to its URI string representation.
Expand Down
21 changes: 21 additions & 0 deletions xftp-web/test/address.node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {expect, test} from 'vitest'
import {formatXFTPServer, parseXFTPServer, serverOrigin} from '../src/protocol/address.js'

const keyHash = 'LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI='

test('parseXFTPServer supports bracketed IPv6 hosts with ports', () => {
const server = parseXFTPServer(`xftp://${keyHash}@[2001:db8::1]:8443,example.com`)

expect(server.host).toBe('[2001:db8::1]')
expect(server.port).toBe('8443')
expect(serverOrigin(server)).toBe('https://[2001:db8::1]:8443')
expect(formatXFTPServer(server)).toBe(`xftp://${keyHash}@[2001:db8::1]:8443`)
})

test('parseXFTPServer uses the default port for bracketed IPv6 hosts', () => {
const server = parseXFTPServer(`xftp://${keyHash}@[2001:db8::1]`)

expect(server.host).toBe('[2001:db8::1]')
expect(server.port).toBe('443')
expect(serverOrigin(server)).toBe('https://[2001:db8::1]')
})
Loading