Merge branch 'v2.0' into task/refactor-spn
This commit is contained in:
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Release
|
name: Release v2.X
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -36,6 +36,8 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
installer-linux:
|
installer-linux:
|
||||||
|
#JOB DISABLED FOR NOW
|
||||||
|
if: false
|
||||||
name: Installer linux
|
name: Installer linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: release-prep
|
needs: release-prep
|
||||||
@@ -63,6 +65,8 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
installer-windows:
|
installer-windows:
|
||||||
|
#JOB DISABLED FOR NOW
|
||||||
|
if: false
|
||||||
name: Installer windows
|
name: Installer windows
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: release-prep
|
needs: release-prep
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -57,3 +57,10 @@ windows_core_dll/.vs/windows_core_dll
|
|||||||
#windows_core_dll
|
#windows_core_dll
|
||||||
windows_core_dll/x64/
|
windows_core_dll/x64/
|
||||||
windows_core_dll/portmaster-core/x64/
|
windows_core_dll/portmaster-core/x64/
|
||||||
|
|
||||||
|
#Tauri-generated files
|
||||||
|
desktop/tauri/src-tauri/gen/
|
||||||
|
|
||||||
|
#Binaries used for installer gereneration for Windows
|
||||||
|
desktop/tauri/src-tauri/binary/
|
||||||
|
desktop/tauri/src-tauri/intel/
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ angular-base:
|
|||||||
COPY desktop/angular/ .
|
COPY desktop/angular/ .
|
||||||
# Remove symlink and copy assets directly.
|
# Remove symlink and copy assets directly.
|
||||||
RUN rm ./assets
|
RUN rm ./assets
|
||||||
COPY assets/data ./assets
|
# COPY assets/data ./assets # Do not include the assets folder into portmaster.zip, we use the assets.zip instead
|
||||||
|
|
||||||
IF [ "${configuration}" = "production" ]
|
IF [ "${configuration}" = "production" ]
|
||||||
RUN --no-cache npm run build-libs
|
RUN --no-cache npm run build-libs
|
||||||
@@ -603,6 +603,10 @@ installer-linux:
|
|||||||
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/deb/*.deb" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
||||||
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
SAVE ARTIFACT --if-exists --keep-ts "target/${target}/release/bundle/rpm/*.rpm" AS LOCAL "${outputDir}/${GO_ARCH_STRING}/"
|
||||||
|
|
||||||
|
all-artifacts:
|
||||||
|
BUILD +release-prep
|
||||||
|
BUILD +installer-linux
|
||||||
|
|
||||||
kext-build:
|
kext-build:
|
||||||
FROM ${rust_builder_image}
|
FROM ${rust_builder_image}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ func (s *WindowsSystemService) Execute(args []string, changeRequests <-chan svc.
|
|||||||
syscall.SIGTERM,
|
syscall.SIGTERM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
isShuttingDown := false
|
||||||
// Wait for shutdown signal.
|
// Wait for shutdown signal.
|
||||||
waitSignal:
|
waitSignal:
|
||||||
for {
|
for {
|
||||||
@@ -119,12 +120,16 @@ waitSignal:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-s.instance.ShuttingDown():
|
case <-s.instance.ShuttingDown():
|
||||||
|
isShuttingDown = true
|
||||||
break waitSignal
|
break waitSignal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger shutdown.
|
// Trigger shutdown,
|
||||||
s.instance.Shutdown()
|
// but only if we are not already shutting down.
|
||||||
|
if !isShuttingDown {
|
||||||
|
s.instance.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
// Notify the service host that service is in shutting down state.
|
// Notify the service host that service is in shutting down state.
|
||||||
changes <- svc.Status{State: svc.StopPending}
|
changes <- svc.Status{State: svc.StopPending}
|
||||||
|
|||||||
80
desktop/angular/package-lock.json
generated
80
desktop/angular/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "portmaster",
|
"name": "portmaster",
|
||||||
"version": "2.0.1",
|
"version": "2.0.14",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "portmaster",
|
"name": "portmaster",
|
||||||
"version": "2.0.1",
|
"version": "2.0.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^16.0.1",
|
"@angular/animations": "^16.0.1",
|
||||||
"@angular/cdk": "^16.0.1",
|
"@angular/cdk": "^16.0.1",
|
||||||
@@ -28,11 +28,10 @@
|
|||||||
"@tauri-apps/plugin-cli": ">=2.0.0",
|
"@tauri-apps/plugin-cli": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": ">=2.0.0",
|
"@tauri-apps/plugin-dialog": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-http": ">=2.2.0",
|
|
||||||
"@tauri-apps/plugin-notification": ">=2.0.0",
|
"@tauri-apps/plugin-notification": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||||
"@tauri-apps/plugin-websocket": ">=2.2.0",
|
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"d3": "^7.8.4",
|
"d3": "^7.8.4",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -1190,9 +1189,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ant-design/fast-color/node_modules/@babel/runtime": {
|
"node_modules/@ant-design/fast-color/node_modules/@babel/runtime": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||||
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
|
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
@@ -1662,39 +1661,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
|
||||||
"integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==",
|
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.26.9",
|
"@babel/template": "^7.27.0",
|
||||||
"@babel/types": "^7.26.9"
|
"@babel/types": "^7.27.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers/node_modules/@babel/template": {
|
"node_modules/@babel/helpers/node_modules/@babel/template": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
||||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/parser": "^7.26.9",
|
"@babel/parser": "^7.27.0",
|
||||||
"@babel/types": "^7.26.9"
|
"@babel/types": "^7.27.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||||
"integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
|
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.9"
|
"@babel/types": "^7.27.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -3125,9 +3124,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.26.9",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||||
"integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
|
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.25.9",
|
"@babel/helper-string-parser": "^7.25.9",
|
||||||
@@ -4846,15 +4845,6 @@
|
|||||||
"@tauri-apps/api": "^2.0.0"
|
"@tauri-apps/api": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/plugin-http": {
|
|
||||||
"version": "2.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.4.2.tgz",
|
|
||||||
"integrity": "sha512-deoafidYelei/fmd4AQoHa2aCA9N2DvnnQrF/91QNjE0xCCTuVpPhIQdVRgdHDhFehEal9uI14OTvERBpcfHrg==",
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@tauri-apps/api": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tauri-apps/plugin-notification": {
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.2.1.tgz",
|
||||||
@@ -4874,9 +4864,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/plugin-shell": {
|
"node_modules/@tauri-apps/plugin-shell": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.1.tgz",
|
||||||
"integrity": "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==",
|
"integrity": "sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0"
|
"@tauri-apps/api": "^2.0.0"
|
||||||
@@ -7075,9 +7065,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.9",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11924,9 +11914,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-proxy-middleware": {
|
"node_modules/http-proxy-middleware": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17424,9 +17414,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prismjs": {
|
"node_modules/prismjs": {
|
||||||
"version": "1.29.0",
|
"version": "1.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portmaster",
|
"name": "portmaster",
|
||||||
"version": "2.0.1",
|
"version": "2.0.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json",
|
"start": "npm install && npm run build-libs:dev && ng serve --proxy-config ./proxy.json",
|
||||||
@@ -45,8 +45,7 @@
|
|||||||
"@tauri-apps/plugin-notification": ">=2.0.0",
|
"@tauri-apps/plugin-notification": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||||
"@tauri-apps/plugin-http": ">=2.2.0",
|
"@tauri-apps/plugin-websocket": "^2.3.0",
|
||||||
"@tauri-apps/plugin-websocket": ">=2.2.0",
|
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"d3": "^7.8.4",
|
"d3": "^7.8.4",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
@@ -105,4 +104,4 @@
|
|||||||
"webpack-ext-reloader": "^1.1.9",
|
"webpack-ext-reloader": "^1.1.9",
|
||||||
"zip-a-folder": "^1.1.5"
|
"zip-a-folder": "^1.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import {
|
|||||||
} from './portapi.service';
|
} from './portapi.service';
|
||||||
import { Process } from './portapi.types';
|
import { Process } from './portapi.types';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable()
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class AppProfileService {
|
export class AppProfileService {
|
||||||
private watchedProfiles = new Map<string, Observable<AppProfile>>();
|
private watchedProfiles = new Map<string, Observable<AppProfile>>();
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export interface AuthKeyResponse {
|
|||||||
export class MetaAPI {
|
export class MetaAPI {
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
@Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string = 'http://localhost:817/api',
|
@Inject(PORTMASTER_HTTP_API_ENDPOINT) @Optional() private httpEndpoint: string = 'http://127.0.0.1:817/api',
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
listEndpoints(): Observable<MetaEndpoint[]> {
|
listEndpoints(): Observable<MetaEndpoint[]> {
|
||||||
|
|||||||
@@ -7,12 +7,37 @@ import { Netquery } from "./netquery.service";
|
|||||||
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service";
|
import { PortapiService, PORTMASTER_HTTP_API_ENDPOINT, PORTMASTER_WS_API_ENDPOINT } from "./portapi.service";
|
||||||
import { SPNService } from "./spn.service";
|
import { SPNService } from "./spn.service";
|
||||||
import { WebsocketService } from "./websocket.service";
|
import { WebsocketService } from "./websocket.service";
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
|
import { TauriHttpInterceptor } from "./platform-specific/tauri/tauri-http-interceptor";
|
||||||
|
import { IsTauriEnvironment } from "./platform-specific/utils";
|
||||||
|
|
||||||
export interface ModuleConfig {
|
export interface ModuleConfig {
|
||||||
httpAPI?: string;
|
httpAPI?: string;
|
||||||
websocketAPI?: string;
|
websocketAPI?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Factory function to provide the appropriate HTTP client configuration
|
||||||
|
//
|
||||||
|
// This function determines the appropriate HTTP client configuration based on the runtime environment.
|
||||||
|
// If the application is running in a Tauri environment, it uses the TauriHttpInterceptor to ensure
|
||||||
|
// that all HTTP requests are made from the application binary instead of the WebView instance.
|
||||||
|
// This allows for more direct and controlled communication with the Portmaster API.
|
||||||
|
// In other environments (e.g., browser, Electron), the standard HttpClient is used without any interceptors.
|
||||||
|
export function HttpClientProviderFactory() {
|
||||||
|
if (IsTauriEnvironment())
|
||||||
|
{
|
||||||
|
console.log("[portmaster-api] Running under Tauri - using TauriHttpClient");
|
||||||
|
return provideHttpClient(
|
||||||
|
withInterceptors([TauriHttpInterceptor])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("[portmaster-api] Running in browser - using default HttpClient");
|
||||||
|
return provideHttpClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({})
|
@NgModule({})
|
||||||
export class PortmasterAPIModule {
|
export class PortmasterAPIModule {
|
||||||
|
|
||||||
@@ -23,15 +48,16 @@ export class PortmasterAPIModule {
|
|||||||
*/
|
*/
|
||||||
static forRoot(cfg: ModuleConfig = {}): ModuleWithProviders<PortmasterAPIModule> {
|
static forRoot(cfg: ModuleConfig = {}): ModuleWithProviders<PortmasterAPIModule> {
|
||||||
if (cfg.httpAPI === undefined) {
|
if (cfg.httpAPI === undefined) {
|
||||||
cfg.httpAPI = `http://${window.location.host}/api`;
|
cfg.httpAPI = `http://127.0.0.1:817/api`;
|
||||||
}
|
}
|
||||||
if (cfg.websocketAPI === undefined) {
|
if (cfg.websocketAPI === undefined) {
|
||||||
cfg.websocketAPI = `ws://${window.location.host}/api/database/v1`;
|
cfg.websocketAPI = `ws://127.0.0.1:817/api/database/v1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ngModule: PortmasterAPIModule,
|
ngModule: PortmasterAPIModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
HttpClientProviderFactory(),
|
||||||
PortapiService,
|
PortapiService,
|
||||||
WebsocketService,
|
WebsocketService,
|
||||||
MetaAPI,
|
MetaAPI,
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import { HttpEvent, HttpHandlerFn, HttpRequest, HttpResponse, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { from, Observable, switchMap, map, catchError, throwError } from 'rxjs';
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TauriHttpInterceptor intercepts HTTP requests and routes them through Tauri's `@tauri-apps/plugin-http` API.
|
||||||
|
*
|
||||||
|
* This allows HTTP requests to be executed from the Tauri application binary instead of the WebView,
|
||||||
|
* enabling more secure and direct communication with external APIs.
|
||||||
|
*
|
||||||
|
* The interceptor handles various response types (e.g., JSON, text, blob, arraybuffer) and ensures
|
||||||
|
* that headers and response data are properly mapped to Angular's HttpResponse format.
|
||||||
|
*
|
||||||
|
* References:
|
||||||
|
* - https://angular.dev/guide/http/interceptors
|
||||||
|
*/
|
||||||
|
export function TauriHttpInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: req.method,
|
||||||
|
headers: req.headers.keys().reduce((acc: Record<string, string>, key) => {
|
||||||
|
acc[key] = req.headers.get(key) || '';
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
body: getRequestBody(req),
|
||||||
|
};
|
||||||
|
//console.log('[TauriHttpInterceptor] Fetching:', req.url, "Headers:", fetchOptions.headers);
|
||||||
|
return from(send_tauri_http_request(req.url, fetchOptions)).pipe(
|
||||||
|
switchMap(response => {
|
||||||
|
// Copy all response headers
|
||||||
|
const headerMap: Record<string, string> = {};
|
||||||
|
response.headers.forEach((value: string, key: string) => {
|
||||||
|
headerMap[key] = value;
|
||||||
|
});
|
||||||
|
const headers = new HttpHeaders(headerMap);
|
||||||
|
|
||||||
|
// Check if response status is ok (2xx)
|
||||||
|
if (!response.ok) {
|
||||||
|
// Get the error content
|
||||||
|
return from(response.text()).pipe(
|
||||||
|
map(errorText => {
|
||||||
|
throw new HttpErrorResponse({
|
||||||
|
error: errorText,
|
||||||
|
headers: headers,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
url: req.url
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the response type from the request
|
||||||
|
const responseType = req.responseType || 'json';
|
||||||
|
|
||||||
|
// Helper function to create HttpResponse from body
|
||||||
|
const createResponse = (body: any): HttpEvent<unknown> => {
|
||||||
|
return new HttpResponse({
|
||||||
|
body,
|
||||||
|
status: response.status,
|
||||||
|
headers: headers,
|
||||||
|
url: req.url
|
||||||
|
}) as HttpEvent<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (responseType) {
|
||||||
|
case 'text':
|
||||||
|
return from(response.text()).pipe(map(createResponse));
|
||||||
|
case 'arraybuffer':
|
||||||
|
return from(response.arrayBuffer()).pipe(map(createResponse));
|
||||||
|
case 'blob':
|
||||||
|
return from(response.blob()).pipe(
|
||||||
|
map(blob => {
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
// Create a new blob with the proper MIME type
|
||||||
|
if (contentType && (!blob.type || blob.type === 'application/octet-stream')) {
|
||||||
|
const typedBlob = new Blob([blob], { type: contentType });
|
||||||
|
return createResponse(typedBlob);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createResponse(blob);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
case 'json':
|
||||||
|
default:
|
||||||
|
return from(response.text()).pipe(
|
||||||
|
map(body => {
|
||||||
|
let parsedBody: any;
|
||||||
|
try {
|
||||||
|
// Only attempt to parse as JSON if we have content
|
||||||
|
// and either explicitly requested JSON or content-type is JSON
|
||||||
|
if (body && (responseType === 'json' ||
|
||||||
|
(response.headers.get('content-type') || '').includes('application/json'))) {
|
||||||
|
parsedBody = JSON.parse(body);
|
||||||
|
} else {
|
||||||
|
parsedBody = body;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TauriHttpInterceptor] Failed to parse JSON response:', e);
|
||||||
|
parsedBody = body;
|
||||||
|
}
|
||||||
|
return createResponse(parsedBody);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('[TauriHttpInterceptor] Request failed:', error);
|
||||||
|
|
||||||
|
// If it's already an HttpErrorResponse, just return it
|
||||||
|
if (error instanceof HttpErrorResponse) {
|
||||||
|
return throwError(() => error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise create a new HttpErrorResponse with available information
|
||||||
|
return throwError(() => new HttpErrorResponse({
|
||||||
|
error: error.message || 'Unknown error occurred',
|
||||||
|
status: error.status || 0,
|
||||||
|
statusText: error.statusText || 'Unknown Error',
|
||||||
|
url: req.url,
|
||||||
|
headers: error.headers ? new HttpHeaders(error.headers) : new HttpHeaders()
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestBody(req: HttpRequest<unknown>): any {
|
||||||
|
if (!req.body) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different body types properly
|
||||||
|
if (req.body instanceof FormData ||
|
||||||
|
req.body instanceof Blob ||
|
||||||
|
req.body instanceof ArrayBuffer ||
|
||||||
|
req.body instanceof URLSearchParams) {
|
||||||
|
return req.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to JSON stringify for object data
|
||||||
|
return JSON.stringify(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function send_tauri_http_request(
|
||||||
|
url: string,
|
||||||
|
init: RequestInit = {}
|
||||||
|
): Promise<Response> {
|
||||||
|
// Extract method, headers, and body buffer
|
||||||
|
const method = init.method || 'GET';
|
||||||
|
const headers = [...(init.headers instanceof Headers
|
||||||
|
? (() => {
|
||||||
|
const headerArray: [string, string][] = [];
|
||||||
|
init.headers.forEach((value, key) => headerArray.push([key, value]));
|
||||||
|
return headerArray;
|
||||||
|
})()
|
||||||
|
: Object.entries(init.headers || {}))];
|
||||||
|
const body = init.body
|
||||||
|
? new Uint8Array(await new Response(init.body as any).arrayBuffer())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const res = await invoke<{
|
||||||
|
status: number;
|
||||||
|
status_text: string;
|
||||||
|
headers: [string, string][];
|
||||||
|
body: number[];
|
||||||
|
}>('send_tauri_http_request', { url, opts: { method, headers, body } });
|
||||||
|
|
||||||
|
return new Response(new Uint8Array(res.body), {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.status_text,
|
||||||
|
headers: res.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import WebSocket, { ConnectionConfig, Message } from '@tauri-apps/plugin-websocket';
|
||||||
|
import { Subject, Observable } from 'rxjs';
|
||||||
|
import { WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
|
||||||
|
const LOG_PREFIX = '[tauri_ws]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a WebSocket connection using the Tauri WebSocket API and wraps it in an RxJS WebSocketSubject-compatible interface.
|
||||||
|
*
|
||||||
|
* @template T - The type of messages sent and received through the WebSocket.
|
||||||
|
* @param {WebSocketSubjectConfig<T>} opts - Configuration options for the WebSocket connection.
|
||||||
|
* @param {NgZone} ngZone - Angular's NgZone to ensure change detection runs properly.
|
||||||
|
* @returns {WebSocketSubject<T>} - An RxJS WebSocketSubject-compatible object for interacting with the WebSocket.
|
||||||
|
* @throws {Error} If the `serializer` or `deserializer` functions are not provided.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const wsSubject = createTauriWsConnection({
|
||||||
|
* url: 'ws://example.com',
|
||||||
|
* serializer: JSON.stringify,
|
||||||
|
* deserializer: JSON.parse,
|
||||||
|
* }, ngZone);
|
||||||
|
*/
|
||||||
|
export function createTauriWsConnection<T>(opts: WebSocketSubjectConfig<T>, ngZone: NgZone): WebSocketSubject<T> {
|
||||||
|
if (!opts.serializer) throw new Error(`${LOG_PREFIX} Messages Serializer not provided!`);
|
||||||
|
if (!opts.deserializer) throw new Error(`${LOG_PREFIX} Messages Deserializer not provided!`);
|
||||||
|
|
||||||
|
const serializer = opts.serializer;
|
||||||
|
const deserializer = opts.deserializer;
|
||||||
|
|
||||||
|
let wsConnection: WebSocket | null = null;
|
||||||
|
const messageSubject = new Subject<T>();
|
||||||
|
const observable$ = messageSubject.asObservable();
|
||||||
|
|
||||||
|
// A queue for messages that need to be sent before the connection is established
|
||||||
|
const pendingMessages: T[] = [];
|
||||||
|
|
||||||
|
const notifySubjectError = (descriptionToLog: string, error: Error | any | null = null) => {
|
||||||
|
if (!descriptionToLog) return;
|
||||||
|
if (!error) error = new Error(descriptionToLog);
|
||||||
|
console.error(`${LOG_PREFIX} ${descriptionToLog}:`, error);
|
||||||
|
|
||||||
|
// Run inside NgZone to ensure Angular detects this change
|
||||||
|
ngZone.run(() => {
|
||||||
|
// This completes the observable and prevents further messages from being processed.
|
||||||
|
messageSubject.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
// RxJS WebSocketSubject-compatible implementation
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
const webSocketSubject = {
|
||||||
|
// Standard Observer interface methods
|
||||||
|
next: (message: T) => {
|
||||||
|
if (!wsConnection) {
|
||||||
|
if (pendingMessages.length >= 1000) {
|
||||||
|
console.error(`${LOG_PREFIX} Too many pending messages, skipping message`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingMessages.push(message);
|
||||||
|
console.log(`${LOG_PREFIX} Connection not established yet, message queued`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let serializedMessage: any;
|
||||||
|
try {
|
||||||
|
serializedMessage = serializer(message);
|
||||||
|
// 'string' type is enough here, since default serializer for portmaster message returns string
|
||||||
|
if (typeof serializedMessage !== 'string')
|
||||||
|
throw new Error('Serialized message is not a string');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${LOG_PREFIX} Error serializing message:`, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run outside NgZone for better performance during send operations
|
||||||
|
ngZone.runOutsideAngular(() => {
|
||||||
|
try {
|
||||||
|
wsConnection!.send(serializedMessage).catch((err: Error) => {
|
||||||
|
notifySubjectError('Error sending text message', err);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
notifySubjectError('Error sending message', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
complete: () => {
|
||||||
|
if (wsConnection) {
|
||||||
|
console.log(`${LOG_PREFIX} Closing connection`);
|
||||||
|
|
||||||
|
// Run inside NgZone to ensure Angular detects this change
|
||||||
|
ngZone.run(() => {
|
||||||
|
if (opts.closingObserver?.next) {
|
||||||
|
opts.closingObserver.next(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConnection!.disconnect().catch((err: Error) => console.error(`${LOG_PREFIX} Error closing connection:`, err));
|
||||||
|
wsConnection = null;
|
||||||
|
messageSubject.complete();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messageSubject.complete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// RxJS Observable methods required for compatibility
|
||||||
|
pipe: function(): Observable<any> {
|
||||||
|
// @ts-ignore - Ignore the parameter type mismatch
|
||||||
|
return observable$.pipe(...arguments);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
// Connect to WebSocket
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
console.log(`${LOG_PREFIX} Connecting to WebSocket:`, opts.url);
|
||||||
|
|
||||||
|
// Connect outside of Angular zone for better performance
|
||||||
|
ngZone.runOutsideAngular(() => {
|
||||||
|
WebSocket.connect(opts.url)
|
||||||
|
.then((ws) => {
|
||||||
|
wsConnection = ws;
|
||||||
|
console.log(`${LOG_PREFIX} Connection established`);
|
||||||
|
|
||||||
|
// Run inside NgZone to ensure Angular detects this connection event
|
||||||
|
ngZone.run(() => {
|
||||||
|
// Create a mock Event for the openObserver
|
||||||
|
if (opts.openObserver) {
|
||||||
|
const mockEvent = new Event('open') as Event;
|
||||||
|
opts.openObserver.next(mockEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send any pending messages
|
||||||
|
while (pendingMessages.length > 0) {
|
||||||
|
const message = pendingMessages.shift();
|
||||||
|
if (message) webSocketSubject.next(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add a single listener for ALL message types according to Tauri WebSocket API
|
||||||
|
ws.addListener((message: Message) => {
|
||||||
|
// Process message inside ngZone to trigger change detection
|
||||||
|
ngZone.run(() => {
|
||||||
|
try {
|
||||||
|
// Handle different message types from Tauri
|
||||||
|
switch (message.type) {
|
||||||
|
case 'Text':
|
||||||
|
const textData = message.data as string;
|
||||||
|
try {
|
||||||
|
const deserializedMessage = deserializer({ data: textData } as any);
|
||||||
|
messageSubject.next(deserializedMessage);
|
||||||
|
} catch (err) {
|
||||||
|
notifySubjectError('Error deserializing text message', err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Binary':
|
||||||
|
const binaryData = message.data as number[];
|
||||||
|
try {
|
||||||
|
const uint8Array = new Uint8Array(binaryData);
|
||||||
|
const buffer = uint8Array.buffer;
|
||||||
|
const deserializedMessage = deserializer({ data: buffer } as any);
|
||||||
|
messageSubject.next(deserializedMessage);
|
||||||
|
} catch (err) {
|
||||||
|
notifySubjectError('Error deserializing binary message', err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Close':
|
||||||
|
// Handle close message
|
||||||
|
const closeData = message.data as { code: number; reason: string } | null;
|
||||||
|
console.log(`${LOG_PREFIX} Connection closed by server`, closeData);
|
||||||
|
|
||||||
|
if (opts.closeObserver) {
|
||||||
|
const closeEvent = {
|
||||||
|
code: closeData?.code || 1000,
|
||||||
|
reason: closeData?.reason || '',
|
||||||
|
wasClean: true,
|
||||||
|
type: 'close',
|
||||||
|
target: null
|
||||||
|
} as unknown as CloseEvent;
|
||||||
|
|
||||||
|
opts.closeObserver.next(closeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSubject.complete();
|
||||||
|
wsConnection = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Ping':
|
||||||
|
console.log(`${LOG_PREFIX} Received ping`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Pong':
|
||||||
|
console.log(`${LOG_PREFIX} Received pong`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${LOG_PREFIX} Error processing message:`, error);
|
||||||
|
// Don't error the subject on message processing errors to keep connection alive
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${LOG_PREFIX} Listener added successfully`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
notifySubjectError('Error adding message listener', error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
notifySubjectError('Connection failed', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cast to WebSocketSubject<T>
|
||||||
|
return webSocketSubject as unknown as WebSocketSubject<T>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Simple function to detect if the app is running in a Tauri environment
|
||||||
|
export function IsTauriEnvironment(): boolean {
|
||||||
|
return '__TAURI__' in window;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { FeatureID } from "./features";
|
|||||||
import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from './portapi.service';
|
import { PORTMASTER_HTTP_API_ENDPOINT, PortapiService } from './portapi.service';
|
||||||
import { Feature, Pin, SPNStatus, UserProfile } from "./spn.types";
|
import { Feature, Pin, SPNStatus, UserProfile } from "./spn.types";
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable()
|
||||||
export class SPNService {
|
export class SPNService {
|
||||||
|
|
||||||
/** Emits the SPN status whenever it changes */
|
/** Emits the SPN status whenever it changes */
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from 'rxjs/webSocket';
|
||||||
|
import { createTauriWsConnection } from './platform-specific/tauri/tauri-websocket-subject';
|
||||||
|
import { IsTauriEnvironment } from './platform-specific/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebsocketService {
|
export class WebsocketService {
|
||||||
constructor() { }
|
constructor(private ngZone: NgZone) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* createConnection creates a new websocket connection using opts.
|
* createConnection creates a new websocket connection using opts.
|
||||||
@@ -11,7 +13,12 @@ export class WebsocketService {
|
|||||||
* @param opts Options for the websocket connection.
|
* @param opts Options for the websocket connection.
|
||||||
*/
|
*/
|
||||||
createConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
createConnection<T>(opts: WebSocketSubjectConfig<T>): WebSocketSubject<T> {
|
||||||
return webSocket(opts);
|
if (IsTauriEnvironment()) {
|
||||||
}
|
console.log('[portmaster-api] Running under Tauri - Using Tauri WebSocket');
|
||||||
}
|
return createTauriWsConnection<T>(opts, this.ngZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[portmaster-api] Running in browser - Using RxJS WebSocket');
|
||||||
|
return webSocket<T>(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import { PortalModule } from '@angular/cdk/portal';
|
|||||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||||
import { CdkTableModule } from '@angular/cdk/table';
|
import { CdkTableModule } from '@angular/cdk/table';
|
||||||
import { CommonModule, registerLocaleData } from '@angular/common';
|
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
|
||||||
|
|
||||||
import { APP_INITIALIZER, LOCALE_ID, NgModule } from '@angular/core';
|
import { APP_INITIALIZER, LOCALE_ID, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
@@ -174,7 +173,6 @@ const localeConfig = {
|
|||||||
PortalModule,
|
PortalModule,
|
||||||
CdkTableModule,
|
CdkTableModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
HttpClientModule,
|
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
SfngAccordionModule,
|
SfngAccordionModule,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function asyncInvoke<T>(method: string, args: object): Promise<T> {
|
|||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const eventId = uuid();
|
const eventId = uuid();
|
||||||
|
|
||||||
once<T & { error: string }>(eventId, (event) => {
|
const listenerPromise = once<T & { error: string }>(eventId, (event) => {
|
||||||
if (typeof event.payload === 'object' && 'error' in event.payload) {
|
if (typeof event.payload === 'object' && 'error' in event.payload) {
|
||||||
reject(event.payload);
|
reject(event.payload);
|
||||||
return
|
return
|
||||||
@@ -33,14 +33,17 @@ function asyncInvoke<T>(method: string, args: object): Promise<T> {
|
|||||||
resolve(event.payload);
|
resolve(event.payload);
|
||||||
})
|
})
|
||||||
|
|
||||||
invoke<string>(method, {
|
// Only make the invoke call after the listener is registered
|
||||||
...args,
|
listenerPromise.then(() => {
|
||||||
responseId: eventId,
|
invoke<string>(method, {
|
||||||
}).catch((err: any) => {
|
...args,
|
||||||
console.error("tauri:invoke rejected: ", method, args, err);
|
responseId: eventId,
|
||||||
reject(err)
|
}).catch((err: any) => {
|
||||||
});
|
console.error("tauri:invoke rejected: ", method, args, err);
|
||||||
})
|
reject(err)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceManagerStatus = 'Running' | 'Stopped' | 'NotFound' | 'unsupported service manager' | 'unsupported operating system';
|
export type ServiceManagerStatus = 'Running' | 'Stopped' | 'NotFound' | 'unsupported service manager' | 'unsupported operating system';
|
||||||
|
|||||||
@@ -348,11 +348,13 @@ export class DashboardPageComponent implements OnInit, AfterViewInit {
|
|||||||
)
|
)
|
||||||
.subscribe(response => {
|
.subscribe(response => {
|
||||||
// bandwidth bar chart
|
// bandwidth bar chart
|
||||||
const barChartData = response.bwBarChart
|
if (response?.bwBarChart){
|
||||||
.filter(value => (value.sent + value.received) > 0)
|
const barChartData = response.bwBarChart
|
||||||
.sort((a, b) => (b.sent + b.received) - (a.sent + a.received))
|
.filter(value => (value.sent + value.received) > 0)
|
||||||
.slice(0, 10);
|
.sort((a, b) => (b.sent + b.received) - (a.sent + a.received))
|
||||||
this.bandwidthBarData = splitQueryResult(barChartData, ['sent', 'received']) as BandwidthBarData[]
|
.slice(0, 10);
|
||||||
|
this.bandwidthBarData = splitQueryResult(barChartData, ['sent', 'received']) as BandwidthBarData[]
|
||||||
|
}
|
||||||
|
|
||||||
// profileCount
|
// profileCount
|
||||||
this.blockedConnections = 0;
|
this.blockedConnections = 0;
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ if (location.pathname !== "/prompt") {
|
|||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
importProvidersFrom(PortmasterAPIModule.forRoot({
|
importProvidersFrom(PortmasterAPIModule.forRoot({
|
||||||
websocketAPI: "ws://localhost:817/api/database/v1",
|
websocketAPI: "ws://127.0.0.1:817/api/database/v1",
|
||||||
httpAPI: "http://localhost:817/api"
|
httpAPI: "http://127.0.0.1:817/api"
|
||||||
})),
|
})),
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Struct representing an RGB color
|
/// Struct representing an RGB color
|
||||||
|
#[allow(dead_code)] // Suppress warnings for unused fields in this struct only
|
||||||
pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32);
|
pub(crate) struct Rgb(pub(crate) u32, pub(crate) u32, pub(crate) u32);
|
||||||
|
|
||||||
impl FromStr for Rgb {
|
impl FromStr for Rgb {
|
||||||
|
|||||||
115
desktop/tauri/src-tauri/Cargo.lock
generated
115
desktop/tauri/src-tauri/Cargo.lock
generated
@@ -1227,12 +1227,6 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "data-url"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dataurl"
|
name = "dataurl"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -2026,10 +2020,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2039,11 +2031,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasi 0.14.2+wasi-0.2.4",
|
"wasi 0.14.2+wasi-0.2.4",
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2381,7 +2371,6 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.2",
|
"tokio-rustls 0.26.2",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"webpki-roots",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3956,7 +3945,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portmaster"
|
name = "portmaster"
|
||||||
version = "2.0.0"
|
version = "2.0.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert_matches",
|
"assert_matches",
|
||||||
"cached",
|
"cached",
|
||||||
@@ -3988,7 +3977,6 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-http",
|
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
@@ -4158,60 +4146,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.11.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"cfg_aliases 0.2.1",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.23.25",
|
|
||||||
"socket2 0.5.9",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.11.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"getrandom 0.3.2",
|
|
||||||
"rand 0.9.0",
|
|
||||||
"ring",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.23.25",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"slab",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.5.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
|
||||||
dependencies = [
|
|
||||||
"cfg_aliases 0.2.1",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"socket2 0.5.9",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@@ -4465,10 +4399,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
|
||||||
"rustls 0.23.25",
|
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"rustls-pki-types",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@@ -4476,7 +4407,6 @@ dependencies = [
|
|||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls 0.26.2",
|
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@@ -4485,7 +4415,6 @@ dependencies = [
|
|||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4601,12 +4530,6 @@ version = "0.1.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "2.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -4676,7 +4599,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki 0.103.1",
|
"rustls-webpki 0.103.1",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -4697,9 +4619,6 @@ name = "rustls-pki-types"
|
|||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||||
dependencies = [
|
|
||||||
"web-time",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
@@ -5561,28 +5480,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-http"
|
|
||||||
version = "2.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "696ef548befeee6c6c17b80ef73e7c41205b6c2204e87ef78ccc231212389a5c"
|
|
||||||
dependencies = [
|
|
||||||
"data-url",
|
|
||||||
"http",
|
|
||||||
"regex",
|
|
||||||
"reqwest",
|
|
||||||
"schemars",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"tauri-plugin",
|
|
||||||
"tauri-plugin-fs",
|
|
||||||
"thiserror 2.0.12",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
"urlpattern",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -6682,16 +6579,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-time"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "portmaster"
|
name = "portmaster"
|
||||||
version = "2.0.0"
|
version = "2.0.14"
|
||||||
description = "Portmaster UI"
|
description = "Portmaster UI"
|
||||||
authors = ["Safing"]
|
authors = ["Safing"]
|
||||||
license = ""
|
license = ""
|
||||||
@@ -25,8 +25,7 @@ tauri-plugin-single-instance = "2.2.1"
|
|||||||
tauri-plugin-notification = "2.2.1"
|
tauri-plugin-notification = "2.2.1"
|
||||||
tauri-plugin-log = "2.2.1"
|
tauri-plugin-log = "2.2.1"
|
||||||
tauri-plugin-window-state = "2.2.1"
|
tauri-plugin-window-state = "2.2.1"
|
||||||
tauri-plugin-http = "2.2.1"
|
tauri-plugin-websocket = "2"
|
||||||
tauri-plugin-websocket = "2.2.1"
|
|
||||||
|
|
||||||
clap_lex = "0.7.2"
|
clap_lex = "0.7.2"
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ http = "1.0.0"
|
|||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
reqwest = { version = "0.12" }
|
reqwest = { version = "0.12", features = ["cookies", "json"] }
|
||||||
|
|
||||||
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
|
rfd = { version = "*", default-features = false, features = [ "tokio", "gtk3", "common-controls-v6" ] }
|
||||||
open = "5.1.3"
|
open = "5.1.3"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"remote": {
|
"remote": {
|
||||||
"urls": [
|
"urls": [
|
||||||
"http://localhost:817"
|
"http://127.0.0.1:817"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"window-state:allow-save-window-state",
|
"window-state:allow-save-window-state",
|
||||||
"window-state:allow-restore-state",
|
"window-state:allow-restore-state",
|
||||||
"clipboard-manager:allow-read-text",
|
"clipboard-manager:allow-read-text",
|
||||||
"clipboard-manager:allow-write-text"
|
"clipboard-manager:allow-write-text",
|
||||||
|
"websocket:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"default":{"identifier":"default","description":"Capability for the main window","remote":{"urls":["http://localhost:817"]},"local":true,"windows":["main","splash"],"permissions":["core:path:default","core:event:allow-listen","core:event:allow-unlisten","core:event:allow-emit","core:event:allow-emit-to","core:window:allow-hide","core:window:allow-show","core:window:allow-is-visible","core:window:allow-set-focus","core:window:allow-close","core:window:allow-get-all-windows","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default","shell:allow-open","notification:default","window-state:allow-save-window-state","window-state:allow-restore-state","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
desktop/tauri/src-tauri/src/commands/mod.rs
Normal file
1
desktop/tauri/src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod tauri_http;
|
||||||
75
desktop/tauri/src-tauri/src/commands/tauri_http.rs
Normal file
75
desktop/tauri/src-tauri/src/commands/tauri_http.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use tauri::State;
|
||||||
|
use reqwest::{Client, Method};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Creates and configures a shared HTTP client for application-wide use.
|
||||||
|
///
|
||||||
|
/// Returns a reqwest Client configured with:
|
||||||
|
/// - Connection pooling
|
||||||
|
/// - Persistent cookie store
|
||||||
|
///
|
||||||
|
/// Client can be accessed from UI through the exposed Tauri command `send_tauri_http_request(...)`
|
||||||
|
/// Such requests execute directly from the Tauri app binary, not from the WebView process
|
||||||
|
pub fn create_http_client() -> Client {
|
||||||
|
Client::builder()
|
||||||
|
// Maximum idle connections per host
|
||||||
|
.pool_max_idle_per_host(10)
|
||||||
|
// Enable cookie support
|
||||||
|
.cookie_store(true)
|
||||||
|
.user_agent("Portmaster UI")
|
||||||
|
.build()
|
||||||
|
.expect("failed to build HTTP client")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HttpRequestOptions {
|
||||||
|
method: String,
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
body: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct HttpResponse {
|
||||||
|
status: u16,
|
||||||
|
status_text: String,
|
||||||
|
headers: Vec<(String, String)>,
|
||||||
|
body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn send_tauri_http_request(
|
||||||
|
client: State<'_, Client>,
|
||||||
|
url: String,
|
||||||
|
opts: HttpRequestOptions
|
||||||
|
) -> Result<HttpResponse, String> {
|
||||||
|
//println!("URL: {}", url);
|
||||||
|
|
||||||
|
// Build the request
|
||||||
|
let mut req = client
|
||||||
|
.request(Method::from_bytes(opts.method.as_bytes()).map_err(|e| e.to_string())?, &url);
|
||||||
|
|
||||||
|
// Apply headers
|
||||||
|
for (k, v) in opts.headers {
|
||||||
|
req = req.header(&k, &v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach body if present
|
||||||
|
if let Some(body) = opts.body {
|
||||||
|
req = req.body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send and await the response
|
||||||
|
let resp = req.send().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Read status, headers, and body
|
||||||
|
let status = resp.status().as_u16();
|
||||||
|
let status_text = resp.status().canonical_reason().unwrap_or("").to_string();
|
||||||
|
let headers = resp
|
||||||
|
.headers()
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||||
|
.collect();
|
||||||
|
let body = resp.bytes().await.map_err(|e| e.to_string())?.to_vec();
|
||||||
|
|
||||||
|
Ok(HttpResponse { status, status_text, headers, body })
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use std::{env, path::Path, time::Duration};
|
use std::{env, time::Duration};
|
||||||
|
|
||||||
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent};
|
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent, WindowEvent};
|
||||||
|
|
||||||
@@ -18,12 +18,14 @@ mod config;
|
|||||||
mod portmaster;
|
mod portmaster;
|
||||||
mod traymenu;
|
mod traymenu;
|
||||||
mod window;
|
mod window;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use portmaster::PortmasterExt;
|
use portmaster::PortmasterExt;
|
||||||
use tauri_plugin_log::RotationStrategy;
|
use tauri_plugin_log::RotationStrategy;
|
||||||
use traymenu::setup_tray_menu;
|
use traymenu::setup_tray_menu;
|
||||||
use window::{close_splash_window, create_main_window, hide_splash_window};
|
use window::{close_splash_window, create_main_window, hide_splash_window};
|
||||||
|
use tauri_plugin_window_state::StateFlags;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
@@ -139,13 +141,22 @@ fn main() {
|
|||||||
|
|
||||||
// TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now.
|
// TODO(vladimir): Permission for logs/app2 folder are not guaranteed. Use the default location for now.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let log_target = if let Some(data_dir) = cli_args.data {
|
let log_target = if let Some(_) = cli_args.data {
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None })
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None })
|
||||||
} else {
|
} else {
|
||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create a single HTTP client that:
|
||||||
|
// - Pools and reuses connections for better performance
|
||||||
|
// - Is exposed to UI through 'send_tauri_http_request()' command
|
||||||
|
// - Such requests execute directly from the Tauri app binary, not from the WebView process
|
||||||
|
let http_client = commands::tauri_http::create_http_client();
|
||||||
|
|
||||||
let app = tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
|
// make HTTP client accessible in commands ('send_tauri_http_request()')
|
||||||
|
.manage(http_client)
|
||||||
|
.plugin(tauri_plugin_websocket::init())
|
||||||
// Shell plugin for open_external support
|
// Shell plugin for open_external support
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
// Initialize Logging plugin.
|
// Initialize Logging plugin.
|
||||||
@@ -164,7 +175,12 @@ fn main() {
|
|||||||
// OS Version and Architecture support
|
// OS Version and Architecture support
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
// Initialize save windows state plugin.
|
// Initialize save windows state plugin.
|
||||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
.plugin(tauri_plugin_window_state::Builder::default()
|
||||||
|
// Don't save visibility state, so it will not interfere with "--background" command line argument
|
||||||
|
.with_state_flags(StateFlags::all() & !StateFlags::VISIBLE)
|
||||||
|
// Don't save splash window state
|
||||||
|
.with_denylist(&["splash",])
|
||||||
|
.build())
|
||||||
// Single instance guard
|
// Single instance guard
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||||
// Send info to already dunning instance.
|
// Send info to already dunning instance.
|
||||||
@@ -179,7 +195,8 @@ fn main() {
|
|||||||
portmaster::commands::get_state,
|
portmaster::commands::get_state,
|
||||||
portmaster::commands::set_state,
|
portmaster::commands::set_state,
|
||||||
portmaster::commands::should_show,
|
portmaster::commands::should_show,
|
||||||
portmaster::commands::should_handle_prompts
|
portmaster::commands::should_handle_prompts,
|
||||||
|
commands::tauri_http::send_tauri_http_request,
|
||||||
])
|
])
|
||||||
// Setup the app an any listeners
|
// Setup the app an any listeners
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::portapi::client::*;
|
|||||||
use crate::portapi::message::*;
|
use crate::portapi::message::*;
|
||||||
use crate::portapi::models::notification::*;
|
use crate::portapi::models::notification::*;
|
||||||
use crate::portapi::types::*;
|
use crate::portapi::types::*;
|
||||||
use log::debug;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
|
|||||||
@@ -181,15 +181,15 @@ pub fn may_navigate_to_ui(win: &mut WebviewWindow, force: bool) {
|
|||||||
// Only for dev build
|
// Only for dev build
|
||||||
// Allow connection to http://localhost:4200
|
// Allow connection to http://localhost:4200
|
||||||
let capabilities = include_str!("../capabilities/default.json")
|
let capabilities = include_str!("../capabilities/default.json")
|
||||||
.replace("http://localhost:817", "http://localhost:4200");
|
.replace("http://127.0.0.1:817", "http://127.0.0.1:4200");
|
||||||
let _ = win.add_capability(capabilities);
|
let _ = win.add_capability(capabilities);
|
||||||
debug!("[tauri] navigating to http://localhost:4200");
|
debug!("[tauri] navigating to http://127.0.0.1:4200");
|
||||||
_ = win.navigate("http://localhost:4200".parse().unwrap());
|
_ = win.navigate("http://127.0.0.1:4200".parse().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
{
|
{
|
||||||
_ = win.navigate("http://localhost:817".parse().unwrap());
|
_ = win.navigate("http://127.0.0.1:817".parse().unwrap());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ var dataDir
|
|||||||
|
|
||||||
SimpleSC::SetServiceDescription "PortmasterCore" "Portmaster Application Firewall - Core Service"
|
SimpleSC::SetServiceDescription "PortmasterCore" "Portmaster Application Firewall - Core Service"
|
||||||
|
|
||||||
|
;
|
||||||
|
; Auto start the UI
|
||||||
|
;
|
||||||
|
DetailPrint "Creating registry entry for autostart"
|
||||||
|
WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "Portmaster" '"$INSTDIR\portmaster.exe" --with-prompts --with-notifications --background'
|
||||||
|
|
||||||
|
;
|
||||||
|
; MIGRATION FROM PMv1 TO PMv2
|
||||||
|
;
|
||||||
StrCpy $oldInstallationDir "$COMMONPROGRAMDATA\Safing\Portmaster"
|
StrCpy $oldInstallationDir "$COMMONPROGRAMDATA\Safing\Portmaster"
|
||||||
StrCpy $dataDir "$COMMONPROGRAMDATA\Portmaster"
|
StrCpy $dataDir "$COMMONPROGRAMDATA\Portmaster"
|
||||||
|
|
||||||
@@ -168,6 +177,10 @@ var dataDir
|
|||||||
Delete /REBOOTOK "$INSTDIR\assets.zip"
|
Delete /REBOOTOK "$INSTDIR\assets.zip"
|
||||||
RMDir /r /REBOOTOK "$INSTDIR"
|
RMDir /r /REBOOTOK "$INSTDIR"
|
||||||
|
|
||||||
|
; remove the registry entry for the autostart
|
||||||
|
DetailPrint "Removing registry entry for autostart"
|
||||||
|
DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
|
||||||
; delete data files
|
; delete data files
|
||||||
Delete /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\history.db"
|
Delete /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\history.db"
|
||||||
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\cache"
|
RMDir /r /REBOOTOK "$COMMONPROGRAMDATA\Portmaster\databases\cache"
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -2,9 +2,6 @@ module github.com/safing/portmaster
|
|||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
// TODO: Remove when https://github.com/tc-hib/winres/pull/4 is released.
|
|
||||||
replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/VictoriaMetrics/metrics v1.35.1
|
github.com/VictoriaMetrics/metrics v1.35.1
|
||||||
github.com/Xuanwo/go-locale v1.1.1
|
github.com/Xuanwo/go-locale v1.1.1
|
||||||
@@ -35,7 +32,6 @@ require (
|
|||||||
github.com/jackc/puddle/v2 v2.2.1
|
github.com/jackc/puddle/v2 v2.2.1
|
||||||
github.com/lmittmann/tint v1.0.5
|
github.com/lmittmann/tint v1.0.5
|
||||||
github.com/maruel/panicparse/v2 v2.3.1
|
github.com/maruel/panicparse/v2 v2.3.1
|
||||||
github.com/mat/besticon v3.12.0+incompatible
|
|
||||||
github.com/mattn/go-colorable v0.1.13
|
github.com/mattn/go-colorable v0.1.13
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/miekg/dns v1.1.62
|
github.com/miekg/dns v1.1.62
|
||||||
@@ -70,6 +66,8 @@ require (
|
|||||||
zombiezen.com/go/sqlite v1.3.0
|
zombiezen.com/go/sqlite v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/sergeymakinen/go-bmp v1.0.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||||
github.com/aead/ecdh v0.2.0 // indirect
|
github.com/aead/ecdh v0.2.0 // indirect
|
||||||
@@ -101,6 +99,7 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/satori/go.uuid v1.2.0 // indirect
|
github.com/satori/go.uuid v1.2.0 // indirect
|
||||||
github.com/seehuhn/sha256d v1.0.0 // indirect
|
github.com/seehuhn/sha256d v1.0.0 // indirect
|
||||||
|
github.com/sergeymakinen/go-ico v1.0.0-beta.0
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -53,8 +53,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
|
|||||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dhaavi/winres v0.2.2 h1:SUago7FwhgLSMyDdeuV6enBZ+ZQSl0KwcnbWzvlfBls=
|
|
||||||
github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf3x9T4=
|
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
@@ -173,8 +171,6 @@ github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:
|
|||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
|
github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
|
||||||
github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
|
github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
|
||||||
github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg=
|
|
||||||
github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
|
|
||||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
@@ -254,6 +250,10 @@ github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0
|
|||||||
github.com/seehuhn/fortuna v1.0.1/go.mod h1:LX8ubejCnUoT/hX+1aKUtbKls2H6DRkqzkc7TdR3iis=
|
github.com/seehuhn/fortuna v1.0.1/go.mod h1:LX8ubejCnUoT/hX+1aKUtbKls2H6DRkqzkc7TdR3iis=
|
||||||
github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw=
|
github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw=
|
||||||
github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4=
|
github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4=
|
||||||
|
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
|
||||||
|
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
|
||||||
|
github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3K6MFJw4GfZvQ=
|
||||||
|
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
@@ -286,6 +286,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tannerryan/ring v1.1.2 h1:iXayOjqHQOLzuy9GwSKuG3nhWfzQkldMlQivcgIr7gQ=
|
github.com/tannerryan/ring v1.1.2 h1:iXayOjqHQOLzuy9GwSKuG3nhWfzQkldMlQivcgIr7gQ=
|
||||||
github.com/tannerryan/ring v1.1.2/go.mod h1:DkELJEjbZhJBtFKR9Xziwj3HKZnb/knRgljNqp65vH4=
|
github.com/tannerryan/ring v1.1.2/go.mod h1:DkELJEjbZhJBtFKR9Xziwj3HKZnb/knRgljNqp65vH4=
|
||||||
|
github.com/tc-hib/winres v0.3.1 h1:CwRjEGrKdbi5CvZ4ID+iyVhgyfatxFoizjPhzez9Io4=
|
||||||
|
github.com/tc-hib/winres v0.3.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||||
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||||
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Name=Portmaster
|
|||||||
GenericName=Application Firewall
|
GenericName=Application Firewall
|
||||||
Exec={{exec}} --data=/opt/safing/portmaster --with-prompts --with-notifications
|
Exec={{exec}} --data=/opt/safing/portmaster --with-prompts --with-notifications
|
||||||
Icon={{icon}}
|
Icon={{icon}}
|
||||||
|
StartupWMClass=portmaster
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=System
|
Categories=System
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Environment=LOGLEVEL=info
|
|||||||
Environment=PORTMASTER_ARGS=
|
Environment=PORTMASTER_ARGS=
|
||||||
EnvironmentFile=-/etc/default/portmaster
|
EnvironmentFile=-/etc/default/portmaster
|
||||||
ProtectSystem=true
|
ProtectSystem=true
|
||||||
|
ReadWritePaths=/usr/lib/portmaster
|
||||||
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
|
||||||
RestrictNamespaces=yes
|
RestrictNamespaces=yes
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
|
|||||||
79
packaging/windows/dev_helpers/build_angular.ps1
Normal file
79
packaging/windows/dev_helpers/build_angular.ps1
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# This script builds the Angular project for the Portmaster application and packages it into a zip file.
|
||||||
|
# The script assumes that all necessary dependencies are installed and available.
|
||||||
|
# Output file: dist/portmaster.zip
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[Alias("d")]
|
||||||
|
[switch]$Development,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[Alias("i")]
|
||||||
|
[switch]$Interactive
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store original directory and find project root
|
||||||
|
$originalDir = Get-Location
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$projectRoot = (Get-Item $scriptDir).Parent.Parent.Parent.FullName
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Create output directory
|
||||||
|
$outputDir = Join-Path $scriptDir "dist"
|
||||||
|
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Navigate to Angular project
|
||||||
|
Set-Location (Join-Path $projectRoot "desktop\angular")
|
||||||
|
|
||||||
|
# npm install - always run in non-interactive mode, ask in interactive mode
|
||||||
|
if (!$Interactive -or (Read-Host "Run 'npm install'? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||||
|
npm install
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
}
|
||||||
|
|
||||||
|
# build libs - always run in non-interactive mode, ask in interactive mode
|
||||||
|
if (!$Interactive -or (Read-Host "Build shared libraries? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||||
|
if ($Development) {
|
||||||
|
Write-Host "Building shared libraries in development mode" -ForegroundColor Yellow
|
||||||
|
npm run build-libs:dev
|
||||||
|
} else {
|
||||||
|
Write-Host "Building shared libraries in production mode" -ForegroundColor Yellow
|
||||||
|
npm run build-libs
|
||||||
|
}
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build Angular project
|
||||||
|
if ($Development) {
|
||||||
|
Write-Host "Building Angular project in development mode" -ForegroundColor Yellow
|
||||||
|
ng build --configuration development --base-href /ui/modules/portmaster/ portmaster
|
||||||
|
} else {
|
||||||
|
Write-Host "Building Angular project in production mode" -ForegroundColor Yellow
|
||||||
|
ng build --configuration production --base-href /ui/modules/portmaster/ portmaster
|
||||||
|
}
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
|
|
||||||
|
# Create zip archive
|
||||||
|
Write-Host "Creating zip archive" -ForegroundColor Yellow
|
||||||
|
Set-Location dist
|
||||||
|
$destinationZip = Join-Path $outputDir "portmaster.zip"
|
||||||
|
if ($PSVersionTable.PSVersion.Major -ge 5) {
|
||||||
|
# Option 1: Use .NET Framework directly (faster than Compress-Archive)
|
||||||
|
Write-Host "Using System.IO.Compression for faster archiving" -ForegroundColor Yellow
|
||||||
|
if (Test-Path $destinationZip) { Remove-Item $destinationZip -Force } # Remove existing zip if it exists
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
$compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
|
||||||
|
[System.IO.Compression.ZipFile]::CreateFromDirectory((Get-Location), $destinationZip, $compressionLevel, $false)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Fall back to Compress-Archive
|
||||||
|
Compress-Archive -Path * -DestinationPath $destinationZip -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Build completed successfully: $(Join-Path $outputDir "portmaster.zip")" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
# Return to original directory - this will execute even if Ctrl+C is pressed
|
||||||
|
Set-Location $originalDir
|
||||||
|
}
|
||||||
38
packaging/windows/dev_helpers/build_tauri.ps1
Normal file
38
packaging/windows/dev_helpers/build_tauri.ps1
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# This script builds the Tauri application for Portmaster on Windows.
|
||||||
|
# It optionally builds the required Angular tauri-builtin project first.
|
||||||
|
# The script assumes that all necessary dependencies (Node.js, Rust, etc.) are installed.
|
||||||
|
# Output file: dist/portmaster.exe
|
||||||
|
|
||||||
|
# Store original directory and find project root
|
||||||
|
$originalDir = Get-Location
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$projectRoot = (Get-Item $scriptDir).Parent.Parent.Parent.FullName
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
$outputDir = Join-Path $scriptDir "dist"
|
||||||
|
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Ask if user wants to build the Angular tauri-builtin project
|
||||||
|
if ((Read-Host "Build Angular tauri-builtin project? (Y/N, default: Y)") -notmatch '^[Nn]$') {
|
||||||
|
# Navigate to Angular project
|
||||||
|
Set-Location (Join-Path $projectRoot "desktop\angular")
|
||||||
|
|
||||||
|
# Build tauri-builtin project
|
||||||
|
ng build --configuration production --base-href / tauri-builtin
|
||||||
|
if ($LASTEXITCODE -ne 0) { Set-Location $originalDir; exit $LASTEXITCODE }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Navigate to Tauri project directory
|
||||||
|
Set-Location (Join-Path $projectRoot "desktop\tauri\src-tauri")
|
||||||
|
|
||||||
|
# Build Tauri project for Windows
|
||||||
|
cargo tauri build --no-bundle
|
||||||
|
if ($LASTEXITCODE -ne 0) { Set-Location $originalDir; exit $LASTEXITCODE }
|
||||||
|
|
||||||
|
# Copy the output files to the script's dist directory
|
||||||
|
$tauriOutput = Join-Path (Get-Location) "target\release"
|
||||||
|
Copy-Item -Path "$tauriOutput\portmaster.exe" -Destination $outputDir -Force
|
||||||
|
|
||||||
|
# Return to original directory
|
||||||
|
Set-Location $originalDir
|
||||||
|
Write-Host "Build completed successfully: $outputDir\portmaster.exe" -ForegroundColor Green
|
||||||
@@ -92,7 +92,8 @@ func (sc *ServiceConfig) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentBinaryFolder() (string, error) {
|
// returns the absolute path of the currently running executable
|
||||||
|
func getCurrentBinaryPath() (string, error) {
|
||||||
// Get the path of the currently running executable
|
// Get the path of the currently running executable
|
||||||
exePath, err := os.Executable()
|
exePath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -105,6 +106,16 @@ func getCurrentBinaryFolder() (string, error) {
|
|||||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return absPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentBinaryFolder() (string, error) {
|
||||||
|
// Get the absolute path of the currently running executable
|
||||||
|
absPath, err := getCurrentBinaryPath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// Get the directory of the executable
|
// Get the directory of the executable
|
||||||
installDir := filepath.Dir(absPath)
|
installDir := filepath.Dir(absPath)
|
||||||
|
|
||||||
@@ -119,8 +130,8 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
Directory: svcCfg.BinDir,
|
Directory: svcCfg.BinDir,
|
||||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
||||||
PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
|
PurgeDirectory: filepath.Join(svcCfg.BinDir, "upgrade_obsolete_binaries"),
|
||||||
Ignore: []string{"databases", "intel", "config.json"},
|
Ignore: []string{"uninstall.exe"}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
|
||||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||||
IndexFile: "index.json",
|
IndexFile: "index.json",
|
||||||
Verify: svcCfg.VerifyBinaryUpdates,
|
Verify: svcCfg.VerifyBinaryUpdates,
|
||||||
AutoCheck: true, // May be changed by config during instance startup.
|
AutoCheck: true, // May be changed by config during instance startup.
|
||||||
@@ -150,7 +161,7 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
Directory: svcCfg.BinDir,
|
Directory: svcCfg.BinDir,
|
||||||
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
DownloadDirectory: filepath.Join(svcCfg.DataDir, "download_binaries"),
|
||||||
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
|
PurgeDirectory: filepath.Join(svcCfg.DataDir, "upgrade_obsolete_binaries"),
|
||||||
Ignore: []string{"databases", "intel", "config.json"},
|
Ignore: []string{}, // "databases", "intel" and "config.json" not needed here since they are not in the bin dir.
|
||||||
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
IndexURLs: svcCfg.BinariesIndexURLs, // May be changed by config during instance startup.
|
||||||
IndexFile: "index.json",
|
IndexFile: "index.json",
|
||||||
Verify: svcCfg.VerifyBinaryUpdates,
|
Verify: svcCfg.VerifyBinaryUpdates,
|
||||||
@@ -160,6 +171,21 @@ func MakeUpdateConfigs(svcCfg *ServiceConfig) (binaryUpdateConfig, intelUpdateCo
|
|||||||
NeedsRestart: true,
|
NeedsRestart: true,
|
||||||
Notify: true,
|
Notify: true,
|
||||||
}
|
}
|
||||||
|
if binPath, err := getCurrentBinaryPath(); err == nil {
|
||||||
|
binaryUpdateConfig.PostUpgradeCommands = []updates.UpdateCommandConfig{
|
||||||
|
// Restore SELinux context for the new core binary after upgrade
|
||||||
|
// (`restorecon /usr/lib/portmaster/portmaster-core`)
|
||||||
|
{
|
||||||
|
Command: "restorecon",
|
||||||
|
Args: []string{binPath},
|
||||||
|
TriggerArtifactFName: binPath,
|
||||||
|
FailOnError: false, // Ignore error: 'restorecon' may not be available on a non-SELinux systems.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, fmt.Errorf("failed to get current binary path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
intelUpdateConfig = &updates.Config{
|
intelUpdateConfig = &updates.Config{
|
||||||
Name: configure.DefaultIntelIndexName,
|
Name: configure.DefaultIntelIndexName,
|
||||||
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
Directory: filepath.Join(svcCfg.DataDir, "intel"),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
|||||||
specialProfileID = profile.PortmasterProfileID
|
specialProfileID = profile.PortmasterProfileID
|
||||||
default:
|
default:
|
||||||
// Check if this is another Portmaster component.
|
// Check if this is another Portmaster component.
|
||||||
if module.portmasterUIPath != "" && p.Path == module.portmasterUIPath {
|
if p.IsPortmasterUi(context.Background()) {
|
||||||
specialProfileID = profile.PortmasterAppProfileID
|
specialProfileID = profile.PortmasterAppProfileID
|
||||||
}
|
}
|
||||||
// Check if this is the system resolver.
|
// Check if this is the system resolver.
|
||||||
@@ -104,3 +105,37 @@ func (p *Process) getSpecialProfileID() (specialProfileID string) {
|
|||||||
|
|
||||||
return specialProfileID
|
return specialProfileID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsPortmasterUi checks if the process is the Portmaster UI or its child (up to 3 parent levels).
|
||||||
|
func (p *Process) IsPortmasterUi(ctx context.Context) bool {
|
||||||
|
if module.portmasterUIPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find parent for up to two levels, if we don't match the path.
|
||||||
|
const checkLevels = 3
|
||||||
|
|
||||||
|
var previousPid int
|
||||||
|
proc := p
|
||||||
|
|
||||||
|
for i := 0; i < checkLevels; i++ {
|
||||||
|
if proc.Pid == UnidentifiedProcessID || proc.Pid == SystemProcessID {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
realPath, err := filepath.EvalSymlinks(proc.Path)
|
||||||
|
if err == nil && realPath == module.portmasterUIPath {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < checkLevels-1 { // no need to check parent if we are at the last level
|
||||||
|
previousPid = proc.Pid
|
||||||
|
proc, err = GetOrFindProcess(ctx, proc.ParentPid)
|
||||||
|
if err != nil || proc.Pid == previousPid {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,17 +3,22 @@ package binmeta
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
_ "image/png" // Register png support for image package
|
|
||||||
|
|
||||||
"github.com/fogleman/gg"
|
"github.com/fogleman/gg"
|
||||||
_ "github.com/mat/besticon/ico" // Register ico support for image package
|
|
||||||
|
// Import the specialized ICO decoder package
|
||||||
|
// This package seems to work better than "github.com/mat/besticon/ico" with ICO files
|
||||||
|
// extracted from Windows binaries, particularly those containing cursor-related data
|
||||||
|
ico "github.com/sergeymakinen/go-ico"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertICOtoPNG converts a an .ico to a .png image.
|
// ConvertICOtoPNG converts a an .ico to a .png image.
|
||||||
func ConvertICOtoPNG(ico []byte) (png []byte, err error) {
|
func ConvertICOtoPNG(icoBytes []byte) (png []byte, err error) {
|
||||||
// Decode the ICO.
|
// Decode ICO image.
|
||||||
icon, _, err := image.Decode(bytes.NewReader(ico))
|
// Note: The standard approach with `image.Decode(bytes.NewReader(icoBytes))` sometimes fails
|
||||||
|
// when processing certain ICO files (particularly those with cursor data),
|
||||||
|
// as it reads initial bytes for format detection before passing the stream to the decoder.
|
||||||
|
icon, err := ico.Decode(bytes.NewReader(icoBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode ICO: %w", err)
|
return nil, fmt.Errorf("failed to decode ICO: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,13 +538,14 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md Matchin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply new icon if found.
|
// Apply new icon if found.
|
||||||
if newIcon != nil {
|
if newIcon != nil && !profile.iconExists(newIcon) {
|
||||||
if len(profile.Icons) == 0 {
|
if len(profile.Icons) == 0 {
|
||||||
profile.Icons = []binmeta.Icon{*newIcon}
|
profile.Icons = []binmeta.Icon{*newIcon}
|
||||||
} else {
|
} else {
|
||||||
profile.Icons = append(profile.Icons, *newIcon)
|
profile.Icons = append(profile.Icons, *newIcon)
|
||||||
profile.Icons = binmeta.SortAndCompactIcons(profile.Icons)
|
profile.Icons = binmeta.SortAndCompactIcons(profile.Icons)
|
||||||
}
|
}
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -559,3 +560,13 @@ func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md Matchin
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if the given icon already assigned to the profile.
|
||||||
|
func (profile *Profile) iconExists(newIcon *binmeta.Icon) bool {
|
||||||
|
for _, icon := range profile.Icons {
|
||||||
|
if icon.Value == newIcon.Value && icon.Type == newIcon.Type && icon.Source == newIcon.Source {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ func Resolve(ctx context.Context, q *Query) (rrCache *RRCache, err error) {
|
|||||||
case !rrCache.Expired():
|
case !rrCache.Expired():
|
||||||
// Return non-expired cached entry immediately.
|
// Return non-expired cached entry immediately.
|
||||||
return rrCache, nil
|
return rrCache, nil
|
||||||
case useStaleCache():
|
case rrCache.RCode == dns.RcodeSuccess && useStaleCache():
|
||||||
// Return expired cache if we should use stale cache entries,
|
// Return expired cache if we should use stale cache entries,
|
||||||
// but start an async query instead.
|
// but start an async query instead.
|
||||||
log.Tracer(ctx).Tracef(
|
log.Tracer(ctx).Tracef(
|
||||||
|
|||||||
@@ -107,15 +107,21 @@ func (tr *TCPResolver) UseTLS() *TCPResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolverConn, error) {
|
func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolverConn, error) {
|
||||||
|
var existingConn *tcpResolverConn
|
||||||
|
|
||||||
|
// Minimize the time we hold the lock to avoid blocking other threads.
|
||||||
tr.Lock()
|
tr.Lock()
|
||||||
defer tr.Unlock()
|
if tr.resolverConn != nil && tr.resolverConn.abandoned.IsNotSet() {
|
||||||
|
existingConn = tr.resolverConn
|
||||||
|
}
|
||||||
|
tr.Unlock()
|
||||||
|
|
||||||
// Check if we have a resolver.
|
// Check if we have a resolver.
|
||||||
if tr.resolverConn != nil && tr.resolverConn.abandoned.IsNotSet() {
|
if existingConn != nil {
|
||||||
// If there is one, check if it's alive!
|
// If there is one, check if it's alive!
|
||||||
select {
|
select {
|
||||||
case tr.resolverConn.heartbeat <- struct{}{}:
|
case existingConn.heartbeat <- struct{}{}:
|
||||||
return tr.resolverConn, nil
|
return existingConn, nil
|
||||||
case <-time.After(heartbeatTimeout):
|
case <-time.After(heartbeatTimeout):
|
||||||
log.Warningf("resolver: heartbeat for dns client %s failed", tr.resolver.Info.DescriptiveName())
|
log.Warningf("resolver: heartbeat for dns client %s failed", tr.resolver.Info.DescriptiveName())
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -162,6 +168,10 @@ func (tr *TCPResolver) getOrCreateResolverConn(ctx context.Context) (*tcpResolve
|
|||||||
tr.resolver.Info.DescriptiveName(),
|
tr.resolver.Info.DescriptiveName(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Thread-safe resolverConn creation.
|
||||||
|
tr.Lock()
|
||||||
|
defer tr.Unlock()
|
||||||
|
|
||||||
// Create resolver connection.
|
// Create resolver connection.
|
||||||
tr.resolverConnInstanceID++
|
tr.resolverConnInstanceID++
|
||||||
resolverConn := &tcpResolverConn{
|
resolverConn := &tcpResolverConn{
|
||||||
|
|||||||
@@ -50,6 +50,22 @@ var (
|
|||||||
ErrActionRequired = errors.New("action required")
|
ErrActionRequired = errors.New("action required")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateCommandConfig defines the configuration for a shell command
|
||||||
|
// that is executed when an update is applied
|
||||||
|
type UpdateCommandConfig struct {
|
||||||
|
// Shell command to execute
|
||||||
|
Command string
|
||||||
|
// Arguments to pass to the command
|
||||||
|
Args []string
|
||||||
|
// Execute triggers: if not empty, the command will be executed only if specified file was updated
|
||||||
|
// if empty, the command will be executed always
|
||||||
|
TriggerArtifactFName string
|
||||||
|
// FailOnError defines whether the upgrade should fail if the command fails
|
||||||
|
// true - upgrade will fail if the command fails
|
||||||
|
// false - upgrade will continue even if the command fails
|
||||||
|
FailOnError bool
|
||||||
|
}
|
||||||
|
|
||||||
// Config holds the configuration for the updates module.
|
// Config holds the configuration for the updates module.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Name of the updater.
|
// Name of the updater.
|
||||||
@@ -87,6 +103,9 @@ type Config struct {
|
|||||||
// Notify defines whether the user shall be informed about events via notifications.
|
// Notify defines whether the user shall be informed about events via notifications.
|
||||||
// If enabled, disables automatic restart after upgrade.
|
// If enabled, disables automatic restart after upgrade.
|
||||||
Notify bool
|
Notify bool
|
||||||
|
|
||||||
|
// list of shell commands needed to run after the upgrade (if any)
|
||||||
|
PostUpgradeCommands []UpdateCommandConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check looks for obvious configuration errors.
|
// Check looks for obvious configuration errors.
|
||||||
@@ -404,7 +423,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
|
|||||||
Type: notifications.ActionTypeWebhook,
|
Type: notifications.ActionTypeWebhook,
|
||||||
Payload: notifications.ActionTypeWebhookPayload{
|
Payload: notifications.ActionTypeWebhookPayload{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
URL: "updates/apply",
|
URL: "core/restart",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,12 +25,20 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unload UI assets to be able to move files on Windows.
|
// If we are running in a UI instance, we need to unload the UI assets
|
||||||
u.instance.UI().EnableUpgradeLock()
|
if u.instance != nil {
|
||||||
defer u.instance.UI().DisableUpgradeLock()
|
u.instance.UI().EnableUpgradeLock()
|
||||||
|
defer u.instance.UI().DisableUpgradeLock()
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the upgrade.
|
// Execute the upgrade.
|
||||||
upgradeError := u.upgradeMoveFiles(downloader)
|
upgradeError := u.upgradeMoveFiles(downloader)
|
||||||
|
if upgradeError == nil {
|
||||||
|
// Files upgraded successfully.
|
||||||
|
// Applying post-upgrade tasks, if any.
|
||||||
|
upgradeError = u.applyPostUpgradeCommands()
|
||||||
|
}
|
||||||
|
|
||||||
if upgradeError == nil {
|
if upgradeError == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -207,3 +216,41 @@ func (u *Updater) deleteUnfinishedFiles(dir string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Updater) applyPostUpgradeCommands() error {
|
||||||
|
// At this point, we assume that the upgrade was successful and all files are in place.
|
||||||
|
// We need to execute the post-upgrade commands, if any.
|
||||||
|
|
||||||
|
if len(u.cfg.PostUpgradeCommands) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect full paths to files that were upgraded, required to check the trigger.
|
||||||
|
upgradedFiles := make(map[string]struct{})
|
||||||
|
for _, artifact := range u.index.Artifacts {
|
||||||
|
upgradedFiles[filepath.Join(u.cfg.Directory, artifact.Filename)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute post-upgrade commands.
|
||||||
|
for _, puCmd := range u.cfg.PostUpgradeCommands {
|
||||||
|
|
||||||
|
// Check trigger to ensure that we need to run this command.
|
||||||
|
if len(puCmd.TriggerArtifactFName) > 0 {
|
||||||
|
if _, ok := upgradedFiles[puCmd.TriggerArtifactFName]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("updates/%s: executing post-upgrade command: '%s %s'", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "))
|
||||||
|
output, err := exec.Command(puCmd.Command, puCmd.Args...).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
if puCmd.FailOnError {
|
||||||
|
return fmt.Errorf("post-upgrade command '%s %s' failed: %w, output: %s", puCmd.Command, strings.Join(puCmd.Args, " "), err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warningf("updates/%s: post-upgrade command '%s %s' failed, but ignored. Error: %s", u.cfg.Name, puCmd.Command, strings.Join(puCmd.Args, " "), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user