diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml
index e8fb52285..17215ffdd 100644
--- a/.github/workflows/build-binaries.yml
+++ b/.github/workflows/build-binaries.yml
@@ -150,6 +150,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
USE_HARD_LINKS: false # see https://github.com/electron-userland/electron-builder/issues/7093
+ GH_PUBLISH_OWNER: ${{ github.repository_owner }}
+ GH_PUBLISH_REPO: ${{ github.event.repository.name }}
steps:
- name: Checkout git repo
@@ -194,7 +196,7 @@ jobs:
- name: Make release build & publish ${{ matrix.identifier }}
if: ${{ needs.create_draft_release_if_needed.outputs.version_tag != '' }}
run: |
- sed -i 's/"target": "deb"/"target": "${{ matrix.electron_target }}"/g' package.json && pnpm run build-release-publish
+ sed -i 's/"target": "deb"/"target": "${{ matrix.electron_target }}"/g' package.json && pnpm run build-release-publish --config.publish.provider=github --config.publish.owner="${GH_PUBLISH_OWNER}" --config.publish.repo="${GH_PUBLISH_REPO}"
- name: Backup release metadata
# only run this on "push" to "master" or alpha releases
@@ -261,6 +263,8 @@ jobs:
needs: [create_draft_release_if_needed, lint]
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_PUBLISH_OWNER: ${{ github.repository_owner }}
+ GH_PUBLISH_REPO: ${{ github.event.repository.name }}
name: "windows x64"
steps:
- run: git config --global core.autocrlf false
@@ -303,7 +307,7 @@ jobs:
- name: Make release build & publish
if: ${{ needs.create_draft_release_if_needed.outputs.version_tag != '' }}
- run: pnpm run build-release-publish # No other args needed for windows publish
+ run: pnpm run build-release-publish --config.publish.provider=github --config.publish.owner="${env:GH_PUBLISH_OWNER}" --config.publish.repo="${env:GH_PUBLISH_REPO}"
# We want both arm64 and intel mac builds, and according to this https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources macos-14 and above is always arm64 and macos-15 is the last intel runner
# NOTE x64 builds made on an arm64 host will not bundle the native modules correctly https://github.com/electron-userland/electron-builder/issues/8646
@@ -329,6 +333,8 @@ jobs:
SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }}
SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }}
SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }}
+ GH_PUBLISH_OWNER: ${{ github.repository_owner }}
+ GH_PUBLISH_REPO: ${{ github.event.repository.name }}
steps:
- run: git config --global core.autocrlf false
diff --git a/actions/make_release_build/action.yml b/actions/make_release_build/action.yml
index a608e7b71..d8b587358 100644
--- a/actions/make_release_build/action.yml
+++ b/actions/make_release_build/action.yml
@@ -33,7 +33,7 @@ runs:
shell: bash
run: |
source ./build/setup-mac-certificate.sh
- pnpm run build-release-publish --config.mac.bundleVersion=${{ github.ref }}
+ pnpm run build-release-publish --config.mac.bundleVersion=${{ github.ref }} --config.publish.provider=github --config.publish.owner="${GH_PUBLISH_OWNER}" --config.publish.repo="${GH_PUBLISH_REPO}"
# Note: We need to backup the latest.yml file because other jobs can overwrite it when they are complete e.g. macOS arm64 and x64
- name: Backup release metadata
diff --git a/package.json b/package.json
index 298da1402..12e7d87c6 100644
--- a/package.json
+++ b/package.json
@@ -96,7 +96,7 @@
"framer-motion": "^12.5.0",
"fs-extra": "11.3.0",
"image-type": "^4.1.0",
- "libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.16/libsession_util_nodejs-v0.6.16.tar.gz",
+ "libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.17/libsession_util_nodejs-v0.6.17.tar.gz",
"libsodium-wrappers-sumo": "^0.7.15",
"linkify-it": "^5.0.0",
"lodash": "^4.17.21",
@@ -110,6 +110,7 @@
"pino": "^9.6.0",
"protobufjs": "^7.4.0",
"punycode": "^2.3.1",
+ "socks-proxy-agent": "^8.0.5",
"qrcode.react": "4.2.0",
"react": "19.2.1",
"react-contexify": "https://github.com/session-foundation/session-react-contexify/releases/download/v7.0.0/react-contexify-7.0.0.tgz",
@@ -348,6 +349,9 @@
"!node_modules/linkify-it/**/*.mjs",
"!node_modules/linkify-it/node_modules/**",
"node_modules/linkify-it/node_modules/uc.micro/build/index.cjs.js",
+ "node_modules/socks-proxy-agent/**/*",
+ "node_modules/socks/build/**/*.js",
+ "node_modules/smart-buffer/build/**/*.js",
"node_modules/uc.micro/build/index.cjs.js",
"!node_modules/uc.micro/**/*.mjs",
"node_modules/libsession_util_nodejs/build/Release/*.node",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index af342bff3..56b63231a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -99,8 +99,8 @@ importers:
specifier: ^4.1.0
version: 4.1.0
libsession_util_nodejs:
- specifier: https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.16/libsession_util_nodejs-v0.6.16.tar.gz
- version: https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.16/libsession_util_nodejs-v0.6.16.tar.gz
+ specifier: https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.17/libsession_util_nodejs-v0.6.17.tar.gz
+ version: https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.17/libsession_util_nodejs-v0.6.17.tar.gz
libsodium-wrappers-sumo:
specifier: ^0.7.15
version: 0.7.15
@@ -194,6 +194,9 @@ importers:
sharp:
specifier: https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz
version: https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz
+ socks-proxy-agent:
+ specifier: ^8.0.5
+ version: 8.0.5
styled-components:
specifier: 6.1.19
version: 6.1.19(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
@@ -1157,11 +1160,11 @@ packages:
resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==}
'@emoji-mart/data@https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-data-v1.2.1.tgz':
- resolution: {tarball: https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-data-v1.2.1.tgz}
+ resolution: {integrity: sha512-JR40zY+VXUMdI9itDS8PGJK8ThRpxKb2eL4lHv/uUFVHdqt2GdBNj1mhKg1RiAqq4/MZDQYQyia8kxDX/TsdtA==, tarball: https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-data-v1.2.1.tgz}
version: 1.2.1
'@emoji-mart/react@https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-react-v1.1.1.tgz':
- resolution: {tarball: https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-react-v1.1.1.tgz}
+ resolution: {integrity: sha512-yysTeYN2fOaK9e9rgohm7UiDnVaX5DFooQziHKutRL/m44AILqqpbgwzNDsHt5Q+E7kF/tIO09O0OHWbAEWHkg==, tarball: https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-react-v1.1.1.tgz}
version: 1.1.1
peerDependencies:
emoji-mart: ^5.2
@@ -1220,72 +1223,72 @@ packages:
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-darwin-arm64-0.34.5.tgz':
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-darwin-arm64-0.34.5.tgz}
+ resolution: {integrity: sha512-0OrdgJ6O6iS3EJwhqIBKUCgFlXGj+IuTehPtkents2kfg7Bi6rOgxEH4E1tPBaPAvO1ot0np/DbJ5a8yTqxUEQ==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-darwin-arm64-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-darwin-x64-0.34.5.tgz':
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-darwin-x64-0.34.5.tgz}
+ resolution: {integrity: sha512-lDdMW6VAz8/lCLRVsdbepsqqohxpJqXDYxU6xHU+amtI9eE16VTqdMdTBU3cc3PqTWF/rX1qhIIi5UbhB69i+w==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-darwin-x64-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-darwin-arm64-1.2.4.tar.gz':
- resolution: {tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-darwin-arm64-1.2.4.tar.gz}
+ resolution: {integrity: sha512-29f2Pg9m4JrkDn6Zw7bM8egPFfFVg6csVZt+CHvkjsEcxa86g9zFdsOOCiezlqeLx1Q8TtmzY5Qb1v7YAJd+ug==, tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-darwin-arm64-1.2.4.tar.gz}
version: 1.2.4
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-darwin-x64-1.2.4.tar.gz':
- resolution: {tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-darwin-x64-1.2.4.tar.gz}
+ resolution: {integrity: sha512-kKENFPJsmcCApNQCClvu/CBfNZ2F3D7ne8QVfTQCNd4xOTAqbUSJIWjWk/O+8isR+Z7cRJel4bbDPkIPcAHfCg==, tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-darwin-x64-1.2.4.tar.gz}
version: 1.2.4
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-arm64-1.2.4.tar.gz':
- resolution: {tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-arm64-1.2.4.tar.gz}
+ resolution: {integrity: sha512-yO7Tjt8yDg7gD0nco7MTkmn47JVRS+NvES4kDYleommmyZpyXIFSqxn1uej80O9+HJrBZNCI3joIb8UoyFkDIw==, tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-arm64-1.2.4.tar.gz}
version: 1.2.4
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-sse2-x64@https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-sse2-x64-1.2.4.tar.gz':
- resolution: {tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-sse2-x64-1.2.4.tar.gz}
+ resolution: {integrity: sha512-uEDadTSKFHrkW+spQBKEE+cIBGA7niUbAs0tdpmiWsXnFjvcmPlNM6vBikDcgUtkor7I1hS2OFWUusJhjO6kew==, tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-sse2-x64-1.2.4.tar.gz}
version: 1.2.4
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linux-x64@https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-x64-1.2.4.tar.gz':
- resolution: {tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-x64-1.2.4.tar.gz}
+ resolution: {integrity: sha512-2BRxNvxW4ZmjilIsS2MPBygl6PAQVFc2GE0+4bFrtUuVuMJG/+xwstfBIHWVzZY8zc436RFNlg11uIxMIy7WcQ==, tarball: https://github.com/session-foundation/sharp-libvips/releases/download/v1.2.4/sharp-libvips-linux-x64-1.2.4.tar.gz}
version: 1.2.4
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-arm64-0.34.5.tgz':
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-arm64-0.34.5.tgz}
+ resolution: {integrity: sha512-yffSwlxntiFtk4rcAZDRdX6nMTfW7W4W8+tEOYLSYYwzmPGaRo1uLsKxJQZs1a02HXD1ToXs1mx6KItMY78SQw==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-arm64-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-sse2-x64@https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-sse2-x64-0.34.5.tgz':
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-sse2-x64-0.34.5.tgz}
+ resolution: {integrity: sha512-Is4HvNKBGw2wIJuyO9zxmDnpO+AAOitAMXw44ioasiDSi1haOBH6knGu4pII0zwzX2XjR+Wp3OyksXZ43xqFcw==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-sse2-x64-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linux-x64@https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-x64-0.34.5.tgz':
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-x64-0.34.5.tgz}
+ resolution: {integrity: sha512-Lta92mZ5KsN1GmAdQSfpIXxRy44TSY6ZUIKaTPw0LSUl3LtrqPeMJAHLulZpEBRLBh6lW/Cv0AENtvWEVZtu1A==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-linux-x64-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-win32-x64@https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-win32-x64-0.34.5.tgz':
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-win32-x64-0.34.5.tgz}
+ resolution: {integrity: sha512-NkWwtSB5y/F3ACj3cW284BhgXsP2KIdW8k0WWj4hqGGbZd3lE5zpcZYdcZjvOPQQkSSX2ePw88PQ5WrK2llCHQ==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/img-sharp-win32-x64-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
@@ -2963,7 +2966,7 @@ packages:
hasBin: true
emoji-mart@https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-v5.6.0.tgz:
- resolution: {tarball: https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-v5.6.0.tgz}
+ resolution: {integrity: sha512-sonXhghObY+cG5B6m57cUhDsNBsuVFShK0ZareQqBtUNyHeMMMdRVCvcS1RnV9qBrwnGoh2nUa8szFITv93gJQ==, tarball: https://github.com/session-foundation/session-emoji-mart/releases/download/v5.6.0/emoji-mart-v5.6.0.tgz}
version: 5.6.0
emoji-regex@8.0.0:
@@ -4020,9 +4023,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
- libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.16/libsession_util_nodejs-v0.6.16.tar.gz:
- resolution: {integrity: sha512-jbsnksU+kYvUsQ2ykhHoco+wgB3r+X6N8UIc7IlBrPZ1LQ0j0uvQVN5c+Ym9Bsi2LXhVLnDsbN0YTpOBlwKO0A==, tarball: https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.16/libsession_util_nodejs-v0.6.16.tar.gz}
- version: 0.6.16
+ libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.17/libsession_util_nodejs-v0.6.17.tar.gz:
+ resolution: {integrity: sha512-SBAWNOhxl3W/h93e7noH8OS4YqGtpDu/5j10LTmxwrcPmRrgMpUHmyV76/Gl/rifX07Bf2jr7GIuVp7Hp711HQ==, tarball: https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.17/libsession_util_nodejs-v0.6.17.tar.gz}
+ version: 0.6.17
libsodium-sumo@0.7.15:
resolution: {integrity: sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==}
@@ -5206,7 +5209,7 @@ packages:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
sharp@https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz:
- resolution: {tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz}
+ resolution: {integrity: sha512-sW1/dqImBRgiIdVf3ku68Ci6aFA2s1fXzEBDYxuuXZQwwWarAJZOzoFCXhWs5XGepLfrQ+T//jaxYqgPgkHQmA==, tarball: https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz}
version: 0.34.5
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -10123,7 +10126,7 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
- libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.16/libsession_util_nodejs-v0.6.16.tar.gz:
+ libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.17/libsession_util_nodejs-v0.6.17.tar.gz:
dependencies:
cmake-js: 7.3.1
node-addon-api: 8.5.0
diff --git a/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx b/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx
index fc2822166..5a3f5eeb5 100644
--- a/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx
+++ b/ts/components/dialog/user-settings/pages/network/SessionNetworkPage.tsx
@@ -9,6 +9,7 @@ import {
useLastRefreshedTimestamp,
} from '../../../../../state/selectors/networkModal';
import { NetworkSection } from './sections/network/NetworkSection';
+import { ProxySection } from './sections/network/ProxySection';
import { useDataIsStale } from '../../../../../state/selectors/networkData';
import { SessionLucideIconButton } from '../../../../icon/SessionIconButton';
import { LUCIDE_ICONS_UNICODE } from '../../../../icon/lucide';
@@ -71,6 +72,7 @@ export function SessionNetworkPage(modalState: UserSettingsModalState) {
onClose={closeAction || undefined}
>
+
{!dataIsStale && lastRefreshedTimestamp && !loading ? (
<>
diff --git a/ts/components/dialog/user-settings/pages/network/sections/network/ProxySection.tsx b/ts/components/dialog/user-settings/pages/network/sections/network/ProxySection.tsx
new file mode 100644
index 000000000..8fa052aac
--- /dev/null
+++ b/ts/components/dialog/user-settings/pages/network/sections/network/ProxySection.tsx
@@ -0,0 +1,285 @@
+import { ipcRenderer } from 'electron';
+import { useMemo, useState } from 'react';
+import styled from 'styled-components';
+import { Flex } from '../../../../../../basic/Flex';
+import { SessionToggle } from '../../../../../../basic/SessionToggle';
+import { SectionHeading, SessionNetworkParagraph, Block } from '../../components';
+import { ModalSimpleSessionInput } from '../../../../../../inputs/SessionInput';
+import {
+ SessionButton,
+ SessionButtonColor,
+ SessionButtonShape,
+ SessionButtonType,
+} from '../../../../../../basic/SessionButton';
+import { SpacerMD, SpacerXS } from '../../../../../../basic/Text';
+import { ToastUtils } from '../../../../../../../session/utils';
+import { SettingsKey } from '../../../../../../../data/settings-key';
+import { normalizeProxySettings } from '../../../../../../../session/utils/ProxySettings';
+
+const PROXY_COPY = {
+ title: 'SOCKS5 Proxy',
+ description: 'Route supported Session traffic and the auto-updater through a SOCKS5 proxy.',
+ enableTitle: 'Enable SOCKS5 proxy',
+ enableDescription: 'Apply SOCKS5 settings to supported Session traffic.',
+ host: 'Host',
+ hostRequired: 'Host is required.',
+ port: 'Port',
+ portInvalid: 'Port must be between 1 and 65535.',
+ usernameOptional: 'Username (optional)',
+ passwordOptional: 'Password (optional)',
+ validationError: 'Invalid SOCKS5 proxy settings.',
+ saved: 'SOCKS5 proxy settings saved.',
+ applyFailed: 'Failed to apply SOCKS5 proxy settings.',
+ save: 'Save',
+ saving: 'Saving...',
+} as const;
+
+type ProxyDraft = {
+ enabled: boolean;
+ host: string;
+ port: string;
+ username: string;
+ password: string;
+};
+
+type ValidationResult = {
+ hostError?: string;
+ portError?: string;
+};
+
+const ToggleRow = styled.button`
+ width: 100%;
+ border: 0;
+ padding: var(--margins-md);
+ text-align: start;
+ background: transparent;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--margins-md);
+ cursor: pointer;
+ color: var(--text-primary-color);
+`;
+
+const ToggleCopy = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: var(--margins-xs);
+`;
+
+const ToggleTitle = styled.p`
+ margin: 0;
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+`;
+
+const ToggleDescription = styled.p`
+ margin: 0;
+ font-size: var(--font-size-md);
+ color: var(--text-secondary-color);
+`;
+
+const InputsContainer = styled(Flex)`
+ width: 100%;
+ gap: var(--margins-sm);
+`;
+
+function loadDraft(): ProxyDraft {
+ return {
+ enabled: Boolean(window.getSettingValue(SettingsKey.proxyEnabled)),
+ host: (window.getSettingValue(SettingsKey.proxyHost) as string) || '',
+ port: String(window.getSettingValue(SettingsKey.proxyPort) || ''),
+ username: (window.getSettingValue(SettingsKey.proxyUsername) as string) || '',
+ password: (window.getSettingValue(SettingsKey.proxyPassword) as string) || '',
+ };
+}
+
+function validateDraft(draft: ProxyDraft): ValidationResult {
+ if (!draft.enabled) {
+ return {};
+ }
+
+ if (!draft.host.trim()) {
+ return { hostError: PROXY_COPY.hostRequired };
+ }
+
+ const port = Number.parseInt(draft.port, 10);
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
+ return { portError: PROXY_COPY.portInvalid };
+ }
+
+ return {};
+}
+
+async function applyProxySettings() {
+ return new Promise(resolve => {
+ ipcRenderer.once('apply-proxy-settings-response', (_event, error) => {
+ resolve(error ?? null);
+ });
+ ipcRenderer.send('apply-proxy-settings');
+ });
+}
+
+export function ProxySection() {
+ const [draft, setDraft] = useState(loadDraft);
+ const [saving, setSaving] = useState(false);
+ const validation = useMemo(() => validateDraft(draft), [draft]);
+
+ const saveDisabled =
+ saving || (!!draft.enabled && (!!validation.hostError || !!validation.portError));
+
+ const saveSettings = async () => {
+ if (saveDisabled) {
+ if (validation.hostError || validation.portError) {
+ ToastUtils.pushToastError(
+ 'proxyValidationError',
+ validation.hostError || validation.portError || PROXY_COPY.validationError
+ );
+ }
+ return;
+ }
+
+ setSaving(true);
+
+ try {
+ await window.setSettingValue(SettingsKey.proxyEnabled, draft.enabled);
+ await window.setSettingValue(SettingsKey.proxyHost, draft.host.trim());
+ await window.setSettingValue(SettingsKey.proxyPort, Number.parseInt(draft.port, 10) || 0);
+ await window.setSettingValue(SettingsKey.proxyUsername, draft.username.trim());
+ await window.setSettingValue(SettingsKey.proxyPassword, draft.password.trim());
+
+ const error = await applyProxySettings();
+ if (error) {
+ throw error;
+ }
+
+ const normalized = normalizeProxySettings({
+ enabled: draft.enabled,
+ host: draft.host,
+ port: draft.port,
+ username: draft.username,
+ password: draft.password,
+ });
+
+ setDraft(
+ normalized
+ ? {
+ enabled: normalized.enabled,
+ host: normalized.host,
+ port: String(normalized.port),
+ username: normalized.username || '',
+ password: normalized.password || '',
+ }
+ : {
+ ...draft,
+ host: draft.host.trim(),
+ username: draft.username.trim(),
+ password: draft.password.trim(),
+ }
+ );
+
+ ToastUtils.pushToastSuccess('proxySaved', PROXY_COPY.saved);
+ } catch {
+ ToastUtils.pushToastError('proxyApplyFailed', PROXY_COPY.applyFailed);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+ {PROXY_COPY.title}
+ {PROXY_COPY.description}
+
+
+ setDraft(current => ({ ...current, enabled: !current.enabled }))}
+ >
+
+ {PROXY_COPY.enableTitle}
+ {PROXY_COPY.enableDescription}
+
+
+
+ {draft.enabled ? (
+ <>
+
+ setDraft(current => ({ ...current, host: value }))}
+ onEnterPressed={() => {
+ void saveSettings();
+ }}
+ providedError={validation.hostError}
+ errorDataTestId={'error-message'}
+ />
+ setDraft(current => ({ ...current, port: value }))}
+ onEnterPressed={() => {
+ void saveSettings();
+ }}
+ providedError={validation.portError}
+ errorDataTestId={'error-message'}
+ />
+ setDraft(current => ({ ...current, username: value }))}
+ onEnterPressed={() => {
+ void saveSettings();
+ }}
+ providedError={undefined}
+ errorDataTestId={'error-message'}
+ />
+ setDraft(current => ({ ...current, password: value }))}
+ onEnterPressed={() => {
+ void saveSettings();
+ }}
+ providedError={undefined}
+ errorDataTestId={'error-message'}
+ type="password"
+ />
+
+ >
+ ) : null}
+
+
+
+
+
+
+ );
+}
diff --git a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
index 8759add0c..a844a8440 100644
--- a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
+++ b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
@@ -509,6 +509,7 @@ function ProInfoBlockRefund() {
case ProPaymentProvider.GooglePlayStore:
return ;
case ProPaymentProvider.Nil:
+ case ProPaymentProvider.Rangeproof:
return ;
default:
return assertUnreachable(data.provider, `Unknown pro payment provider: ${data.provider}`);
diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts
index 254e9bd85..1b4796f30 100644
--- a/ts/data/settings-key.ts
+++ b/ts/data/settings-key.ts
@@ -14,6 +14,11 @@ const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed';
const hasFollowSystemThemeEnabled = 'hasFollowSystemThemeEnabled';
const hideRecoveryPassword = 'hideRecoveryPassword';
+const proxyEnabled = 'proxyEnabled';
+const proxyHost = 'proxyHost';
+const proxyPort = 'proxyPort';
+const proxyUsername = 'proxyUsername';
+const proxyPassword = 'proxyPassword';
// Pro stats counters
const proLongerMessagesSent = 'proLongerMessagesSent';
@@ -52,6 +57,11 @@ export const SettingsKey = {
settingsAudioNotification,
hasSyncedInitialConfigurationItem,
hasLinkPreviewPopupBeenDisplayed,
+ proxyEnabled,
+ proxyHost,
+ proxyPort,
+ proxyUsername,
+ proxyPassword,
latestUserProfileEnvelopeTimestamp,
latestUserGroupEnvelopeTimestamp,
latestUserContactsEnvelopeTimestamp,
diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts
index b297791b8..6f2ee2be9 100644
--- a/ts/mains/main_node.ts
+++ b/ts/mains/main_node.ts
@@ -15,6 +15,7 @@ import {
Menu,
nativeTheme,
screen,
+ session,
shell,
systemPreferences,
} from 'electron';
@@ -196,6 +197,7 @@ import LIBSESSION_CONSTANTS from '../session/utils/libsession/libsession_constan
import { isReleaseChannel } from '../updater/types';
import { canAutoUpdate, checkForUpdates } from '../updater/updater';
import { initializeMainProcessLogger } from '../util/logger/main_process_logging';
+import { buildProxyUrl, normalizeProxySettings } from '../session/utils/ProxySettings';
import * as log from '../util/logger/log';
import { DURATION } from '../session/constants';
@@ -843,6 +845,7 @@ async function showMainWindow(sqlKey: string, passwordAttempt = false) {
tray = createTrayIcon(getMainWindow);
}
+ await applyProxySettings();
setupMenu();
}
@@ -1237,6 +1240,60 @@ ipc.on('media-access', async () => {
await askForMediaAccess();
});
+async function applyProxySettings() {
+ const settings = normalizeProxySettings({
+ enabled: sqlNode.getItemById(SettingsKey.proxyEnabled)?.value,
+ host: sqlNode.getItemById(SettingsKey.proxyHost)?.value,
+ port: sqlNode.getItemById(SettingsKey.proxyPort)?.value,
+ username: sqlNode.getItemById(SettingsKey.proxyUsername)?.value,
+ password: sqlNode.getItemById(SettingsKey.proxyPassword)?.value,
+ });
+
+ if (!settings) {
+ await session.defaultSession.setProxy({ proxyRules: '' });
+ delete process.env.HTTPS_PROXY;
+ delete process.env.HTTP_PROXY;
+ delete process.env.NO_PROXY;
+ return;
+ }
+
+ const proxyRules = buildProxyUrl(settings, { includeAuth: false, protocol: 'socks5' });
+ const proxyEnv = buildProxyUrl(settings, { includeAuth: true, protocol: 'socks5' });
+
+ await session.defaultSession.setProxy({
+ proxyRules,
+ proxyBypassRules: '',
+ });
+
+ process.env.HTTPS_PROXY = proxyEnv;
+ process.env.HTTP_PROXY = proxyEnv;
+ process.env.NO_PROXY = '';
+}
+
+app.on('login', (event, _webContents, _request, authInfo, callback) => {
+ const settings = normalizeProxySettings({
+ enabled: sqlNode.getItemById(SettingsKey.proxyEnabled)?.value,
+ host: sqlNode.getItemById(SettingsKey.proxyHost)?.value,
+ port: sqlNode.getItemById(SettingsKey.proxyPort)?.value,
+ username: sqlNode.getItemById(SettingsKey.proxyUsername)?.value,
+ password: sqlNode.getItemById(SettingsKey.proxyPassword)?.value,
+ });
+
+ if (authInfo.isProxy && settings?.username && settings.password) {
+ event.preventDefault();
+ callback(settings.username, settings.password);
+ }
+});
+
+ipc.on('apply-proxy-settings', async event => {
+ try {
+ await applyProxySettings();
+ event.sender.send('apply-proxy-settings-response', null);
+ } catch (error) {
+ event.sender.send('apply-proxy-settings-response', error);
+ }
+});
+
ipc.handle('get-storage-profile', async (): Promise => {
return app.getPath('userData');
});
diff --git a/ts/session/apis/pro_backend_api/types.ts b/ts/session/apis/pro_backend_api/types.ts
index 896749d49..9b592a967 100644
--- a/ts/session/apis/pro_backend_api/types.ts
+++ b/ts/session/apis/pro_backend_api/types.ts
@@ -30,6 +30,7 @@ export enum ProPaymentProvider {
Nil = 0,
GooglePlayStore = 1,
iOSAppStore = 2,
+ Rangeproof = 3,
}
export function getProPaymentProviderFromProOriginatingPlatform(
@@ -42,6 +43,8 @@ export function getProPaymentProviderFromProOriginatingPlatform(
return ProPaymentProvider.GooglePlayStore;
case 'iOS':
return ProPaymentProvider.iOSAppStore;
+ case 'Rangeproof':
+ return ProPaymentProvider.Rangeproof;
default:
assertUnreachable(v, 'getProPaymentProviderFromProOriginatingPlatform');
throw new Error('getProPaymentProviderFromProOriginatingPlatform: case not handled');
@@ -58,6 +61,8 @@ export function getProOriginatingPlatformFromProPaymentProvider(
return 'Google';
case ProPaymentProvider.iOSAppStore:
return 'iOS';
+ case ProPaymentProvider.Rangeproof:
+ return 'Rangeproof';
default:
assertUnreachable(v, 'getProOriginatingPlatformFromProPaymentProvider');
throw new Error('getProOriginatingPlatformFromProPaymentProvider: case not handled');
diff --git a/ts/session/apis/seed_node_api/SeedNodeAPI.ts b/ts/session/apis/seed_node_api/SeedNodeAPI.ts
index 9cb87584b..44ce0f6aa 100644
--- a/ts/session/apis/seed_node_api/SeedNodeAPI.ts
+++ b/ts/session/apis/seed_node_api/SeedNodeAPI.ts
@@ -18,7 +18,7 @@ import {
getDataFeatureFlag,
getFeatureFlag,
} from '../../../state/ducks/types/releasedFeaturesReduxTypes';
-import { FetchDestination, insecureNodeFetch } from '../../utils/InsecureNodeFetch';
+import { FetchDestination, insecureNodeFetch, isProxyEnabled } from '../../utils/InsecureNodeFetch';
import { zodSafeParse } from '../../../util/zod';
import {
ServiceNodesResponseSchema,
@@ -333,10 +333,13 @@ async function getSnodesFromSeedUrl(urlObj: URL) {
urlObj.hostname,
urlObj.protocol !== Constants.PROTOCOLS.HTTP
);
+ const tlsOptions = (sslAgent as (https.Agent & { options?: https.AgentOptions }) | undefined)
+ ?.options;
+ const requestTimeout = isProxyEnabled() ? 10000 : 5000;
const fetchOptions = {
method: 'POST',
- timeout: 5000,
+ timeout: requestTimeout,
body: JSON.stringify(body),
headers: {
'User-Agent': 'WhatsApp',
@@ -354,6 +357,7 @@ async function getSnodesFromSeedUrl(urlObj: URL) {
fetchOptions,
destination: FetchDestination.SEED_NODE,
caller: 'getSnodesFromSeedUrl',
+ tlsOptions,
});
if (response.status !== 200) {
diff --git a/ts/session/apis/snode_api/onions.ts b/ts/session/apis/snode_api/onions.ts
index 8c2b04a13..2f45dacf2 100644
--- a/ts/session/apis/snode_api/onions.ts
+++ b/ts/session/apis/snode_api/onions.ts
@@ -1130,6 +1130,7 @@ const sendOnionRequestNoRetries = async ({
fetchOptions: guardFetchOptions,
destination,
caller: `${caller} -> sendOnionRequestNoRetries`,
+ tlsOptions: { rejectUnauthorized: false },
});
return { response, decodingSymmetricKey: destCtx.symmetricKey };
};
diff --git a/ts/session/apis/snode_api/sessionRpc.ts b/ts/session/apis/snode_api/sessionRpc.ts
index 29e835839..de3cb54c0 100644
--- a/ts/session/apis/snode_api/sessionRpc.ts
+++ b/ts/session/apis/snode_api/sessionRpc.ts
@@ -91,6 +91,7 @@ async function doRequestNoRetries({
},
destination: FetchDestination.SERVICE_NODE,
caller: 'doRequestNoRetries',
+ tlsOptions: fetchOptions.agent ? { rejectUnauthorized: false } : undefined,
});
if (!response.ok) {
throw new HTTPError('Loki_rpc error', response);
diff --git a/ts/session/onions/onionPath.ts b/ts/session/onions/onionPath.ts
index b5ccaf4ea..92071a21b 100644
--- a/ts/session/onions/onionPath.ts
+++ b/ts/session/onions/onionPath.ts
@@ -37,7 +37,7 @@ import {
} from '../../state/ducks/types/releasedFeaturesReduxTypes';
import { logDebugWithCat } from '../../util/logger/debugLog';
import { stringify } from '../../types/sqlSharedTypes';
-import { FetchDestination, insecureNodeFetch } from '../utils/InsecureNodeFetch';
+import { FetchDestination, insecureNodeFetch, isProxyEnabled } from '../utils/InsecureNodeFetch';
export function getOnionPathMinTimeout() {
return DURATION.SECONDS;
@@ -353,6 +353,8 @@ export async function testGuardNode(snode: Snode) {
params,
};
+ const requestTimeout = isProxyEnabled() ? 20000 : 10000;
+
const fetchOptions = {
method: 'POST',
body: JSON.stringify(body),
@@ -361,7 +363,7 @@ export async function testGuardNode(snode: Snode) {
'User-Agent': 'WhatsApp',
'Accept-Language': 'en-us',
},
- timeout: 10000, // 10s, we want a smaller timeout for testing
+ timeout: requestTimeout,
agent: snodeHttpsAgent,
};
@@ -377,6 +379,7 @@ export async function testGuardNode(snode: Snode) {
fetchOptions,
destination: FetchDestination.SERVICE_NODE,
caller: 'testGuardNode',
+ tlsOptions: { rejectUnauthorized: false },
});
} catch (e) {
if (e.type === 'request-timeout') {
diff --git a/ts/session/utils/InsecureNodeFetch.ts b/ts/session/utils/InsecureNodeFetch.ts
index e3ec069b1..610cc10ac 100644
--- a/ts/session/utils/InsecureNodeFetch.ts
+++ b/ts/session/utils/InsecureNodeFetch.ts
@@ -4,10 +4,13 @@ import nodeFetch, {
type RequestInit,
type Response,
} from 'node-fetch';
+import { SocksProxyAgent } from 'socks-proxy-agent';
+import { SettingsKey } from '../../data/settings-key';
import { ReduxOnionSelectors } from '../../state/selectors/onions';
import { ERROR_CODE_NO_CONNECT } from '../apis/snode_api/SNodeAPI';
import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
import { updateIsOnline } from '../../state/ducks/onions';
+import { buildProxyUrl, normalizeProxySettings } from './ProxySettings';
function debugLogRequestIfEnabled(params: NodeFetchParams) {
if (getFeatureFlag('debugInsecureNodeFetch')) {
@@ -52,6 +55,48 @@ export enum FetchDestination {
PUBLIC = 5,
}
+type TlsOptionKey =
+ | 'ca'
+ | 'cert'
+ | 'key'
+ | 'rejectUnauthorized'
+ | 'checkServerIdentity'
+ | 'servername'
+ | 'ciphers'
+ | 'minVersion'
+ | 'maxVersion';
+
+type NodeFetchParams = {
+ url: RequestInfo;
+ fetchOptions?: RequestInit;
+ destination: FetchDestination;
+ caller: string;
+ tlsOptions?: Partial>;
+};
+
+class SocksProxyAgentWithTls extends SocksProxyAgent {
+ private readonly tlsOptions?: Partial>;
+
+ constructor(
+ proxyUrl: string,
+ options: ConstructorParameters[1],
+ tlsOptions?: Partial>
+ ) {
+ super(proxyUrl, options);
+ this.tlsOptions = tlsOptions;
+ }
+
+ async connect(req: unknown, opts: any) {
+ if (opts?.secureEndpoint && this.tlsOptions) {
+ Object.assign(opts, this.tlsOptions);
+ }
+
+ return super.connect(req as any, opts);
+ }
+}
+
+const cachedAgents = new Map();
+
function isFetchError(e: Error): e is FetchError {
return 'code' in e;
}
@@ -60,7 +105,6 @@ function isClientOfflineFromError(e: FetchError) {
return e.code === NodeFetchErrorCode.EHOSTUNREACH || e.code === NodeFetchErrorCode.ENETUNREACH;
}
-// If a request succeeds and was for any destination on the Session Network we want to set online to true.
function handleGoOnline(response: Response, destination: FetchDestination) {
if (
response.ok &&
@@ -73,7 +117,6 @@ function handleGoOnline(response: Response, destination: FetchDestination) {
}
}
-// Only go offline if the device itself is offline
function handleGoOffline(error: Error) {
if (
!navigator.onLine ||
@@ -84,23 +127,127 @@ function handleGoOffline(error: Error) {
}
}
-type NodeFetchParams = {
- url: RequestInfo;
- fetchOptions?: RequestInit;
- destination: FetchDestination;
- caller: string;
-};
+function getProxySettings() {
+ if (typeof window === 'undefined' || !window.getSettingValue) {
+ return undefined;
+ }
+
+ return normalizeProxySettings({
+ enabled: window.getSettingValue(SettingsKey.proxyEnabled),
+ host: window.getSettingValue(SettingsKey.proxyHost),
+ port: window.getSettingValue(SettingsKey.proxyPort),
+ username: window.getSettingValue(SettingsKey.proxyUsername),
+ password: window.getSettingValue(SettingsKey.proxyPassword),
+ });
+}
+
+export function isProxyEnabled(): boolean {
+ return getProxySettings() !== undefined;
+}
+
+function hasTlsOptions(tlsOptions?: Partial>): boolean {
+ return !!tlsOptions && Object.keys(tlsOptions).length > 0;
+}
+
+export function buildTlsOptionsCacheKey(
+ tlsOptions?: Partial>
+): string | undefined {
+ if (!hasTlsOptions(tlsOptions)) {
+ return 'no-tls';
+ }
+
+ if (
+ tlsOptions &&
+ 'checkServerIdentity' in tlsOptions &&
+ typeof (tlsOptions as Record).checkServerIdentity === 'function'
+ ) {
+ return undefined;
+ }
+
+ const parts = Object.entries(tlsOptions || {})
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([key, value]) => {
+ if (Array.isArray(value)) {
+ return `${key}:${value.map(item => String(item)).join(',')}`;
+ }
+
+ return `${key}:${String(value)}`;
+ });
+
+ return parts.length ? parts.join('|') : 'no-tls';
+}
+
+function getProxyAgent(
+ tlsOptions: Partial> | undefined,
+ _destination?: FetchDestination
+): SocksProxyAgent | undefined {
+ const settings = getProxySettings();
+ if (!settings) {
+ return undefined;
+ }
+
+ const proxyUrl = buildProxyUrl(settings, { includeAuth: true, protocol: 'socks5h' });
+ const tlsOpts = hasTlsOptions(tlsOptions) ? tlsOptions : undefined;
+ const tlsOptionsKey = buildTlsOptionsCacheKey(tlsOpts);
+ const cacheKey = tlsOptionsKey ? `${proxyUrl}:${tlsOptionsKey}` : undefined;
+
+ if (cacheKey) {
+ const cachedAgent = cachedAgents.get(cacheKey);
+ if (cachedAgent) {
+ return cachedAgent;
+ }
+ }
+
+ const agent = new SocksProxyAgentWithTls(
+ proxyUrl,
+ {
+ timeout: 30000,
+ },
+ tlsOpts
+ );
+
+ if (cacheKey) {
+ cachedAgents.set(cacheKey, agent);
+ }
+
+ return agent;
+}
+
+function buildAgentForRequest(params: NodeFetchParams): RequestInit['agent'] | undefined {
+ const proxyAgent = getProxyAgent(params.tlsOptions, params.destination);
+ if (proxyAgent) {
+ window?.log?.info(
+ `insecureNodeFetch: using SOCKS5 proxy for ${FetchDestination[params.destination]}`
+ );
+ return proxyAgent;
+ }
+
+ return params.fetchOptions?.agent;
+}
export async function insecureNodeFetch(params: NodeFetchParams) {
try {
+ const finalAgent = buildAgentForRequest(params);
+ const fetchOptions = {
+ ...params.fetchOptions,
+ ...(finalAgent ? { agent: finalAgent } : {}),
+ } as RequestInit;
+
debugLogRequestIfEnabled(params);
- const result = await nodeFetch(params.url, params.fetchOptions);
+ const result = await nodeFetch(params.url, fetchOptions);
handleGoOnline(result, params.destination);
debugLogResponseIfEnabled(result);
return result;
} catch (e) {
- handleGoOffline(e);
- window?.log?.error('insecureNodeFetch', e);
+ handleGoOffline(e as Error);
+ window?.log?.error(`insecureNodeFetch error: ${(e as Error).message}`);
+ window?.log?.debug('insecureNodeFetch error details', {
+ code: (e as any).code,
+ errno: (e as any).errno,
+ syscall: (e as any).syscall,
+ type: (e as any).type,
+ stack: (e as Error).stack?.split('\n').slice(0, 3).join('\n'),
+ });
throw e;
}
}
diff --git a/ts/session/utils/ProxySettings.ts b/ts/session/utils/ProxySettings.ts
new file mode 100644
index 000000000..2c91bcaab
--- /dev/null
+++ b/ts/session/utils/ProxySettings.ts
@@ -0,0 +1,69 @@
+export type ProxySettings = {
+ enabled: true;
+ host: string;
+ port: number;
+ username?: string;
+ password?: string;
+};
+
+export type ProxySettingsInput = {
+ enabled?: unknown;
+ host?: unknown;
+ port?: unknown;
+ username?: unknown;
+ password?: unknown;
+};
+
+function normalizeString(value: unknown): string {
+ return typeof value === 'string' ? value.trim() : '';
+}
+
+function normalizePort(value: unknown): number {
+ if (typeof value === 'number' && Number.isInteger(value)) {
+ return value;
+ }
+
+ if (typeof value === 'string' && value.trim()) {
+ return Number.parseInt(value.trim(), 10);
+ }
+
+ return Number.NaN;
+}
+
+export function normalizeProxySettings(input: ProxySettingsInput): ProxySettings | undefined {
+ if (!input.enabled) {
+ return undefined;
+ }
+
+ const host = normalizeString(input.host);
+ const port = normalizePort(input.port);
+
+ if (!host || Number.isNaN(port) || port < 1 || port > 65535) {
+ return undefined;
+ }
+
+ const username = normalizeString(input.username);
+ const password = normalizeString(input.password);
+
+ return {
+ enabled: true,
+ host,
+ port,
+ username: username || undefined,
+ password: password || undefined,
+ };
+}
+
+export function buildProxyUrl(
+ settings: ProxySettings,
+ options?: { includeAuth?: boolean; protocol?: 'socks5' | 'socks5h' }
+): string {
+ const includeAuth = options?.includeAuth ?? true;
+ const protocol = options?.protocol ?? 'socks5';
+ const auth =
+ includeAuth && settings.username && settings.password
+ ? `${encodeURIComponent(settings.username)}:${encodeURIComponent(settings.password)}@`
+ : '';
+
+ return `${protocol}://${auth}${settings.host}:${settings.port}`;
+}
diff --git a/ts/test/session/unit/utils/ProxySettings_test.ts b/ts/test/session/unit/utils/ProxySettings_test.ts
new file mode 100644
index 000000000..24322d978
--- /dev/null
+++ b/ts/test/session/unit/utils/ProxySettings_test.ts
@@ -0,0 +1,79 @@
+import { expect } from 'chai';
+
+import { buildTlsOptionsCacheKey } from '../../../../session/utils/InsecureNodeFetch';
+import { buildProxyUrl, normalizeProxySettings } from '../../../../session/utils/ProxySettings';
+
+describe('ProxySettings', () => {
+ it('normalizes valid proxy settings', () => {
+ const settings = normalizeProxySettings({
+ enabled: true,
+ host: ' 127.0.0.1 ',
+ port: '9050',
+ username: ' alice ',
+ password: ' secret ',
+ });
+
+ expect(settings).to.deep.equal({
+ enabled: true,
+ host: '127.0.0.1',
+ port: 9050,
+ username: 'alice',
+ password: 'secret',
+ });
+ });
+
+ it('rejects invalid proxy settings', () => {
+ expect(
+ normalizeProxySettings({
+ enabled: true,
+ host: '',
+ port: '9050',
+ })
+ ).to.be.eq(undefined);
+
+ expect(
+ normalizeProxySettings({
+ enabled: true,
+ host: '127.0.0.1',
+ port: '99999',
+ })
+ ).to.be.eq(undefined);
+ });
+
+ it('builds proxy urls with and without auth', () => {
+ const settings = normalizeProxySettings({
+ enabled: true,
+ host: '127.0.0.1',
+ port: 9050,
+ username: 'user',
+ password: 'pass',
+ });
+
+ if (!settings) {
+ throw new Error('Expected settings to normalize');
+ }
+
+ expect(buildProxyUrl(settings, { includeAuth: true, protocol: 'socks5' })).to.be.eq(
+ 'socks5://user:pass@127.0.0.1:9050'
+ );
+ expect(buildProxyUrl(settings, { includeAuth: false, protocol: 'socks5h' })).to.be.eq(
+ 'socks5h://127.0.0.1:9050'
+ );
+ });
+
+ it('omits tls cache keys when checkServerIdentity is a function', () => {
+ expect(buildTlsOptionsCacheKey()).to.be.eq('no-tls');
+ expect(
+ buildTlsOptionsCacheKey({
+ rejectUnauthorized: false,
+ checkServerIdentity: () => undefined,
+ })
+ ).to.be.eq(undefined);
+ expect(
+ buildTlsOptionsCacheKey({
+ servername: 'seed1.getsession.org',
+ rejectUnauthorized: false,
+ })
+ ).to.be.eq('rejectUnauthorized:false|servername:seed1.getsession.org');
+ });
+});
diff --git a/ts/test/session/unit/utils/TimerBucket_test.ts b/ts/test/session/unit/utils/TimerBucket_test.ts
index 307fa9a6a..6a90e6835 100644
--- a/ts/test/session/unit/utils/TimerBucket_test.ts
+++ b/ts/test/session/unit/utils/TimerBucket_test.ts
@@ -1,4 +1,5 @@
import { expect } from 'chai';
+import Sinon from 'sinon';
import { getIncrement, getTimerBucketIcon } from '../../../../util/timer';
describe('getIncrement', () => {
@@ -54,23 +55,33 @@ describe('getIncrement', () => {
});
describe('getTimerBucketIcon', () => {
+ const mockNow = 1000000;
+
+ beforeEach(() => {
+ Sinon.stub(Date, 'now').returns(mockNow);
+ });
+
+ afterEach(() => {
+ Sinon.restore();
+ });
+
describe('absolute values', () => {
it('delta < 0', () => {
- expect(getTimerBucketIcon(Date.now() - 1000, 100)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow - 1000, 100)).to.be.equal(
'timer60',
'should have return timer60'
);
});
it('delta > length by a little', () => {
- expect(getTimerBucketIcon(Date.now() + 105, 100)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + 105, 100)).to.be.equal(
'timer00',
'should have return timer00'
);
});
it('delta > length by a lot', () => {
- expect(getTimerBucketIcon(Date.now() + 10100000, 100)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + 10100000, 100)).to.be.equal(
'timer00',
'should have return timer00'
);
@@ -80,90 +91,90 @@ describe('getTimerBucketIcon', () => {
describe('calculated values for length 1000', () => {
const length = 1000;
it('delta = 0', () => {
- expect(getTimerBucketIcon(Date.now(), length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow, length)).to.be.equal(
'timer00',
'should have return timer00'
);
});
it('delta = 1/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (1 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (1 / 12) * length, length)).to.be.equal(
'timer05',
'should have return timer05'
);
});
it('delta = 2/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (2 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (2 / 12) * length, length)).to.be.equal(
'timer10',
'should have return timer10'
);
});
it('delta = 3/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (3 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (3 / 12) * length, length)).to.be.equal(
'timer15',
'should have return timer15'
);
});
it('delta = 4/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (4 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (4 / 12) * length, length)).to.be.equal(
'timer20',
'should have return timer20'
);
});
it('delta = 5/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (5 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (5 / 12) * length, length)).to.be.equal(
'timer25',
'should have return timer25'
);
});
it('delta = 6/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (6 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (6 / 12) * length, length)).to.be.equal(
'timer30',
'should have return timer30'
);
});
it('delta = 7/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (7 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (7 / 12) * length, length)).to.be.equal(
'timer35',
'should have return timer35'
);
});
it('delta = 8/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (8 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (8 / 12) * length, length)).to.be.equal(
'timer40',
'should have return timer40'
);
});
it('delta = 9/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (9 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (9 / 12) * length, length)).to.be.equal(
'timer45',
'should have return timer45'
);
});
it('delta = 10/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (10 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (10 / 12) * length, length)).to.be.equal(
'timer50',
'should have return timer50'
);
});
it('delta = 11/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (11 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (11 / 12) * length, length)).to.be.equal(
'timer55',
'should have return timer55'
);
});
it('delta = 12/12 of length', () => {
- expect(getTimerBucketIcon(Date.now() + (12 / 12) * length, length)).to.be.equal(
+ expect(getTimerBucketIcon(mockNow + (12 / 12) * length, length)).to.be.equal(
'timer60',
'should have return timer60'
);
diff --git a/ts/updater/updater.ts b/ts/updater/updater.ts
index 6e78ad0a3..e513a9414 100644
--- a/ts/updater/updater.ts
+++ b/ts/updater/updater.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable no-console */
-import { app, type BrowserWindow } from 'electron';
+import { app, session, type BrowserWindow } from 'electron';
import { autoUpdater, DOWNLOAD_PROGRESS, type UpdateInfo } from 'electron-updater';
import * as fs from 'fs-extra';
import * as path from 'path';
@@ -15,11 +15,15 @@ import { getLatestRelease } from '../node/latest_desktop_release';
import { Errors } from '../types/Errors';
import type { LoggerType } from '../util/logger/Logging';
import { isDebianBased, isRunningViaAppImage } from '../OS';
+import { sqlNode } from '../node/sql';
+import { SettingsKey } from '../data/settings-key';
+import { buildProxyUrl, normalizeProxySettings } from '../session/utils/ProxySettings';
let isUpdating = false;
let downloadIgnored = false;
let interval: NodeJS.Timeout | undefined;
let stopped = false;
+let lastAppliedProxy: string | null = null;
autoUpdater.on(DOWNLOAD_PROGRESS, eventDownloadProgress => {
console.log(
@@ -74,6 +78,8 @@ export async function checkForUpdates(
logger: LoggerType,
force?: boolean
) {
+ await configureUpdaterProxy(logger);
+
if (stopped || isUpdating || (downloadIgnored && !force)) {
logger.info(
`[updater] checkForUpdates is returning early stopped ${stopped} isUpdating ${isUpdating} downloadIgnored ${downloadIgnored}`
@@ -239,3 +245,46 @@ export async function canAutoUpdate(): Promise {
return false;
}
}
+
+async function configureUpdaterProxy(logger: LoggerType) {
+ const settings = normalizeProxySettings({
+ enabled: sqlNode.getItemById(SettingsKey.proxyEnabled)?.value,
+ host: sqlNode.getItemById(SettingsKey.proxyHost)?.value,
+ port: sqlNode.getItemById(SettingsKey.proxyPort)?.value,
+ username: sqlNode.getItemById(SettingsKey.proxyUsername)?.value,
+ password: sqlNode.getItemById(SettingsKey.proxyPassword)?.value,
+ });
+ const updaterAny = autoUpdater as any;
+ const getUpdaterSession = () => updaterAny._netSession || session.defaultSession;
+ const setUpdaterSession = (targetSession: Electron.Session | null) => {
+ updaterAny._netSession = targetSession;
+ };
+
+ if (!settings) {
+ if (lastAppliedProxy) {
+ setUpdaterSession(null);
+ lastAppliedProxy = null;
+ }
+ return;
+ }
+
+ try {
+ const proxyRules = buildProxyUrl(settings, { includeAuth: false, protocol: 'socks5' });
+ const proxyUrl = buildProxyUrl(settings, { includeAuth: true, protocol: 'socks5' });
+ const targetSession = session.defaultSession;
+
+ if (lastAppliedProxy === proxyUrl && getUpdaterSession() === targetSession) {
+ return;
+ }
+
+ await targetSession.setProxy({
+ proxyRules,
+ proxyBypassRules: '',
+ });
+
+ setUpdaterSession(targetSession);
+ lastAppliedProxy = proxyUrl;
+ } catch (error) {
+ logger.warn('[updater] failed to apply proxy for auto-updater', Errors.toString(error));
+ }
+}