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

View File

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

View File

@@ -1,12 +1,12 @@
{ {
"name": "portmaster", "name": "portmaster",
"version": "0.8.11", "version": "2.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "portmaster", "name": "portmaster",
"version": "0.8.11", "version": "2.0.1",
"dependencies": { "dependencies": {
"@angular/animations": "^16.0.1", "@angular/animations": "^16.0.1",
"@angular/cdk": "^16.0.1", "@angular/cdk": "^16.0.1",
@@ -28,9 +28,11 @@
"@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",
"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",
@@ -4844,6 +4846,15 @@
"@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",
@@ -4871,6 +4882,15 @@
"@tauri-apps/api": "^2.0.0" "@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": { "node_modules/@tootallnate/once": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "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-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.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",

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-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.2.1"
clap_lex = "0.7.2" clap_lex = "0.7.2"

File diff suppressed because one or more lines are too long

View File

@@ -37,7 +37,7 @@
], ],
"definitions": { "definitions": {
"Capability": { "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", "type": "object",
"required": [ "required": [
"identifier", "identifier",
@@ -70,14 +70,14 @@
"type": "boolean" "type": "boolean"
}, },
"windows": { "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", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"webviews": { "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", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
@@ -134,6 +134,123 @@
"description": "Reference a permission or permission set by identifier and extends its scope.", "description": "Reference a permission or permission set by identifier and extends its scope.",
"type": "object", "type": "object",
"allOf": [ "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": { "if": {
"properties": { "properties": {
@@ -465,11 +582,26 @@
"type": "string", "type": "string",
"const": "core:app:allow-default-window-icon" "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.", "description": "Enables the name command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "core:app:allow-name" "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.", "description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -500,11 +632,26 @@
"type": "string", "type": "string",
"const": "core:app:deny-default-window-icon" "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.", "description": "Denies the name command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "core:app:deny-name" "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.", "description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -1300,6 +1447,11 @@
"type": "string", "type": "string",
"const": "core:window:allow-internal-toggle-maximize" "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.", "description": "Enables the is_closable command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -1665,6 +1817,11 @@
"type": "string", "type": "string",
"const": "core:window:deny-internal-toggle-maximize" "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.", "description": "Denies the is_closable command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -2025,6 +2182,51 @@
"type": "string", "type": "string",
"const": "dialog:deny-save" "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", "description": "Allows the log command",
"type": "string", "type": "string",
@@ -2345,6 +2547,31 @@
"type": "string", "type": "string",
"const": "shell:deny-stdin-write" "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", "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", "type": "string",

View File

@@ -37,7 +37,7 @@
], ],
"definitions": { "definitions": {
"Capability": { "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", "type": "object",
"required": [ "required": [
"identifier", "identifier",
@@ -70,14 +70,14 @@
"type": "boolean" "type": "boolean"
}, },
"windows": { "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", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"webviews": { "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", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
@@ -134,6 +134,123 @@
"description": "Reference a permission or permission set by identifier and extends its scope.", "description": "Reference a permission or permission set by identifier and extends its scope.",
"type": "object", "type": "object",
"allOf": [ "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": { "if": {
"properties": { "properties": {
@@ -465,11 +582,26 @@
"type": "string", "type": "string",
"const": "core:app:allow-default-window-icon" "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.", "description": "Enables the name command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "core:app:allow-name" "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.", "description": "Enables the set_app_theme command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -500,11 +632,26 @@
"type": "string", "type": "string",
"const": "core:app:deny-default-window-icon" "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.", "description": "Denies the name command without any pre-configured scope.",
"type": "string", "type": "string",
"const": "core:app:deny-name" "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.", "description": "Denies the set_app_theme command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -1300,6 +1447,11 @@
"type": "string", "type": "string",
"const": "core:window:allow-internal-toggle-maximize" "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.", "description": "Enables the is_closable command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -1665,6 +1817,11 @@
"type": "string", "type": "string",
"const": "core:window:deny-internal-toggle-maximize" "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.", "description": "Denies the is_closable command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -2025,6 +2182,51 @@
"type": "string", "type": "string",
"const": "dialog:deny-save" "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", "description": "Allows the log command",
"type": "string", "type": "string",
@@ -2345,6 +2547,31 @@
"type": "string", "type": "string",
"const": "shell:deny-stdin-write" "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", "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", "type": "string",

View File

@@ -438,6 +438,15 @@ func (i *Instance) BinaryUpdates() *updates.Updater {
return i.binaryUpdates 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. // IntelUpdates returns the updates module.
func (i *Instance) IntelUpdates() *updates.Updater { func (i *Instance) IntelUpdates() *updates.Updater {
return i.intelUpdates return i.intelUpdates

View File

@@ -19,7 +19,7 @@ var (
// pidsByUserLock is also used for locking the socketInfo.PID on all socket.*Info structs. // pidsByUserLock is also used for locking the socketInfo.PID on all socket.*Info structs.
pidsByUser = make(map[int][]int) pidsByUser = make(map[int][]int)
pidsByUserLock sync.RWMutex pidsByUserLock sync.RWMutex
fetchPidsByUser = utils.NewCallLimiter(10 * time.Millisecond) fetchPidsByUser = utils.NewCallLimiter2(10 * time.Millisecond)
) )
// getPidsByUser returns the cached PIDs for the given UID. // 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 stores the time when the tables where last updated as unix nanoseconds.
lastUpdateAt atomic.Int64 lastUpdateAt atomic.Int64
fetchLimiter *utils.CallLimiter fetchLimiter *utils.CallLimiter2
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error) fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
dualStack *tcpTable dualStack *tcpTable
@@ -34,13 +34,13 @@ type tcpTable struct {
var ( var (
tcp6Table = &tcpTable{ tcp6Table = &tcpTable{
version: 6, version: 6,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates), fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getTCP6Table, fetchTable: getTCP6Table,
} }
tcp4Table = &tcpTable{ tcp4Table = &tcpTable{
version: 4, version: 4,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates), fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getTCP4Table, 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 stores the time when the tables where last updated as unix nanoseconds.
lastUpdateAt atomic.Int64 lastUpdateAt atomic.Int64
fetchLimiter *utils.CallLimiter fetchLimiter *utils.CallLimiter2
fetchTable func() (binds []*socket.BindInfo, err error) fetchTable func() (binds []*socket.BindInfo, err error)
states map[string]map[string]*udpState states map[string]map[string]*udpState
@@ -52,14 +52,14 @@ const (
var ( var (
udp6Table = &udpTable{ udp6Table = &udpTable{
version: 6, version: 6,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates), fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getUDP6Table, fetchTable: getUDP6Table,
states: make(map[string]map[string]*udpState), states: make(map[string]map[string]*udpState),
} }
udp4Table = &udpTable{ udp4Table = &udpTable{
version: 4, version: 4,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates), fetchLimiter: utils.NewCallLimiter2(minDurationBetweenTableUpdates),
fetchTable: getUDP4Table, fetchTable: getUDP4Table,
states: make(map[string]map[string]*udpState), states: make(map[string]map[string]*udpState),
} }

View File

@@ -2,6 +2,7 @@ package process
import ( import (
"errors" "errors"
"runtime"
"sync/atomic" "sync/atomic"
"github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/log"
@@ -21,7 +22,12 @@ func (pm *ProcessModule) Manager() *mgr.Manager {
} }
func (pm *ProcessModule) Start() error { 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 { if err != nil {
log.Errorf("process: failed to get path of ui: %s", err) log.Errorf("process: failed to get path of ui: %s", err)
} else { } 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 holds a list of icons to represent the application.
Icons []binmeta.Icon 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. // profile was created for.
// Until removed, it will be added to the Fingerprints as an exact path match. // Until removed, it will be added to the Fingerprints as an exact path match.
LinkedPath string // constant LinkedPath string // constant

View File

@@ -2,35 +2,21 @@ package ui
import ( import (
"github.com/safing/portmaster/base/api" "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{ return api.RegisterEndpoint(api.Endpoint{
Path: "ui/reload", Path: "ui/reload",
Write: api.PermitUser, Write: api.PermitUser,
ActionFunc: reloadUI, ActionFunc: ui.reloadUI,
Name: "Reload UI Assets", Name: "Reload UI Assets",
Description: "Removes all assets from the cache and reloads the current (possibly updated) version from disk when requested.", 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) { func (ui *UI) reloadUI(_ *api.Request) (msg string, err error) {
appsLock.Lock()
defer appsLock.Unlock()
// Close all archives. // Close all archives.
for id, archiveFS := range apps { ui.CloseArchives()
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)
}
return "all ui archives successfully reloaded", nil return "all ui archives successfully reloaded", nil
} }

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import (
"github.com/safing/portmaster/base/utils" "github.com/safing/portmaster/base/utils"
"github.com/safing/portmaster/service/configure" "github.com/safing/portmaster/service/configure"
"github.com/safing/portmaster/service/mgr" "github.com/safing/portmaster/service/mgr"
"github.com/safing/portmaster/service/ui"
) )
const ( const (
@@ -645,4 +646,5 @@ type instance interface {
Restart() Restart()
Shutdown() Shutdown()
Notifications() *notifications.Notifications 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. // Execute the upgrade.
upgradeError := u.upgradeMoveFiles(downloader) upgradeError := u.upgradeMoveFiles(downloader)
if upgradeError == nil { if upgradeError == nil {