Merge branch 'v2.0' into task/refactor-spn

This commit is contained in:
Natanael Rodriguez Ramos
2025-04-14 09:44:09 +01:00
21 changed files with 1746 additions and 790 deletions

View File

@@ -0,0 +1,74 @@
package utils
import (
"sync"
"sync/atomic"
"time"
)
// CallLimiter2 bundles concurrent calls and optionally limits how fast a function is called.
type CallLimiter2 struct {
pause time.Duration
slot atomic.Int64
slotWait sync.RWMutex
executing atomic.Bool
lastExec time.Time
}
// NewCallLimiter2 returns a new call limiter.
// Set minPause to zero to disable the minimum pause between calls.
func NewCallLimiter2(minPause time.Duration) *CallLimiter2 {
return &CallLimiter2{
pause: minPause,
}
}
// Do executes the given function.
// All concurrent calls to Do are bundled and return when f() finishes.
// Waits until the minimum pause is over before executing f() again.
func (l *CallLimiter2) Do(f func()) {
// Get ticket number.
slot := l.slot.Load()
// Check if we can execute.
if l.executing.CompareAndSwap(false, true) {
// Make others wait.
l.slotWait.Lock()
defer l.slotWait.Unlock()
// Execute and return.
l.waitAndExec(f)
return
}
// Wait for slot to end and check if slot is done.
for l.slot.Load() == slot {
time.Sleep(100 * time.Microsecond)
l.slotWait.RLock()
l.slotWait.RUnlock() //nolint:staticcheck
}
}
func (l *CallLimiter2) waitAndExec(f func()) {
defer func() {
// Update last exec time.
l.lastExec = time.Now().UTC()
// Enable next execution first.
l.executing.Store(false)
// Move to next slot aftewards to prevent wait loops.
l.slot.Add(1)
}()
// Wait for the minimum duration between executions.
if l.pause > 0 {
sinceLastExec := time.Since(l.lastExec)
if sinceLastExec < l.pause {
time.Sleep(l.pause - sinceLastExec)
}
}
// Execute.
f()
}

View File

