From 6aa13c23355fb7e55d4836c48e704c2fde564748 Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Tue, 19 Jul 2022 10:27:29 +0200 Subject: [PATCH 1/2] Add endpoint to retrieve the current profile ID --- core/api.go | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/core/api.go b/core/api.go index 97adb4cd..6c65e470 100644 --- a/core/api.go +++ b/core/api.go @@ -1,14 +1,26 @@ package core import ( + "context" + "encoding/hex" + "errors" + "fmt" + "net" "net/http" + "net/url" + "strconv" + "time" "github.com/safing/portbase/api" "github.com/safing/portbase/config" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" + "github.com/safing/portbase/notifications" + "github.com/safing/portbase/rng" "github.com/safing/portbase/utils/debug" "github.com/safing/portmaster/compat" + "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/process" "github.com/safing/portmaster/resolver" "github.com/safing/portmaster/status" "github.com/safing/portmaster/updates" @@ -55,6 +67,48 @@ func registerAPIEndpoints() error { return err } + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "app/auth", + Read: api.PermitAnyone, + BelongsTo: module, + StructFunc: authorizeApp, + Name: "Call to request an authentication token", + Parameters: []api.Parameter{ + { + Method: http.MethodGet, + Field: "app-name", + Description: "The name of the application requesting access", + }, + { + Method: http.MethodGet, + Field: "read", + Description: "The requested read permission", + }, + { + Method: http.MethodGet, + Field: "write", + Description: "The requested write permission", + }, + { + Method: http.MethodGet, + Field: "ttl", + Description: "The time-to-live for the new access token. Defaults to 24h", + }, + }, + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "app/profile", + Read: api.PermitUser, + BelongsTo: module, + StructFunc: getMyProfile, + Name: "Get the ID of the calling profile", + }); err != nil { + return err + } + return nil } @@ -99,3 +153,160 @@ func debugInfo(ar *api.Request) (data []byte, err error) { // Return data. return di.Bytes(), nil } + +func getPermission(p string) api.Permission { + switch p { + case "user": + return api.PermitUser + case "admin": + return api.PermitAdmin + default: + return api.NotSupported + } +} + +func getMyProfile(ar *api.Request) (interface{}, error) { + // get remote IP/Port + remoteIP, remotePort, err := parseHostPort(ar.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("failed to get remote IP/Port: %w", err) + } + + pkt := &packet.Info{ + Inbound: false, // outbound as we are looking for the process of the source address + Version: packet.IPv4, + Protocol: packet.TCP, + Src: remoteIP, // source as in the process we are looking for + SrcPort: remotePort, // source as in the process we are looking for + } + + proc, _, err := process.GetProcessByConnection(ar.Context(), pkt) + if err != nil { + return nil, err + } + + localProfile := proc.Profile().LocalProfile() + return map[string]interface{}{ + "profile": localProfile.ID, + "source": localProfile.Source, + "name": localProfile.Name, + }, nil +} + +func parseHostPort(address string) (net.IP, uint16, error) { + ipString, portString, err := net.SplitHostPort(address) + if err != nil { + return nil, 0, err + } + + ip := net.ParseIP(ipString) + if ip == nil { + return nil, 0, errors.New("invalid IP address") + } + + port, err := strconv.ParseUint(portString, 10, 16) + if err != nil { + return nil, 0, err + } + + return ip, uint16(port), nil +} + +func authorizeApp(ar *api.Request) (interface{}, error) { + appName := ar.Request.URL.Query().Get("app-name") + readPermStr := ar.Request.URL.Query().Get("read") + writePermStr := ar.Request.URL.Query().Get("write") + + ttl := time.Hour * 24 + if ttlStr := ar.Request.URL.Query().Get("ttl"); ttlStr != "" { + var err error + ttl, err = time.ParseDuration(ttlStr) + if err != nil { + return nil, err + } + } + + if getPermission(readPermStr) <= api.NotSupported { + return nil, fmt.Errorf("invalid read permission") + } + if getPermission(writePermStr) <= api.NotSupported { + return nil, fmt.Errorf("invalid read permission") + } + + // appIcon := ar.Request.URL.Query().Get("app-icon") + // TODO(ppacher): get the guessed mime-type from appIcon and make sure it's an image + + n := notifications.Notification{ + Type: notifications.Prompt, + EventID: "core:authorize-app-" + time.Now().String(), + Title: "An app requests access to the Portmaster", + Message: "Allow " + appName + " to query and modify the Portmaster?", + ShowOnSystem: true, + Expires: time.Now().Add(time.Minute).UnixNano(), + AvailableActions: []*notifications.Action{ + { + ID: "allow", + Text: "Authorize", + }, + { + ID: "deny", + Text: "Deny", + }, + }, + } + + ch := make(chan string) + + validUntil := time.Now().Add(ttl) + + n.SetActionFunction(func(ctx context.Context, n *notifications.Notification) error { + n.Lock() + defer n.Unlock() + + if n.SelectedActionID != "allow" { + close(ch) + return nil + } + + keys := config.Concurrent.GetAsStringArray(api.CfgAPIKeys, []string{})() + + newKeyData, err := rng.Bytes(8) + if err != nil { + return err + } + + newKeyHex := hex.EncodeToString(newKeyData) + + query := url.Values{ + "read": []string{readPermStr}, + "write": []string{writePermStr}, + "expires": []string{validUntil.Format(time.RFC3339)}, + } + + keys = append(keys, fmt.Sprintf("%s?%s", newKeyHex, query.Encode())) + + if err := config.SetConfigOption(api.CfgAPIKeys, keys); err != nil { + return err + } + + ch <- newKeyHex + + return nil + }) + + n.Save() + + select { + case key := <-ch: + if len(key) == 0 { + return nil, fmt.Errorf("access denied") + } + + return map[string]interface{}{ + "key": key, + "validUntil": validUntil, + }, nil + case <-ar.Context().Done(): + return nil, fmt.Errorf("timeout") + } +} From d49232a37b40e8b0a2b27ac7c3da4015cf4a4b3a Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 27 Jul 2022 15:14:52 +0200 Subject: [PATCH 2/2] Improve app-auth request flow and implement review suggestions --- core/api.go | 61 ++++++++++--------------------------- network/netutils/address.go | 24 +++++++++++++++ process/find.go | 26 ++++++++++++++++ 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/core/api.go b/core/api.go index 6c65e470..3ef37924 100644 --- a/core/api.go +++ b/core/api.go @@ -3,12 +3,9 @@ package core import ( "context" "encoding/hex" - "errors" "fmt" - "net" "net/http" "net/url" - "strconv" "time" "github.com/safing/portbase/api" @@ -19,7 +16,6 @@ import ( "github.com/safing/portbase/rng" "github.com/safing/portbase/utils/debug" "github.com/safing/portmaster/compat" - "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/process" "github.com/safing/portmaster/resolver" "github.com/safing/portmaster/status" @@ -72,7 +68,7 @@ func registerAPIEndpoints() error { Read: api.PermitAnyone, BelongsTo: module, StructFunc: authorizeApp, - Name: "Call to request an authentication token", + Name: "Request an authentication token with a given set of permissions. The user will be prompted to either authorize or deny the request. Used for external or third-party tool integrations.", Parameters: []api.Parameter{ { Method: http.MethodGet, @@ -154,7 +150,10 @@ func debugInfo(ar *api.Request) (data []byte, err error) { return di.Bytes(), nil } -func getPermission(p string) api.Permission { +// getSavePermission returns the requested api.Permission from p. +// It only allows "user" and "admin" as external processes should +// never be able to request "self". +func getSavePermission(p string) api.Permission { switch p { case "user": return api.PermitUser @@ -166,26 +165,13 @@ func getPermission(p string) api.Permission { } func getMyProfile(ar *api.Request) (interface{}, error) { - // get remote IP/Port - remoteIP, remotePort, err := parseHostPort(ar.RemoteAddr) - if err != nil { - return nil, fmt.Errorf("failed to get remote IP/Port: %w", err) - } - - pkt := &packet.Info{ - Inbound: false, // outbound as we are looking for the process of the source address - Version: packet.IPv4, - Protocol: packet.TCP, - Src: remoteIP, // source as in the process we are looking for - SrcPort: remotePort, // source as in the process we are looking for - } - - proc, _, err := process.GetProcessByConnection(ar.Context(), pkt) + proc, err := process.GetProcessByRequestOrigin(ar) if err != nil { return nil, err } localProfile := proc.Profile().LocalProfile() + return map[string]interface{}{ "profile": localProfile.ID, "source": localProfile.Source, @@ -193,25 +179,6 @@ func getMyProfile(ar *api.Request) (interface{}, error) { }, nil } -func parseHostPort(address string) (net.IP, uint16, error) { - ipString, portString, err := net.SplitHostPort(address) - if err != nil { - return nil, 0, err - } - - ip := net.ParseIP(ipString) - if ip == nil { - return nil, 0, errors.New("invalid IP address") - } - - port, err := strconv.ParseUint(portString, 10, 16) - if err != nil { - return nil, 0, err - } - - return ip, uint16(port), nil -} - func authorizeApp(ar *api.Request) (interface{}, error) { appName := ar.Request.URL.Query().Get("app-name") readPermStr := ar.Request.URL.Query().Get("read") @@ -226,21 +193,25 @@ func authorizeApp(ar *api.Request) (interface{}, error) { } } - if getPermission(readPermStr) <= api.NotSupported { + // convert the requested read and write permissions to their api.Permission + // value. This ensures only "user" or "admin" permissions can be requested. + if getSavePermission(readPermStr) <= api.NotSupported { return nil, fmt.Errorf("invalid read permission") } - if getPermission(writePermStr) <= api.NotSupported { + if getSavePermission(writePermStr) <= api.NotSupported { return nil, fmt.Errorf("invalid read permission") } - // appIcon := ar.Request.URL.Query().Get("app-icon") - // TODO(ppacher): get the guessed mime-type from appIcon and make sure it's an image + proc, err := process.GetProcessByRequestOrigin(ar) + if err != nil { + return nil, fmt.Errorf("failed to identify requesting process: %w", err) + } n := notifications.Notification{ Type: notifications.Prompt, EventID: "core:authorize-app-" + time.Now().String(), Title: "An app requests access to the Portmaster", - Message: "Allow " + appName + " to query and modify the Portmaster?", + Message: "Allow " + appName + " (" + proc.Profile().LocalProfile().Name + ") to query and modify the Portmaster?\n\nBinary: " + proc.Path, ShowOnSystem: true, Expires: time.Now().Add(time.Minute).UnixNano(), AvailableActions: []*notifications.Action{ diff --git a/network/netutils/address.go b/network/netutils/address.go index b7ef1165..59d2a262 100644 --- a/network/netutils/address.go +++ b/network/netutils/address.go @@ -1,10 +1,14 @@ package netutils import ( + "errors" "fmt" "net" + "strconv" ) +var errInvalidIP = errors.New("invalid IP address") + // IPFromAddr extracts or parses the IP address contained in the given address. func IPFromAddr(addr net.Addr) (net.IP, error) { // Convert addr to IP if needed. @@ -28,3 +32,23 @@ func IPFromAddr(addr net.Addr) (net.IP, error) { return ip, nil } } + +// ParseHostPort parses a :port formatted address. +func ParseHostPort(address string) (net.IP, uint16, error) { + ipString, portString, err := net.SplitHostPort(address) + if err != nil { + return nil, 0, err + } + + ip := net.ParseIP(ipString) + if ip == nil { + return nil, 0, errInvalidIP + } + + port, err := strconv.ParseUint(portString, 10, 16) + if err != nil { + return nil, 0, err + } + + return ip, uint16(port), nil +} diff --git a/process/find.go b/process/find.go index c04599d7..425fee78 100644 --- a/process/find.go +++ b/process/find.go @@ -6,7 +6,9 @@ import ( "net" "time" + "github.com/safing/portbase/api" "github.com/safing/portbase/log" + "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/state" "github.com/safing/portmaster/profile" @@ -94,3 +96,27 @@ func GetNetworkHost(ctx context.Context, remoteIP net.IP) (process *Process, err return networkHost, nil } + +// GetProcessByRequestOrigin returns the process that initiated the API request ar. +func GetProcessByRequestOrigin(ar *api.Request) (*Process, error) { + // get remote IP/Port + remoteIP, remotePort, err := netutils.ParseHostPort(ar.RemoteAddr) + if err != nil { + return nil, fmt.Errorf("failed to get remote IP/Port: %w", err) + } + + pkt := &packet.Info{ + Inbound: false, // outbound as we are looking for the process of the source address + Version: packet.IPv4, + Protocol: packet.TCP, + Src: remoteIP, // source as in the process we are looking for + SrcPort: remotePort, // source as in the process we are looking for + } + + proc, _, err := GetProcessByConnection(ar.Context(), pkt) + if err != nil { + return nil, err + } + + return proc, nil +}