Merge branch 'rcv-services' into ep/spec-2

This commit is contained in:
Evgeny Poberezkin
2026-03-12 17:06:07 +00:00
41 changed files with 108 additions and 62 deletions
+15
View File
@@ -0,0 +1,15 @@
{-# LANGUAGE TemplateHaskell #-}
module Web.Embedded where
import Data.FileEmbed (embedDir, embedFile)
import Simplex.Messaging.Server.Web (EmbeddedContent (..))
embeddedContent :: EmbeddedContent
embeddedContent =
EmbeddedContent
{ indexHtml = $(embedFile "apps/common/Web/static/index.html"),
linkHtml = $(embedFile "apps/common/Web/static/link.html"),
mediaContent = $(embedDir "apps/common/Web/static/media/"),
wellKnown = $(embedDir "apps/common/Web/static/.well-known/")
}

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

+3 -3
View File
@@ -8,23 +8,23 @@ module SMPWeb
import Data.ByteString (ByteString)
import Data.String (fromString)
import Web.Embedded (embeddedContent)
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Server.Information
import Simplex.Messaging.Server.Main (simplexmqSource)
import qualified Simplex.Messaging.Server.Web as Web
import Simplex.Messaging.Server.Web (render, serverInfoSubsts, timedTTLText)
import Simplex.Messaging.Server.Web.Embedded as E
import Simplex.Messaging.Transport.Client (TransportHost (..))
smpGenerateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO ()
smpGenerateSite si onionHost path =
Web.generateSite (serverInformation si onionHost) smpLinkPages path
Web.generateSite embeddedContent (serverInformation si onionHost) smpLinkPages path
smpLinkPages :: [String]
smpLinkPages = ["contact", "invitation", "a", "c", "g", "r", "i"]
serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString
serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs
serverInformation ServerInformation {config, information} onionHost = render (Web.indexHtml embeddedContent) substs
where
substs = [("smpConfig", Just "y"), ("xftpConfig", Nothing)] <> substConfig <> serverInfoSubsts simplexmqSource information <> [("onionHost", strEncode <$> onionHost), ("iniFileName", Just "smp-server.ini")]
substConfig =
+3 -3
View File
@@ -9,6 +9,7 @@ module XFTPWeb
import Data.ByteString (ByteString)
import Data.Maybe (isJust)
import Data.String (fromString)
import Web.Embedded (embeddedContent)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
@@ -16,15 +17,14 @@ import Simplex.Messaging.Server.Information (ServerPublicInfo)
import Simplex.Messaging.Server.Main (simplexmqSource)
import qualified Simplex.Messaging.Server.Web as Web
import Simplex.Messaging.Server.Web (render, serverInfoSubsts, timedTTLText)
import Simplex.Messaging.Server.Web.Embedded as E
import Simplex.Messaging.Transport.Client (TransportHost (..))
xftpGenerateSite :: XFTPServerConfig -> Maybe ServerPublicInfo -> Maybe TransportHost -> FilePath -> IO ()
xftpGenerateSite cfg info onionHost path =
Web.generateSite (xftpServerInformation cfg info onionHost) [] path
Web.generateSite embeddedContent (xftpServerInformation cfg info onionHost) [] path
xftpServerInformation :: XFTPServerConfig -> Maybe ServerPublicInfo -> Maybe TransportHost -> ByteString
xftpServerInformation XFTPServerConfig {fileExpiration, logStatsInterval, allowNewFiles, newFileBasicAuth} information onionHost = render E.indexHtml substs
xftpServerInformation XFTPServerConfig {fileExpiration, logStatsInterval, allowNewFiles, newFileBasicAuth} information onionHost = render (Web.indexHtml embeddedContent) substs
where
substs = [("smpConfig", Nothing), ("xftpConfig", Just "y")] <> substConfig <> serverInfoSubsts simplexmqSource information <> [("onionHost", strEncode <$> onionHost), ("iniFileName", Just "file-server.ini")]
substConfig =
+36 -29
View File
@@ -24,33 +24,33 @@ extra-source-files:
CHANGELOG.md
cbits/sha512.h
cbits/sntrup761.h
src/Simplex/Messaging/Server/Web/index.html
src/Simplex/Messaging/Server/Web/link.html
src/Simplex/Messaging/Server/Web/media/apk_icon.png
src/Simplex/Messaging/Server/Web/media/apple_store.svg
src/Simplex/Messaging/Server/Web/media/contact.js
src/Simplex/Messaging/Server/Web/media/contact_page_mobile.png
src/Simplex/Messaging/Server/Web/media/f_droid.svg
src/Simplex/Messaging/Server/Web/media/favicon.ico
src/Simplex/Messaging/Server/Web/media/GilroyBold.woff2
src/Simplex/Messaging/Server/Web/media/GilroyLight.woff2
src/Simplex/Messaging/Server/Web/media/GilroyMedium.woff2
src/Simplex/Messaging/Server/Web/media/GilroyRegular.woff2
src/Simplex/Messaging/Server/Web/media/GilroyRegularItalic.woff2
src/Simplex/Messaging/Server/Web/media/google_play.svg
src/Simplex/Messaging/Server/Web/media/logo-dark.png
src/Simplex/Messaging/Server/Web/media/logo-light.png
src/Simplex/Messaging/Server/Web/media/logo-symbol-dark.svg
src/Simplex/Messaging/Server/Web/media/logo-symbol-light.svg
src/Simplex/Messaging/Server/Web/media/moon.svg
src/Simplex/Messaging/Server/Web/media/qrcode.js
src/Simplex/Messaging/Server/Web/media/script.js
src/Simplex/Messaging/Server/Web/media/style.css
src/Simplex/Messaging/Server/Web/media/sun.svg
src/Simplex/Messaging/Server/Web/media/swiper-bundle.min.css
src/Simplex/Messaging/Server/Web/media/swiper-bundle.min.js
src/Simplex/Messaging/Server/Web/media/tailwind.css
src/Simplex/Messaging/Server/Web/media/testflight.png
apps/common/Web/static/index.html
apps/common/Web/static/link.html
apps/common/Web/static/media/apk_icon.png
apps/common/Web/static/media/apple_store.svg
apps/common/Web/static/media/contact.js
apps/common/Web/static/media/contact_page_mobile.png
apps/common/Web/static/media/f_droid.svg
apps/common/Web/static/media/favicon.ico
apps/common/Web/static/media/GilroyBold.woff2
apps/common/Web/static/media/GilroyLight.woff2
apps/common/Web/static/media/GilroyMedium.woff2
apps/common/Web/static/media/GilroyRegular.woff2
apps/common/Web/static/media/GilroyRegularItalic.woff2
apps/common/Web/static/media/google_play.svg
apps/common/Web/static/media/logo-dark.png
apps/common/Web/static/media/logo-light.png
apps/common/Web/static/media/logo-symbol-dark.svg
apps/common/Web/static/media/logo-symbol-light.svg
apps/common/Web/static/media/moon.svg
apps/common/Web/static/media/qrcode.js
apps/common/Web/static/media/script.js
apps/common/Web/static/media/style.css
apps/common/Web/static/media/sun.svg
apps/common/Web/static/media/swiper-bundle.min.css
apps/common/Web/static/media/swiper-bundle.min.js
apps/common/Web/static/media/tailwind.css
apps/common/Web/static/media/testflight.png
flag swift
description: Enable swift JSON format
@@ -249,7 +249,6 @@ library
Simplex.Messaging.Server.Main.GitCommit
Simplex.Messaging.Server.Main.Init
Simplex.Messaging.Server.Web
Simplex.Messaging.Server.Web.Embedded
Simplex.Messaging.Server.MsgStore
Simplex.Messaging.Server.MsgStore.Journal
Simplex.Messaging.Server.MsgStore.Journal.SharedLock
@@ -345,7 +344,6 @@ library
if !flag(client_library)
build-depends:
case-insensitive ==1.2.*
, file-embed >=0.0.10 && <0.1
, hashable ==1.4.*
, ini ==0.4.1
, optparse-applicative >=0.15 && <0.17
@@ -412,15 +410,18 @@ executable smp-server
main-is: Main.hs
other-modules:
SMPWeb
Web.Embedded
Paths_simplexmq
hs-source-dirs:
apps/smp-server
apps/common
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-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:
base
, bytestring
, file-embed >=0.0.10 && <0.1
, simple-logger
, simplexmq
, text
@@ -448,15 +449,18 @@ executable xftp-server
main-is: Main.hs
other-modules:
XFTPWeb
Web.Embedded
Paths_simplexmq
hs-source-dirs:
apps/xftp-server
apps/common
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-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:
base
, bytestring
, file-embed >=0.0.10 && <0.1
, simple-logger
, simplexmq
default-language: Haskell2010
@@ -502,6 +506,7 @@ test-suite simplexmq-test
XFTPWebTests
SMPWeb
XFTPWeb
Web.Embedded
Paths_simplexmq
if flag(client_postgres)
other-modules:
@@ -522,6 +527,7 @@ test-suite simplexmq-test
tests
apps/smp-server
apps/xftp-server
apps/common
default-extensions:
StrictData
-- add -fhpc to ghc-options to run tests with coverage
@@ -539,6 +545,7 @@ test-suite simplexmq-test
, crypton-x509-store
, crypton-x509-validation
, directory
, file-embed >=0.0.10 && <0.1
, filepath
, generic-random ==1.5.*
, hashable
+1
View File
@@ -19,6 +19,7 @@ module Simplex.Messaging.Parsers
sumTypeJSON,
taggedObjectJSON,
singleFieldJSON,
singleFieldJSON_,
defaultJSON,
textP,
pattern SingleFieldJSONTag,
+13 -6
View File
@@ -6,6 +6,7 @@
module Simplex.Messaging.Server.Web
( EmbeddedWebParams (..),
WebHttpsParams (..),
EmbeddedContent (..),
serveStaticFiles,
attachStaticFiles,
serveStaticPageH2,
@@ -41,7 +42,6 @@ import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Server (AttachHTTP)
import Simplex.Messaging.Server.CLI (simplexmqCommit)
import Simplex.Messaging.Server.Information
import Simplex.Messaging.Server.Web.Embedded as E
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Util (ifM, tshow)
import System.Directory (canonicalizePath, createDirectoryIfMissing, doesFileExist)
@@ -62,6 +62,13 @@ data WebHttpsParams = WebHttpsParams
key :: FilePath
}
data EmbeddedContent = EmbeddedContent
{ indexHtml :: ByteString,
linkHtml :: ByteString,
mediaContent :: [(FilePath, ByteString)],
wellKnown :: [(FilePath, ByteString)]
}
serveStaticFiles :: EmbeddedWebParams -> IO ()
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do
forM_ webHttpPort $ \port -> flip forkFinally (\e -> logError $ "HTTP server crashed: " <> tshow e) $ do
@@ -118,14 +125,14 @@ staticFiles root = S.staticApp settings . changeWellKnownPath
_ -> req
pfxLen = B.length "/.well-known/"
generateSite :: ByteString -> [String] -> FilePath -> IO ()
generateSite indexContent linkPages sitePath = do
generateSite :: EmbeddedContent -> ByteString -> [String] -> FilePath -> IO ()
generateSite embedded indexContent linkPages sitePath = do
createDirectoryIfMissing True sitePath
B.writeFile (sitePath </> "index.html") indexContent
copyDir "media" E.mediaContent
copyDir "media" $ mediaContent embedded
-- `.well-known` path is re-written in changeWellKnownPath,
-- staticApp does not allow hidden folders.
copyDir "well-known" E.wellKnown
copyDir "well-known" $ wellKnown embedded
forM_ linkPages createLinkPage
logInfo $ "Generated static site contents at " <> tshow sitePath
where
@@ -134,7 +141,7 @@ generateSite indexContent linkPages sitePath = do
forM_ content $ \(path, s) -> B.writeFile (sitePath </> dir </> path) s
createLinkPage path = do
createDirectoryIfMissing True $ sitePath </> path
B.writeFile (sitePath </> path </> "index.html") E.linkHtml
B.writeFile (sitePath </> path </> "index.html") $ linkHtml embedded
-- | Serve static files via HTTP/2 directly (without WAI).
-- Path traversal protection: resolved path must stay under canonicalRoot.
@@ -1,18 +0,0 @@
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Messaging.Server.Web.Embedded where
import Data.ByteString (ByteString)
import Data.FileEmbed (embedDir, embedFile)
indexHtml :: ByteString
indexHtml = $(embedFile "src/Simplex/Messaging/Server/Web/index.html")
linkHtml :: ByteString
linkHtml = $(embedFile "src/Simplex/Messaging/Server/Web/link.html")
mediaContent :: [(FilePath, ByteString)]
mediaContent = $(embedDir "src/Simplex/Messaging/Server/Web/media/")
wellKnown :: [(FilePath, ByteString)]
wellKnown = $(embedDir "src/Simplex/Messaging/Server/Web/.well-known/")
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@simplex-chat/xftp-web",
"version": "0.1.0",
"version": "0.2.0",
"description": "XFTP file transfer protocol client for web/browser environments",
"license": "AGPL-3.0-only",
"repository": {
+20
View File
@@ -52,3 +52,23 @@ export function parseXFTPServer(address: string): XFTPServer {
export function formatXFTPServer(srv: XFTPServer): string {
return "xftp://" + base64urlEncode(srv.keyHash) + "@" + srv.host + ":" + srv.port
}
// Extract unique XFTP servers referenced in a file description's chunk replicas.
export function getDescriptionServers(fd: {chunks: {replicas: {server: string}[]}[]}): XFTPServer[] {
const seen = new Set<string>()
const servers: XFTPServer[] = []
for (const chunk of fd.chunks) {
for (const replica of chunk.replicas) {
if (!seen.has(replica.server)) {
seen.add(replica.server)
servers.push(parseXFTPServer(replica.server))
}
}
}
return servers
}
// Build an HTTPS origin from an XFTP server address.
export function serverOrigin(server: XFTPServer): string {
return server.port === "443" ? `https://${server.host}` : `https://${server.host}:${server.port}`
}
+2 -1
View File
@@ -75,7 +75,7 @@ export default defineConfig(({mode}) => {
servers = ['xftp://fp@localhost:443']
} else {
// In production mode, use the preset servers
servers = [...presets.simplex, ...presets.flux]
servers = [...presets.simplex]
define['__XFTP_SERVERS__'] = JSON.stringify(servers)
define['__XFTP_PROXY_PORT__'] = JSON.stringify(null)
}
@@ -94,6 +94,7 @@ export default defineConfig(({mode}) => {
outDir: resolve(__dirname, 'dist-web'),
emptyOutDir: true,
target: 'esnext',
minify: false,
chunkSizeWarningLimit: 1200,
rollupOptions: {
external: ['node:http2', 'url'],
+8
View File
@@ -5,6 +5,7 @@ import {
newXFTPAgent, closeXFTPAgent,
decodeDescriptionURI, downloadFileRaw
} from '../src/agent.js'
import {getDescriptionServers} from '../src/protocol/address.js'
import {XFTPPermanentError} from '../src/client.js'
const DECRYPT_WEIGHT = 0.15
@@ -20,6 +21,8 @@ export function initDownload(app: HTMLElement, hash: string) {
return
}
const wrongServer = !getDescriptionServers(fd).map(s => s.host).includes(window.location.hostname)
const size = fd.redirect ? fd.redirect.size : fd.size
app.innerHTML = `
<div class="card">
@@ -62,6 +65,11 @@ export function initDownload(app: HTMLElement, hash: string) {
showStage(errorStage)
}
if (wrongServer) {
readyStage.innerHTML = `<p class="error">${t('wrongServer', 'This file is not hosted on this server.')}</p>`
return
}
dlBtn.addEventListener('click', startDownload)
retryBtn.addEventListener('click', () => showStage(readyStage))
+6 -1
View File
@@ -6,6 +6,7 @@ import {
newXFTPAgent, closeXFTPAgent, uploadFile, encodeDescriptionURI,
type EncryptedFileMetadata
} from '../src/agent.js'
import {getDescriptionServers, serverOrigin} from '../src/protocol/address.js'
import {XFTPPermanentError} from '../src/client.js'
const MAX_SIZE = 100 * 1024 * 1024
@@ -170,7 +171,11 @@ export function initUpload(app: HTMLElement) {
})
if (aborted) return
const url = window.location.origin + window.location.pathname + '#' + result.uri
const descServers = getDescriptionServers(result.rcvDescription)
const origin = descServers.length > 0
? serverOrigin(descServers[0])
: window.location.origin
const url = origin + window.location.pathname + '#' + result.uri
shareLink.value = url
showStage(completeStage)
app.dispatchEvent(new CustomEvent('xftp:upload-complete', {detail: {url}, bubbles: true}))