@@ -13,7 +13,7 @@ func TestCallLimiter(t *testing.T) {
t.Parallel()
pause := 10 * time.Millisecond
oa := NewCallLimiter(pause)
oa := NewCallLimiter2(pause)
executed := abool.New()
var testWg sync.WaitGroup
@@ -41,14 +41,14 @@ func TestCallLimiter(t *testing.T) {
executed.UnSet() // reset check
}
// Wait for pause to reset.
time.Sleep(pause)
// Wait for 2x pause to reset.
time.Sleep(2 * pause)
// Continuous use with re-execution.
// Choose values so that about 10 executions are expected
var execs uint32
testWg.Add(200)
for range 200 {
testWg.Add(100)
for range 100 {
go func() {
oa.Do(func() {
atomic.AddUint32(&execs, 1)
@@ -69,8 +69,8 @@ func TestCallLimiter(t *testing.T) {
t.Errorf("unexpected high exec count: %d", execs)
}
// Wait for pause to reset.
time.Sleep(pause)
// Wait for 2x pause to reset.
time.Sleep(2 * pause)
// Check if the limiter correctly handles panics.
testWg.Add(100)

View File

@@ -9,6 +9,7 @@ import (
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/notifications"
"github.com/safing/portmaster/service"
"github.com/safing/portmaster/service/ui"
"github.com/safing/portmaster/service/updates"
)
@@ -71,3 +72,4 @@ type updateDummyInstance struct{}
func (udi *updateDummyInstance) Restart() {}
func (udi *updateDummyInstance) Shutdown() {}
func (udi *updateDummyInstance) Notifications() *notifications.Notifications { return nil }
func (udi *updateDummyInstance) UI() *ui.UI { return nil }

View File

@@ -1,12 +1,12 @@
{
"name": "portmaster",
"version": "0.8.11",
"version": "2.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "portmaster",
"version": "0.8.11",
"version": "2.0.1",
"dependencies": {
"@angular/animations": "^16.0.1",
"@angular/cdk": "^16.0.1",
@@ -28,9 +28,11 @@
"@tauri-apps/plugin-cli": ">=2.0.0",
"@tauri-apps/plugin-clipboard-manager": ">=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-os": ">=2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-websocket": ">=2.2.0",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",
@@ -4844,6 +4846,15 @@
"@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": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.2.1.tgz",
@@ -4871,6 +4882,15 @@
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-websocket": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-websocket/-/plugin-websocket-2.3.0.tgz",
"integrity": "sha512-eAwRGe3tnqDeQYE0wq4g1PUKbam9tYvlC4uP/au12Y/z7MP4lrS4ylv+aoZ5Ly+hTlBdi7hDkhHomwF/UeBesA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",

View File

@@ -45,6 +45,8 @@
"@tauri-apps/plugin-notification": ">=2.0.0",
"@tauri-apps/plugin-os": ">=2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-http": ">=2.2.0",
"@tauri-apps/plugin-websocket": ">=2.2.0",
"autoprefixer": "^10.4.14",
"d3": "^7.8.4",
"data-urls": "^5.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,8 @@ tauri-plugin-single-instance = "2.2.1"
tauri-plugin-notification = "2.2.1"
tauri-plugin-log = "2.2.1"
tauri-plugin-window-state = "2.2.1"
tauri-plugin-http = "2.2.1"
tauri-plugin-websocket = "2.2.1"
clap_lex = "0.7.2"

File diff suppressed because one or more lines are too long

View File

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -70,14 +70,14 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
@@ -134,6 +134,123 @@
"description": "Reference a permission or permission set by identifier and extends its scope.",
"type": "object",
"allOf": [
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"const": "http:default"
},
{
"description": "Enables the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch"
},
{
"description": "Enables the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-cancel"
},
{
"description": "Enables the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-read-body"
},
{
"description": "Enables the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-send"
},
{
"description": "Denies the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch"
},
{
"description": "Denies the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-cancel"
},
{
"description": "Denies the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-read-body"
},
{
"description": "Denies the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-send"
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "HttpScopeEntry",
"description": "HTTP scope entry.",
"anyOf": [
{
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "HttpScopeEntry",
"description": "HTTP scope entry.",
"anyOf": [
{
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"if": {
"properties": {
@@ -465,11 +582,26 @@
"type": "string",
"const": "core:app:allow-default-window-icon"
},
{
"description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-fetch-data-store-identifiers"
},
{
"description": "Enables the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-identifier"
},
{
"description": "Enables the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-name"
},
{
"description": "Enables the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-remove-data-store"
},
{
"description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -500,11 +632,26 @@
"type": "string",
"const": "core:app:deny-default-window-icon"
},
{
"description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-fetch-data-store-identifiers"
},
{
"description": "Denies the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-identifier"
},
{
"description": "Denies the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-name"
},
{
"description": "Denies the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-remove-data-store"
},
{
"description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -1300,6 +1447,11 @@
"type": "string",
"const": "core:window:allow-internal-toggle-maximize"
},
{
"description": "Enables the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-is-always-on-top"
},
{
"description": "Enables the is_closable command without any pre-configured scope.",
"type": "string",
@@ -1665,6 +1817,11 @@
"type": "string",
"const": "core:window:deny-internal-toggle-maximize"
},
{
"description": "Denies the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-is-always-on-top"
},
{
"description": "Denies the is_closable command without any pre-configured scope.",
"type": "string",
@@ -2025,6 +2182,51 @@
"type": "string",
"const": "dialog:deny-save"
},
{
"description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"const": "http:default"
},
{
"description": "Enables the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch"
},
{
"description": "Enables the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-cancel"
},
{
"description": "Enables the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-read-body"
},
{
"description": "Enables the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-send"
},
{
"description": "Denies the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch"
},
{
"description": "Denies the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-cancel"
},
{
"description": "Denies the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-read-body"
},
{
"description": "Denies the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-send"
},
{
"description": "Allows the log command",
"type": "string",
@@ -2345,6 +2547,31 @@
"type": "string",
"const": "shell:deny-stdin-write"
},
{
"description": "Allows connecting and sending data to a WebSocket server",
"type": "string",
"const": "websocket:default"
},
{
"description": "Enables the connect command without any pre-configured scope.",
"type": "string",
"const": "websocket:allow-connect"
},
{
"description": "Enables the send command without any pre-configured scope.",
"type": "string",
"const": "websocket:allow-send"
},
{
"description": "Denies the connect command without any pre-configured scope.",
"type": "string",
"const": "websocket:deny-connect"
},
{
"description": "Denies the send command without any pre-configured scope.",
"type": "string",
"const": "websocket:deny-send"
},
{
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",

View File

@@ -37,7 +37,7 @@
],
"definitions": {
"Capability": {
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows fine grained access to the Tauri core, application, or plugin commands. If a window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
"type": "object",
"required": [
"identifier",
@@ -70,14 +70,14 @@
"type": "boolean"
},
"windows": {
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nOn multiwebview windows, prefer [`Self::webviews`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`",
"type": "array",
"items": {
"type": "string"
}
},
"webviews": {
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThis is only required when using on multiwebview contexts, by default all child webviews of a window that matches [`Self::windows`] are linked.\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`",
"type": "array",
"items": {
"type": "string"
@@ -134,6 +134,123 @@
"description": "Reference a permission or permission set by identifier and extends its scope.",
"type": "object",
"allOf": [
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"const": "http:default"
},
{
"description": "Enables the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch"
},
{
"description": "Enables the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-cancel"
},
{
"description": "Enables the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-read-body"
},
{
"description": "Enables the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-send"
},
{
"description": "Denies the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch"
},
{
"description": "Denies the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-cancel"
},
{
"description": "Denies the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-read-body"
},
{
"description": "Denies the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-send"
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "HttpScopeEntry",
"description": "HTTP scope entry.",
"anyOf": [
{
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
}
]
}
},
"deny": {
"items": {
"title": "HttpScopeEntry",
"description": "HTTP scope entry.",
"anyOf": [
{
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
},
{
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"description": "A URL that can be accessed by the webview when using the HTTP APIs. Wildcards can be used following the URL pattern standard.\n\nSee [the URL Pattern spec](https://urlpattern.spec.whatwg.org/) for more information.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin on port 443\n\n- \"https://*:*\" : allows all HTTPS origin on any port\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"",
"type": "string"
}
}
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"if": {
"properties": {
@@ -465,11 +582,26 @@
"type": "string",
"const": "core:app:allow-default-window-icon"
},
{
"description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-fetch-data-store-identifiers"
},
{
"description": "Enables the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-identifier"
},
{
"description": "Enables the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-name"
},
{
"description": "Enables the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:allow-remove-data-store"
},
{
"description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -500,11 +632,26 @@
"type": "string",
"const": "core:app:deny-default-window-icon"
},
{
"description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-fetch-data-store-identifiers"
},
{
"description": "Denies the identifier command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-identifier"
},
{
"description": "Denies the name command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-name"
},
{
"description": "Denies the remove_data_store command without any pre-configured scope.",
"type": "string",
"const": "core:app:deny-remove-data-store"
},
{
"description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string",
@@ -1300,6 +1447,11 @@
"type": "string",
"const": "core:window:allow-internal-toggle-maximize"
},
{
"description": "Enables the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:allow-is-always-on-top"
},
{
"description": "Enables the is_closable command without any pre-configured scope.",
"type": "string",
@@ -1665,6 +1817,11 @@
"type": "string",
"const": "core:window:deny-internal-toggle-maximize"
},
{
"description": "Denies the is_always_on_top command without any pre-configured scope.",
"type": "string",
"const": "core:window:deny-is-always-on-top"
},
{
"description": "Denies the is_closable command without any pre-configured scope.",
"type": "string",
@@ -2025,6 +2182,51 @@
"type": "string",
"const": "dialog:deny-save"
},
{
"description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n",
"type": "string",
"const": "http:default"
},
{
"description": "Enables the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch"
},
{
"description": "Enables the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-cancel"
},
{
"description": "Enables the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-read-body"
},
{
"description": "Enables the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:allow-fetch-send"
},
{
"description": "Denies the fetch command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch"
},
{
"description": "Denies the fetch_cancel command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-cancel"
},
{
"description": "Denies the fetch_read_body command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-read-body"
},
{
"description": "Denies the fetch_send command without any pre-configured scope.",
"type": "string",
"const": "http:deny-fetch-send"
},
{
"description": "Allows the log command",
"type": "string",
@@ -2345,6 +2547,31 @@
"type": "string",
"const": "shell:deny-stdin-write"
},
{
"description": "Allows connecting and sending data to a WebSocket server",
"type": "string",
"const": "websocket:default"
},
{
"description": "Enables the connect command without any pre-configured scope.",
"type": "string",
"const": "websocket:allow-connect"
},
{
"description": "Enables the send command without any pre-configured scope.",
"type": "string",
"const": "websocket:allow-send"
},
{
"description": "Denies the connect command without any pre-configured scope.",
"type": "string",
"const": "websocket:deny-connect"
},
{
"description": "Denies the send command without any pre-configured scope.",
"type": "string",
"const": "websocket:deny-send"
},
{
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n",
"type": "string",

View File

@@ -438,6 +438,15 @@ func (i *Instance) BinaryUpdates() *updates.Updater {
return i.binaryUpdates
}
// GetBinaryUpdateFile returns the file path of a binary update file.
func (i *Instance) GetBinaryUpdateFile(name string) (path string, err error) {
file, err := i.binaryUpdates.GetFile(name)
if err != nil {
return "", err
}
return file.Path(), nil
}
// IntelUpdates returns the updates module.
func (i *Instance) IntelUpdates() *updates.Updater {
return i.intelUpdates

View File

@@ -19,7 +19,7 @@ var (
// pidsByUserLock is also used for locking the socketInfo.PID on all socket.*Info structs.
pidsByUser = make(map[int][]int)
pidsByUserLock sync.RWMutex
fetchPidsByUser = utils.NewCallLimiter(10 * time.Millisecond)
fetchPidsByUser = utils.NewCallLimiter2(10 * time.Millisecond)
)
// getPidsByUser returns the cached PIDs for the given UID.

View File

@@ -25,7 +25,7 @@ type tcpTable struct {
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
lastUpdateAt atomic.Int64
fetchLimiter *utils.CallLimiter
fetchLimiter *utils.CallLimiter2
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
dualStack *tcpTable
@@ -34,13 +34,13 @@ type tcpTable struct {
var (
tcp6Table = &tcpTable{
version: 6,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getTCP6Table,
}
tcp4Table = &tcpTable{
version: 4,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getTCP4Table,
}
)

View File

@@ -24,7 +24,7 @@ type udpTable struct {
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
lastUpdateAt atomic.Int64
fetchLimiter *utils.CallLimiter
fetchLimiter *utils.CallLimiter2
fetchTable func() (binds []*socket.BindInfo, err error)
states map[string]map[string]*udpState
@@ -52,14 +52,14 @@ const (
var (
udp6Table = &udpTable{
version: 6,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getUDP6Table,
states: make(map[string]map[string]*udpState),
}
udp4Table = &udpTable{
version: 4,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getUDP4Table,
states: make(map[string]map[string]*udpState),
}

View File

@@ -2,6 +2,7 @@ package process
import (
"errors"
"runtime"
"sync/atomic"
"github.com/safing/portmaster/base/log"
@@ -21,7 +22,12 @@ func (pm *ProcessModule) Manager() *mgr.Manager {
}
func (pm *ProcessModule) Start() error {
file, err := pm.instance.BinaryUpdates().GetFile("portmaster")
identifier := "portmaster"
if runtime.GOOS == "windows" {
identifier += ".exe"
}
file, err := pm.instance.BinaryUpdates().GetFile(identifier)
if err != nil {
log.Errorf("process: failed to get path of ui: %s", err)
} else {

View File

@@ -72,7 +72,7 @@ type Profile struct { //nolint:maligned // not worth the effort
// Icons holds a list of icons to represent the application.
Icons []binmeta.Icon
// Deprecated: LinkedPath used to point to the executableis this
// Deprecated: LinkedPath used to point to the executables this
// profile was created for.
// Until removed, it will be added to the Fingerprints as an exact path match.
LinkedPath string // constant

View File

@@ -2,35 +2,21 @@ package ui
import (
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/log"
)
func registerAPIEndpoints() error {
func (ui *UI) registerAPIEndpoints() error {
return api.RegisterEndpoint(api.Endpoint{
Path: "ui/reload",
Write: api.PermitUser,
ActionFunc: reloadUI,
ActionFunc: ui.reloadUI,
Name: "Reload UI Assets",
Description: "Removes all assets from the cache and reloads the current (possibly updated) version from disk when requested.",
})
}
func reloadUI(_ *api.Request) (msg string, err error) {
appsLock.Lock()
defer appsLock.Unlock()
func (ui *UI) reloadUI(_ *api.Request) (msg string, err error) {
// Close all archives.
for id, archiveFS := range apps {
err := archiveFS.Close()
if err != nil {
log.Warningf("ui: failed to close archive %s: %s", id, err)
}
}
// Reset index.
for key := range apps {
delete(apps, key)
}
ui.CloseArchives()
return "all ui archives successfully reloaded", nil
}

View File

@@ -1,27 +1,55 @@
package ui
import (
"errors"
"os"
"path/filepath"
"sync"
"sync/atomic"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/updates"
"github.com/spkg/zipfs"
)
func prep() error {
if err := registerAPIEndpoints(); err != nil {
return err
}
// UI serves the user interface files.
type UI struct {
mgr *mgr.Manager
instance instance
return registerRoutes()
archives map[string]*zipfs.FileSystem
archivesLock sync.RWMutex
upgradeLock atomic.Bool
}
func start() error {
// New returns a new UI module.
func New(instance instance) (*UI, error) {
m := mgr.New("UI")
ui := &UI{
mgr: m,
instance: instance,
archives: make(map[string]*zipfs.FileSystem),
}
if err := ui.registerAPIEndpoints(); err != nil {
return nil, err
}
if err := ui.registerRoutes(); err != nil {
return nil, err
}
return ui, nil
}
func (ui *UI) Manager() *mgr.Manager {
return ui.mgr
}
// Start starts the module.
func (ui *UI) Start() error {
// Create a dummy directory to which processes change their working directory
// to. Currently this includes the App and the Notifier. The aim is protect
// all other directories and increase compatibility should any process want
@@ -30,7 +58,7 @@ func start() error {
// may seem dangerous, but proper permission on the parent directory provide
// (some) protection.
// Processes must _never_ read from this directory.
execDir := filepath.Join(module.instance.DataDir(), "exec")
execDir := filepath.Join(ui.instance.DataDir(), "exec")
err := os.MkdirAll(execDir, 0o0777) //nolint:gosec // This is intentional.
if err != nil {
log.Warningf("ui: failed to create safe exec dir: %s", err)
@@ -45,52 +73,67 @@ func start() error {
return nil
}
// UI serves the user interface files.
type UI struct {
mgr *mgr.Manager
instance instance
}
func (ui *UI) Manager() *mgr.Manager {
return ui.mgr
}
// Start starts the module.
func (ui *UI) Start() error {
return start()
}
// Stop stops the module.
func (ui *UI) Stop() error {
return nil
}
var (
shimLoaded atomic.Bool
module *UI
)
func (ui *UI) getArchive(name string) (archive *zipfs.FileSystem, ok bool) {
ui.archivesLock.RLock()
defer ui.archivesLock.RUnlock()
// New returns a new UI module.
func New(instance instance) (*UI, error) {
if !shimLoaded.CompareAndSwap(false, true) {
return nil, errors.New("only one instance allowed")
}
m := mgr.New("UI")
module = &UI{
mgr: m,
instance: instance,
archive, ok = ui.archives[name]
return
}
func (ui *UI) setArchive(name string, archive *zipfs.FileSystem) {
ui.archivesLock.Lock()
defer ui.archivesLock.Unlock()
ui.archives[name] = archive
}
// CloseArchives closes all open archives.
func (ui *UI) CloseArchives() {
if ui == nil {
return
}
if err := prep(); err != nil {
return nil, err
ui.archivesLock.Lock()
defer ui.archivesLock.Unlock()
// Close archives.
for _, archive := range ui.archives {
if err := archive.Close(); err != nil {
ui.mgr.Warn("failed to close ui archive", "err", err)
}
}
return module, nil
// Reset map.
clear(ui.archives)
}
// EnableUpgradeLock enables the upgrade lock and closes all open archives.
func (ui *UI) EnableUpgradeLock() {
if ui == nil {
return
}
ui.upgradeLock.Store(true)
ui.CloseArchives()
}
// DisableUpgradeLock disables the upgrade lock.
func (ui *UI) DisableUpgradeLock() {
if ui == nil {
return
}
ui.upgradeLock.Store(false)
}
type instance interface {
DataDir() string
API() *api.API
BinaryUpdates() *updates.Updater
GetBinaryUpdateFile(name string) (path string, err error)
}

View File

@@ -9,26 +9,19 @@ import (
"net/url"
"path/filepath"
"strings"
"sync"
"github.com/spkg/zipfs"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/log"
"github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/updates"
)
var (
apps = make(map[string]*zipfs.FileSystem)
appsLock sync.RWMutex
)
func registerRoutes() error {
func (ui *UI) registerRoutes() error {
// Server assets.
api.RegisterHandler(
"/assets/{resPath:[a-zA-Z0-9/\\._-]+}",
&archiveServer{defaultModuleName: "assets"},
&archiveServer{ui: ui, defaultModuleName: "assets"},
)
// Add slash to plain module namespaces.
@@ -38,7 +31,7 @@ func registerRoutes() error {
)
// Serve modules.
srv := &archiveServer{}
srv := &archiveServer{ui: ui}
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/", srv)
api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", srv)
@@ -52,6 +45,7 @@ func registerRoutes() error {
}
type archiveServer struct {
ui *UI
defaultModuleName string
}
@@ -82,39 +76,35 @@ func (bs *archiveServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
resPath = "index.html"
}
appsLock.RLock()
archiveFS, ok := apps[moduleName]
appsLock.RUnlock()
archiveFS, ok := bs.ui.getArchive(moduleName)
if ok {
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
return
}
// Check if the upgrade lock is enabled.
if bs.ui.upgradeLock.Load() {
http.Error(w, "Resources locked, upgrade in progress.", http.StatusLocked)
return
}
// get file from update system
zipFile, err := module.instance.BinaryUpdates().GetFile(fmt.Sprintf("%s.zip", moduleName))
zipFile, err := bs.ui.instance.GetBinaryUpdateFile(fmt.Sprintf("%s.zip", moduleName))
if err != nil {
if errors.Is(err, updates.ErrNotFound) {
log.Tracef("ui: requested module %s does not exist", moduleName)
http.Error(w, err.Error(), http.StatusNotFound)
} else {
log.Tracef("ui: error loading module %s: %s", moduleName, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
log.Tracef("ui: error loading module %s: %s", moduleName, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Open archive from disk.
archiveFS, err = zipfs.New(zipFile.Path())
archiveFS, err = zipfs.New(zipFile)
if err != nil {
log.Tracef("ui: error prepping module %s: %s", moduleName, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
appsLock.Lock()
apps[moduleName] = archiveFS
appsLock.Unlock()
bs.ui.setArchive(moduleName, archiveFS)
ServeFileFromArchive(w, r, moduleName, archiveFS, resPath)
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/configure"
"github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/ui"
)
const (
@@ -645,4 +646,5 @@ type instance interface {
Restart()
Shutdown()
Notifications() *notifications.Notifications
UI() *ui.UI
}

View File

@@ -24,6 +24,10 @@ func (u *Updater) upgrade(downloader *Downloader, ignoreVersion bool) error {
}
}
// Unload UI assets to be able to move files on Windows.
u.instance.UI().EnableUpgradeLock()
defer u.instance.UI().DisableUpgradeLock()
// Execute the upgrade.
upgradeError := u.upgradeMoveFiles(downloader)
if upgradeError == nil {