Release to master
This commit is contained in:
21
Gopkg.lock
generated
21
Gopkg.lock
generated
@@ -84,12 +84,12 @@
|
|||||||
version = "v0.1.0"
|
version = "v0.1.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:cbec35fe4d5a4fba369a656a8cd65e244ea2c743007d8f6c1ccb132acf9d1296"
|
digest = "1:88e0b0baeb9072f0a4afbcf12dda615fc8be001d1802357538591155998da21b"
|
||||||
name = "github.com/gorilla/mux"
|
name = "github.com/hashicorp/go-version"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "00bdffe0f3c77e27d2cf6f5c70232a2d3e4d9c15"
|
revision = "ac23dc3fea5d1a983c43f6a0f6e2c13f0195d8bd"
|
||||||
version = "v1.7.3"
|
version = "v1.2.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||||
@@ -219,12 +219,17 @@
|
|||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:7e3e63385ebe2dd5210be1534c516d8ae33c7ffac74126e5243d43ba7222e0d4"
|
digest = "1:84945c0665ea5fc3ccbd067c35890a7d28e369131ac411b8a820b40115245c19"
|
||||||
name = "golang.org/x/sys"
|
name = "golang.org/x/sys"
|
||||||
packages = [
|
packages = [
|
||||||
"cpu",
|
"cpu",
|
||||||
"unix",
|
"unix",
|
||||||
"windows",
|
"windows",
|
||||||
|
"windows/registry",
|
||||||
|
"windows/svc",
|
||||||
|
"windows/svc/debug",
|
||||||
|
"windows/svc/eventlog",
|
||||||
|
"windows/svc/mgr",
|
||||||
]
|
]
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "04f50cda93cbb67f2afa353c52f342100e80e625"
|
revision = "04f50cda93cbb67f2afa353c52f342100e80e625"
|
||||||
@@ -242,7 +247,7 @@
|
|||||||
"github.com/google/gopacket/layers",
|
"github.com/google/gopacket/layers",
|
||||||
"github.com/google/gopacket/tcpassembly",
|
"github.com/google/gopacket/tcpassembly",
|
||||||
"github.com/google/renameio",
|
"github.com/google/renameio",
|
||||||
"github.com/gorilla/mux",
|
"github.com/hashicorp/go-version",
|
||||||
"github.com/miekg/dns",
|
"github.com/miekg/dns",
|
||||||
"github.com/oschwald/maxminddb-golang",
|
"github.com/oschwald/maxminddb-golang",
|
||||||
"github.com/satori/go.uuid",
|
"github.com/satori/go.uuid",
|
||||||
@@ -256,6 +261,10 @@
|
|||||||
"golang.org/x/net/icmp",
|
"golang.org/x/net/icmp",
|
||||||
"golang.org/x/net/ipv4",
|
"golang.org/x/net/ipv4",
|
||||||
"golang.org/x/sys/windows",
|
"golang.org/x/sys/windows",
|
||||||
|
"golang.org/x/sys/windows/svc",
|
||||||
|
"golang.org/x/sys/windows/svc/debug",
|
||||||
|
"golang.org/x/sys/windows/svc/eventlog",
|
||||||
|
"golang.org/x/sys/windows/svc/mgr",
|
||||||
]
|
]
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|||||||
@@ -54,10 +54,6 @@ ignored = ["github.com/safing/portbase/*"]
|
|||||||
name = "github.com/google/renameio"
|
name = "github.com/google/renameio"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/gorilla/mux"
|
|
||||||
version = "1.7.3"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/miekg/dns"
|
name = "github.com/miekg/dns"
|
||||||
version = "1.1.15"
|
version = "1.1.15"
|
||||||
@@ -101,3 +97,7 @@ ignored = ["github.com/safing/portbase/*"]
|
|||||||
[prune]
|
[prune]
|
||||||
go-tests = true
|
go-tests = true
|
||||||
unused-packages = true
|
unused-packages = true
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/hashicorp/go-version"
|
||||||
|
version = "1.2.0"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package algs
|
package algs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package algs
|
package algs
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|||||||
60
core/base.go
Normal file
60
core/base.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/api"
|
||||||
|
"github.com/safing/portbase/database/dbmodule"
|
||||||
|
"github.com/safing/portbase/modules"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/core/structure"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataDir string
|
||||||
|
databaseDir string
|
||||||
|
|
||||||
|
baseModule = modules.Register("base", prepBase, nil, nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&dataDir, "data", "", "set data directory")
|
||||||
|
flag.StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
|
||||||
|
|
||||||
|
notifications.SetPersistenceBasePath("core:notifications")
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepBase() error {
|
||||||
|
// backwards compatibility
|
||||||
|
if dataDir == "" {
|
||||||
|
dataDir = databaseDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// check data dir
|
||||||
|
if dataDir == "" {
|
||||||
|
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize structure
|
||||||
|
err := structure.Initialize(dataDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set database location
|
||||||
|
dbmodule.SetDatabaseLocation("", structure.Root())
|
||||||
|
|
||||||
|
// init config
|
||||||
|
logFlagOverrides()
|
||||||
|
err = registerConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set api listen address
|
||||||
|
api.SetDefaultAPIListenAddress("127.0.0.1:817")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
39
core/config.go
Normal file
39
core/config.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/config"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
devMode config.BoolOption
|
||||||
|
defaultDevMode bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&defaultDevMode, "devmode", false, "force development mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFlagOverrides() {
|
||||||
|
if defaultDevMode {
|
||||||
|
log.Warning("core: core/devMode default config is being forced by -devmode flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerConfig() error {
|
||||||
|
err := config.Register(&config.Option{
|
||||||
|
Name: "Development Mode",
|
||||||
|
Key: "core/devMode",
|
||||||
|
Description: "In Development Mode security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
|
||||||
|
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||||
|
OptType: config.OptTypeBool,
|
||||||
|
DefaultValue: defaultDevMode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
19
core/core.go
Normal file
19
core/core.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
coreModule = modules.Register("core", nil, startCore, nil, "base", "database", "config", "api", "random")
|
||||||
|
)
|
||||||
|
|
||||||
|
func startCore() error {
|
||||||
|
if err := startPlatformSpecific(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start plattform-specific components: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return registerDatabases()
|
||||||
|
}
|
||||||
@@ -2,21 +2,12 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/safing/portbase/database"
|
"github.com/safing/portbase/database"
|
||||||
"github.com/safing/portbase/modules"
|
|
||||||
"github.com/safing/portbase/notifications"
|
|
||||||
|
|
||||||
// module dependencies
|
// module dependencies
|
||||||
_ "github.com/safing/portbase/database/dbmodule"
|
|
||||||
_ "github.com/safing/portbase/database/storage/bbolt"
|
_ "github.com/safing/portbase/database/storage/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func registerDatabases() error {
|
||||||
modules.Register("core", nil, start, nil, "database")
|
|
||||||
|
|
||||||
notifications.SetPersistenceBasePath("core:notifications")
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() error {
|
|
||||||
_, err := database.Register(&database.Database{
|
_, err := database.Register(&database.Database{
|
||||||
Name: "core",
|
Name: "core",
|
||||||
Description: "Holds core data, such as settings and profiles",
|
Description: "Holds core data, such as settings and profiles",
|
||||||
|
|||||||
8
core/os_default.go
Normal file
8
core/os_default.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
// only return on Fatal error!
|
||||||
|
func startPlatformSpecific() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
16
core/os_windows.go
Normal file
16
core/os_windows.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/utils/osdetail"
|
||||||
|
)
|
||||||
|
|
||||||
|
// only return on Fatal error!
|
||||||
|
func startPlatformSpecific() error {
|
||||||
|
// We can't catch errors when calling WindowsNTVersion() in logging, so we call the function here, just to catch possible errors
|
||||||
|
if _, err := osdetail.WindowsNTVersion(); err != nil {
|
||||||
|
log.Errorf("failed to obtain WindowsNTVersion: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
27
core/structure/dirs.go
Normal file
27
core/structure/dirs.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package structure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
root *utils.DirStructure
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize initializes the data root directory
|
||||||
|
func Initialize(rootDir string, perm os.FileMode) error {
|
||||||
|
root = utils.NewDirStructure(rootDir, perm)
|
||||||
|
return root.Ensure()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root returns the data root directory.
|
||||||
|
func Root() *utils.DirStructure {
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRootDir calls ChildDir() on the data root directory.
|
||||||
|
func NewRootDir(dirName string, perm os.FileMode) (childDir *utils.DirStructure) {
|
||||||
|
return root.ChildDir(dirName, perm)
|
||||||
|
}
|
||||||
110
firewall/api.go
Normal file
110
firewall/api.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/utils"
|
||||||
|
"github.com/safing/portmaster/core/structure"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/network/packet"
|
||||||
|
"github.com/safing/portmaster/process"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataRoot *utils.DirStructure
|
||||||
|
|
||||||
|
apiPortSet bool
|
||||||
|
apiPort uint16
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepAPIAuth() error {
|
||||||
|
dataRoot = structure.Root()
|
||||||
|
return api.SetAuthenticator(apiAuthenticator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startAPIAuth() {
|
||||||
|
var err error
|
||||||
|
_, apiPort, err = parseHostPort(apiListenAddress())
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("firewall: failed to parse API address for improved api auth mechanism: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apiPortSet = true
|
||||||
|
log.Tracef("firewall: api port set to %d", apiPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiAuthenticator(s *http.Server, r *http.Request) (grantAccess bool, err error) {
|
||||||
|
if devMode() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get local IP/Port
|
||||||
|
localIP, localPort, err := parseHostPort(s.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get local IP/Port: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get remote IP/Port
|
||||||
|
remoteIP, remotePort, err := parseHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get remote IP/Port: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var procsChecked []string
|
||||||
|
|
||||||
|
// get process
|
||||||
|
proc, err := process.GetProcessByEndpoints(r.Context(), remoteIP, remotePort, localIP, localPort, packet.TCP) // switch reverse/local to get remote process
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get process: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// go up up to two levels, if we don't match
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
// check if the requesting process is in database root / updates dir
|
||||||
|
if strings.HasPrefix(proc.Path, dataRoot.Path) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// add checked process to list
|
||||||
|
procsChecked = append(procsChecked, proc.Path)
|
||||||
|
|
||||||
|
if i < 2 {
|
||||||
|
// get parent process
|
||||||
|
proc, err = process.GetOrFindProcess(context.Background(), proc.ParentPid)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get process: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("firewall: denying api access to %s - also checked %s (trusted root is %s)", procsChecked[0], strings.Join(procsChecked[1:], " "), dataRoot.Path)
|
||||||
|
return false, 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
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@ var (
|
|||||||
permanentVerdicts config.BoolOption
|
permanentVerdicts config.BoolOption
|
||||||
filterDNSByScope status.SecurityLevelOption
|
filterDNSByScope status.SecurityLevelOption
|
||||||
filterDNSByProfile status.SecurityLevelOption
|
filterDNSByProfile status.SecurityLevelOption
|
||||||
|
promptTimeout config.IntOption
|
||||||
|
|
||||||
|
devMode config.BoolOption
|
||||||
|
apiListenAddress config.StringOption
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerConfig() error {
|
func registerConfig() error {
|
||||||
@@ -55,5 +59,21 @@ func registerConfig() error {
|
|||||||
}
|
}
|
||||||
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
|
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
|
||||||
|
|
||||||
|
err = config.Register(&config.Option{
|
||||||
|
Name: "Timeout for prompt notifications",
|
||||||
|
Key: "firewall/promptTimeout",
|
||||||
|
Description: "Amount of time how long Portmaster will wait for a response when prompting about a connection via a notification. In seconds.",
|
||||||
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
|
OptType: config.OptTypeInt,
|
||||||
|
DefaultValue: 60,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
promptTimeout = config.Concurrent.GetAsInt("firewall/promptTimeout", 30)
|
||||||
|
|
||||||
|
devMode = config.Concurrent.GetAsBool("firewall/permanentVerdicts", false)
|
||||||
|
apiListenAddress = config.GetAsString("api/listenAddress", "")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ func prep() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = prepAPIAuth()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
_, localNet4, err = net.ParseCIDR("127.0.0.0/24")
|
_, localNet4, err = net.ParseCIDR("127.0.0.0/24")
|
||||||
// Yes, this would normally be 127.0.0.0/8
|
// Yes, this would normally be 127.0.0.0/8
|
||||||
// TODO: figure out any side effects
|
// TODO: figure out any side effects
|
||||||
@@ -77,12 +82,9 @@ func prep() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
|
startAPIAuth()
|
||||||
go statLogger()
|
go statLogger()
|
||||||
go run()
|
go run()
|
||||||
// go run()
|
|
||||||
// go run()
|
|
||||||
// go run()
|
|
||||||
|
|
||||||
go portsInUseCleaner()
|
go portsInUseCleaner()
|
||||||
|
|
||||||
return interception.Start()
|
return interception.Start()
|
||||||
@@ -108,6 +110,15 @@ func handlePacket(pkt packet.Packet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// allow api access, if address was parsed successfully
|
||||||
|
if apiPortSet {
|
||||||
|
if (pkt.Info().DstPort == apiPort || pkt.Info().SrcPort == apiPort) && pkt.Info().Src.Equal(pkt.Info().Dst) {
|
||||||
|
log.Debugf("accepting api connection: %s", pkt)
|
||||||
|
pkt.PermanentAccept()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // redirect dns (if we know that it's not our own request)
|
// // redirect dns (if we know that it's not our own request)
|
||||||
// if pkt.IsOutbound() && intel.RemoteIsActiveNameserver(pkt) {
|
// if pkt.IsOutbound() && intel.RemoteIsActiveNameserver(pkt) {
|
||||||
// log.Debugf("redirecting dns: %s", pkt)
|
// log.Debugf("redirecting dns: %s", pkt)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package inspection
|
package inspection
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package interception
|
package interception
|
||||||
|
|
||||||
import "github.com/safing/portmaster/network/packet"
|
import "github.com/safing/portmaster/network/packet"
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func notifyDisableDNSCache() {
|
|||||||
ID: "windows-disable-dns-cache",
|
ID: "windows-disable-dns-cache",
|
||||||
Message: "The Portmaster needs the Windows Service \"DNS Client\" (dnscache) to be disabled for best effectiveness.",
|
Message: "The Portmaster needs the Windows Service \"DNS Client\" (dnscache) to be disabled for best effectiveness.",
|
||||||
Type: notifications.Warning,
|
Type: notifications.Warning,
|
||||||
}).Init().Save()
|
}).Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyRebootRequired() {
|
func notifyRebootRequired() {
|
||||||
@@ -89,5 +89,5 @@ func notifyRebootRequired() {
|
|||||||
ID: "windows-dnscache-reboot-required",
|
ID: "windows-dnscache-reboot-required",
|
||||||
Message: "Please restart your system to complete Portmaster integration.",
|
Message: "Please restart your system to complete Portmaster integration.",
|
||||||
Type: notifications.Warning,
|
Type: notifications.Warning,
|
||||||
}).Init().Save()
|
}).Save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package nfqueue
|
package nfqueue
|
||||||
|
|
||||||
// suspended for now
|
// suspended for now
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package nfqueue
|
package nfqueue
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package nfqueue
|
package nfqueue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/notifications"
|
|
||||||
"github.com/safing/portmaster/intel"
|
"github.com/safing/portmaster/intel"
|
||||||
"github.com/safing/portmaster/network"
|
"github.com/safing/portmaster/network"
|
||||||
"github.com/safing/portmaster/network/netutils"
|
"github.com/safing/portmaster/network/netutils"
|
||||||
@@ -16,7 +15,6 @@ import (
|
|||||||
"github.com/safing/portmaster/process"
|
"github.com/safing/portmaster/process"
|
||||||
"github.com/safing/portmaster/profile"
|
"github.com/safing/portmaster/profile"
|
||||||
"github.com/safing/portmaster/status"
|
"github.com/safing/portmaster/status"
|
||||||
"github.com/miekg/dns"
|
|
||||||
|
|
||||||
"github.com/agext/levenshtein"
|
"github.com/agext/levenshtein"
|
||||||
)
|
)
|
||||||
@@ -137,93 +135,7 @@ func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prompt
|
// prompt
|
||||||
|
prompt(comm, nil, nil, fqdn)
|
||||||
// first check if there is an existing notification for this.
|
|
||||||
nID := fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain)
|
|
||||||
nTTL := 15 * time.Second
|
|
||||||
n := notifications.Get(nID)
|
|
||||||
if n != nil {
|
|
||||||
// we were not here first, only get verdict, do not make changes
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-all", "permit-distinct":
|
|
||||||
comm.Accept("permitted by user")
|
|
||||||
default:
|
|
||||||
comm.Deny("denied by user")
|
|
||||||
}
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
comm.SetReason("user did not respond to prompt")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new notification
|
|
||||||
n = (¬ifications.Notification{
|
|
||||||
ID: nID,
|
|
||||||
Message: fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain),
|
|
||||||
Type: notifications.Prompt,
|
|
||||||
AvailableActions: []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-all",
|
|
||||||
Text: fmt.Sprintf("Permit all %s", comm.Domain),
|
|
||||||
},
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-distinct",
|
|
||||||
Text: fmt.Sprintf("Permit %s", comm.Domain),
|
|
||||||
},
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "deny",
|
|
||||||
Text: "Deny",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Expires: time.Now().Add(nTTL).Unix(),
|
|
||||||
}).Init().Save()
|
|
||||||
|
|
||||||
// react
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
n.Cancel()
|
|
||||||
|
|
||||||
new := &profile.EndpointPermission{
|
|
||||||
Type: profile.EptDomain,
|
|
||||||
Value: comm.Domain,
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-all":
|
|
||||||
new.Value = "." + new.Value
|
|
||||||
case "permit-distinct":
|
|
||||||
// everything already set
|
|
||||||
default:
|
|
||||||
// deny
|
|
||||||
new.Permit = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if new.Permit {
|
|
||||||
log.Infof("firewall: user permitted communication %s -> %s", comm.Process(), new.Value)
|
|
||||||
comm.Accept("permitted by user")
|
|
||||||
} else {
|
|
||||||
log.Infof("firewall: user denied communication %s -> %s", comm.Process(), new.Value)
|
|
||||||
comm.Deny("denied by user")
|
|
||||||
}
|
|
||||||
|
|
||||||
profileSet.Lock()
|
|
||||||
defer profileSet.Unlock()
|
|
||||||
userProfile := profileSet.UserProfile()
|
|
||||||
userProfile.Lock()
|
|
||||||
defer userProfile.Unlock()
|
|
||||||
|
|
||||||
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
|
||||||
go userProfile.Save("")
|
|
||||||
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
n.Cancel()
|
|
||||||
comm.SetReason("user did not respond to prompt")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterDNSResponse filters a dns response according to the application profile and settings.
|
// FilterDNSResponse filters a dns response according to the application profile and settings.
|
||||||
@@ -573,134 +485,8 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// first check if there is an existing notification for this.
|
// prompt
|
||||||
var nID string
|
prompt(comm, link, pkt, fqdn)
|
||||||
switch {
|
|
||||||
case comm.Direction:
|
|
||||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s-%d-%d", comm.Process().Pid, comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
case fqdn == "":
|
|
||||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s-%d-%d", comm.Process().Pid, comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
default:
|
|
||||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s-%d-%d", comm.Process().Pid, comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
}
|
|
||||||
nTTL := 15 * time.Second
|
|
||||||
n := notifications.Get(nID)
|
|
||||||
|
|
||||||
if n != nil {
|
|
||||||
// we were not here first, only get verdict, do not make changes
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-domain-all", "permit-domain-distinct", "permit-ip", "permit-ip-incoming":
|
|
||||||
link.Accept("permitted by user")
|
|
||||||
default:
|
|
||||||
link.Deny("denied by user")
|
|
||||||
}
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
link.Deny("user did not respond to prompt")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new notification
|
|
||||||
n = (¬ifications.Notification{
|
|
||||||
ID: nID,
|
|
||||||
Type: notifications.Prompt,
|
|
||||||
Expires: time.Now().Add(nTTL).Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case comm.Direction:
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", comm.Process(), remoteIP, protocol, dstPort)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-ip-incoming",
|
|
||||||
Text: fmt.Sprintf("Permit serving to %s", remoteIP),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case fqdn == "":
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", comm.Process(), remoteIP, protocol, dstPort)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-ip",
|
|
||||||
Text: fmt.Sprintf("Permit %s", remoteIP),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-domain-all",
|
|
||||||
Text: fmt.Sprintf("Permit all %s", comm.Domain),
|
|
||||||
},
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-domain-distinct",
|
|
||||||
Text: fmt.Sprintf("Permit %s", comm.Domain),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n.AvailableActions = append(n.AvailableActions, ¬ifications.Action{
|
|
||||||
ID: "deny",
|
|
||||||
Text: "deny",
|
|
||||||
})
|
|
||||||
n.Init().Save()
|
|
||||||
|
|
||||||
// react
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
n.Cancel()
|
|
||||||
|
|
||||||
new := &profile.EndpointPermission{
|
|
||||||
Type: profile.EptDomain,
|
|
||||||
Value: comm.Domain,
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-domain-all":
|
|
||||||
new.Value = "." + new.Value
|
|
||||||
case "permit-domain-distinct":
|
|
||||||
// everything already set
|
|
||||||
case "permit-ip", "permit-ip-incoming":
|
|
||||||
if pkt.Info().Version == packet.IPv4 {
|
|
||||||
new.Type = profile.EptIPv4
|
|
||||||
} else {
|
|
||||||
new.Type = profile.EptIPv6
|
|
||||||
}
|
|
||||||
new.Value = remoteIP.String()
|
|
||||||
default:
|
|
||||||
// deny
|
|
||||||
new.Permit = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if new.Permit {
|
|
||||||
log.Infof("firewall: user permitted link %s -> %s", comm.Process(), new.Value)
|
|
||||||
link.Accept("permitted by user")
|
|
||||||
} else {
|
|
||||||
log.Infof("firewall: user denied link %s -> %s", comm.Process(), new.Value)
|
|
||||||
link.Deny("denied by user")
|
|
||||||
}
|
|
||||||
|
|
||||||
profileSet.Lock()
|
|
||||||
defer profileSet.Unlock()
|
|
||||||
userProfile := profileSet.UserProfile()
|
|
||||||
userProfile.Lock()
|
|
||||||
defer userProfile.Unlock()
|
|
||||||
|
|
||||||
if promptResponse == "permit-ip-incoming" {
|
|
||||||
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new)
|
|
||||||
} else {
|
|
||||||
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
|
||||||
}
|
|
||||||
go userProfile.Save("")
|
|
||||||
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
n.Cancel()
|
|
||||||
link.Deny("user did not respond to prompt")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
|
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
|
||||||
|
|||||||
194
firewall/prompt.go
Normal file
194
firewall/prompt.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
|
"github.com/safing/portmaster/network"
|
||||||
|
"github.com/safing/portmaster/network/packet"
|
||||||
|
"github.com/safing/portmaster/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// notification action IDs
|
||||||
|
permitDomainAll = "permit-domain-all"
|
||||||
|
permitDomainDistinct = "permit-domain-distinct"
|
||||||
|
denyDomainAll = "deny-domain-all"
|
||||||
|
denyDomainDistinct = "deny-domain-distinct"
|
||||||
|
|
||||||
|
permitIP = "permit-ip"
|
||||||
|
denyIP = "deny-ip"
|
||||||
|
permitServingIP = "permit-serving-ip"
|
||||||
|
denyServingIP = "deny-serving-ip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, fqdn string) {
|
||||||
|
nTTL := time.Duration(promptTimeout()) * time.Second
|
||||||
|
|
||||||
|
// first check if there is an existing notification for this.
|
||||||
|
// build notification ID
|
||||||
|
var nID string
|
||||||
|
switch {
|
||||||
|
case comm.Direction, fqdn == "": // connection to/from IP
|
||||||
|
if pkt == nil {
|
||||||
|
log.Error("firewall: could not prompt for incoming/direct connection: missing pkt")
|
||||||
|
if link != nil {
|
||||||
|
link.Deny("internal error")
|
||||||
|
} else {
|
||||||
|
comm.Deny("internal error")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Domain, pkt.Info().RemoteIP)
|
||||||
|
default: // connection to domain
|
||||||
|
nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain)
|
||||||
|
}
|
||||||
|
n := notifications.Get(nID)
|
||||||
|
saveResponse := true
|
||||||
|
|
||||||
|
if n != nil {
|
||||||
|
// update with new expiry
|
||||||
|
n.Update(time.Now().Add(nTTL).Unix())
|
||||||
|
// do not save response to profile
|
||||||
|
saveResponse = false
|
||||||
|
} else {
|
||||||
|
// create new notification
|
||||||
|
n = (¬ifications.Notification{
|
||||||
|
ID: nID,
|
||||||
|
Type: notifications.Prompt,
|
||||||
|
Expires: time.Now().Add(nTTL).Unix(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// add message and actions
|
||||||
|
switch {
|
||||||
|
case comm.Direction: // incoming
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().LocalPort())
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitServingIP,
|
||||||
|
Text: "Permit",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: denyServingIP,
|
||||||
|
Text: "Deny",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case fqdn == "": // direct connection
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to connect to %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitIP,
|
||||||
|
Text: "Permit",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: denyIP,
|
||||||
|
Text: "Deny",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default: // connection to domain
|
||||||
|
if pkt != nil {
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
|
||||||
|
} else {
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain)
|
||||||
|
}
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitDomainAll,
|
||||||
|
Text: "Permit all",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitDomainDistinct,
|
||||||
|
Text: "Permit",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: denyDomainDistinct,
|
||||||
|
Text: "Deny",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// save new notification
|
||||||
|
n.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for response/timeout
|
||||||
|
select {
|
||||||
|
case promptResponse := <-n.Response():
|
||||||
|
switch promptResponse {
|
||||||
|
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
|
||||||
|
if link != nil {
|
||||||
|
link.Accept("permitted by user")
|
||||||
|
} else {
|
||||||
|
comm.Accept("permitted by user")
|
||||||
|
}
|
||||||
|
default: // deny
|
||||||
|
if link != nil {
|
||||||
|
link.Accept("denied by user")
|
||||||
|
} else {
|
||||||
|
comm.Accept("denied by user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// end here if we won't save the response to the profile
|
||||||
|
if !saveResponse {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
new := &profile.EndpointPermission{
|
||||||
|
Type: profile.EptDomain,
|
||||||
|
Value: comm.Domain,
|
||||||
|
Permit: false,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// permission type
|
||||||
|
switch promptResponse {
|
||||||
|
case permitDomainAll, denyDomainAll:
|
||||||
|
new.Value = "." + new.Value
|
||||||
|
case permitIP, permitServingIP, denyIP, denyServingIP:
|
||||||
|
if pkt == nil {
|
||||||
|
log.Warningf("firewall: received invalid prompt response: %s for %s", promptResponse, comm.Domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pkt.Info().Version == packet.IPv4 {
|
||||||
|
new.Type = profile.EptIPv4
|
||||||
|
} else {
|
||||||
|
new.Type = profile.EptIPv6
|
||||||
|
}
|
||||||
|
new.Value = pkt.Info().RemoteIP().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// permission verdict
|
||||||
|
switch promptResponse {
|
||||||
|
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
|
||||||
|
new.Permit = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user profile
|
||||||
|
profileSet := comm.Process().ProfileSet()
|
||||||
|
profileSet.Lock()
|
||||||
|
defer profileSet.Unlock()
|
||||||
|
userProfile := profileSet.UserProfile()
|
||||||
|
userProfile.Lock()
|
||||||
|
defer userProfile.Unlock()
|
||||||
|
|
||||||
|
// add to correct list
|
||||||
|
switch promptResponse {
|
||||||
|
case permitServingIP, denyServingIP:
|
||||||
|
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new)
|
||||||
|
default:
|
||||||
|
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save!
|
||||||
|
go userProfile.Save("")
|
||||||
|
|
||||||
|
case <-n.Expired():
|
||||||
|
if link != nil {
|
||||||
|
link.Accept("no response to prompt")
|
||||||
|
} else {
|
||||||
|
comm.Accept("no response to prompt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Package intel is responsible for fetching intelligence data, including DNS, on remote entities.
|
Package intel is responsible for fetching intelligence data, including DNS, on remote entities.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
// DISABLE TESTING FOR NOW: find a way to have tests with the module system
|
// DISABLE TESTING FOR NOW: find a way to have tests with the module system
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|||||||
33
main.go
33
main.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -21,17 +22,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
printStackOnExit bool
|
printStackOnExit bool
|
||||||
|
enableInputSignals bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
|
flag.BoolVar(&printStackOnExit, "print-stack-on-exit", false, "prints the stack before of shutting down")
|
||||||
|
flag.BoolVar(&enableInputSignals, "input-signals", false, "emulate signals using stdin")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
// Set Info
|
// Set Info
|
||||||
info.Set("Portmaster", "0.3.1", "AGPLv3", true)
|
info.Set("Portmaster", "0.3.8", "AGPLv3", true)
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
err := modules.Start()
|
err := modules.Start()
|
||||||
@@ -47,6 +49,9 @@ func main() {
|
|||||||
// Shutdown
|
// Shutdown
|
||||||
// catch interrupt for clean shutdown
|
// catch interrupt for clean shutdown
|
||||||
signalCh := make(chan os.Signal)
|
signalCh := make(chan os.Signal)
|
||||||
|
if enableInputSignals {
|
||||||
|
go inputSignals(signalCh)
|
||||||
|
}
|
||||||
signal.Notify(
|
signal.Notify(
|
||||||
signalCh,
|
signalCh,
|
||||||
os.Interrupt,
|
os.Interrupt,
|
||||||
@@ -82,9 +87,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
fmt.Println("===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====")
|
fmt.Fprintln(os.Stderr, "===== TAKING TOO LONG FOR SHUTDOWN - PRINTING STACK TRACES =====")
|
||||||
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -99,3 +104,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inputSignals(signalCh chan os.Signal) {
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
switch scanner.Text() {
|
||||||
|
case "SIGHUP":
|
||||||
|
signalCh <- syscall.SIGHUP
|
||||||
|
case "SIGINT":
|
||||||
|
signalCh <- syscall.SIGINT
|
||||||
|
case "SIGQUIT":
|
||||||
|
signalCh <- syscall.SIGQUIT
|
||||||
|
case "SIGTERM":
|
||||||
|
signalCh <- syscall.SIGTERM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package nameserver
|
package nameserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -29,7 +27,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
modules.Register("nameserver", prep, start, nil, "intel")
|
modules.Register("nameserver", prep, start, nil, "core", "intel")
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
listenAddress = "0.0.0.0:53"
|
listenAddress = "0.0.0.0:53"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
modules.Register("nameserver", prep, start, nil, "intel")
|
modules.Register("nameserver", prep, start, nil, "core", "intel")
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func checkForConflictingService(err error) {
|
|||||||
(¬ifications.Notification{
|
(¬ifications.Notification{
|
||||||
ID: "nameserver-stopped-conflicting-service",
|
ID: "nameserver-stopped-conflicting-service",
|
||||||
Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
|
Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
|
||||||
}).Init().Save()
|
}).Save()
|
||||||
|
|
||||||
// wait for a short duration for the other service to shut down
|
// wait for a short duration for the other service to shut down
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !server
|
// +build !server
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !linux
|
// +build !linux
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build root
|
// +build root
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
modules.Register("network", nil, start, nil, "database")
|
modules.Register("network", nil, start, nil, "core")
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package netutils
|
package netutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package netutils
|
package netutils
|
||||||
|
|
||||||
import "net"
|
import "net"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package netutils
|
package netutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package packet
|
package packet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package network
|
package network
|
||||||
|
|
||||||
// Verdict describes the decision made about a connection or link.
|
// Verdict describes the decision made about a connection or link.
|
||||||
|
|||||||
24
pmctl/build
24
pmctl/build
@@ -43,10 +43,32 @@ if [[ "$BUILD_SOURCE" == "" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# build tools
|
||||||
|
EXTRA_LD_FLAGS=""
|
||||||
|
if [[ $GOOS == "windows" ]]; then
|
||||||
|
# checks
|
||||||
|
if [[ $CC_FOR_windows_amd64 == "" ]]; then
|
||||||
|
echo "ENV variable CC_FOR_windows_amd64 (c compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ $CXX_FOR_windows_amd64 == "" ]]; then
|
||||||
|
echo "ENV variable CXX_FOR_windows_amd64 (c++ compiler) is not set. Please set it to the cross compiler you want to use for compiling for windows_amd64"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# compilers
|
||||||
|
export CC=$CC_FOR_windows_amd64
|
||||||
|
export CXX=$CXX_FOR_windows_amd64
|
||||||
|
# custom
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
EXTRA_LD_FLAGS='-H windowsgui' # Hide console window by default (but we attach to parent console if available)
|
||||||
|
# generate resource.syso for windows metadata / icon
|
||||||
|
go generate
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Please notice, that this build script includes metadata into the build."
|
echo "Please notice, that this build script includes metadata into the build."
|
||||||
echo "This information is useful for debugging and license compliance."
|
echo "This information is useful for debugging and license compliance."
|
||||||
echo "Run the compiled binary with the -version flag to see the information included."
|
echo "Run the compiled binary with the -version flag to see the information included."
|
||||||
|
|
||||||
# build
|
# build
|
||||||
BUILD_PATH="github.com/safing/portbase/info"
|
BUILD_PATH="github.com/safing/portbase/info"
|
||||||
go build -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*
|
go build -ldflags "$EXTRA_LD_FLAGS -X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $*
|
||||||
|
|||||||
12
pmctl/console_default.go
Normal file
12
pmctl/console_default.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "os/exec"
|
||||||
|
|
||||||
|
func attachToParentConsole() (attached bool, err error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideWindow(cmd *exec.Cmd) {
|
||||||
|
}
|
||||||
150
pmctl/console_windows.go
Normal file
150
pmctl/console_windows.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Parts of this file are FORKED
|
||||||
|
// from https://github.com/apenwarr/fixconsole/blob/35b2e7d921eb80a71a5f04f166ff0a1405bddf79/fixconsole_windows.go
|
||||||
|
// on 16.07.2019
|
||||||
|
// with Apache-2.0 license
|
||||||
|
// authored by https://github.com/apenwarr
|
||||||
|
|
||||||
|
// docs/sources:
|
||||||
|
// Stackoverflow Question: https://stackoverflow.com/questions/23743217/printing-output-to-a-command-window-when-golang-application-is-compiled-with-ld
|
||||||
|
// MS AttachConsole: https://docs.microsoft.com/en-us/windows/console/attachconsole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
windowsAttachParentProcess = ^uintptr(0) // (DWORD)-1
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procAttachConsole = kernel32.NewProc("AttachConsole")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Windows console output is a mess.
|
||||||
|
//
|
||||||
|
// If you compile as "-H windows", then if you launch your program without
|
||||||
|
// a console, Windows forcibly creates one to use as your stdin/stdout, which
|
||||||
|
// is silly for a GUI app, so we can't do that.
|
||||||
|
//
|
||||||
|
// If you compile as "-H windowsgui", then it doesn't create a console for
|
||||||
|
// your app... but also doesn't provide a working stdin/stdout/stderr even if
|
||||||
|
// you *did* launch from the console. However, you can use AttachConsole()
|
||||||
|
// to get a handle to your parent process's console, if any, and then
|
||||||
|
// os.NewFile() to turn that handle into a fd usable as stdout/stderr.
|
||||||
|
//
|
||||||
|
// However, then you have the problem that if you redirect stdout or stderr
|
||||||
|
// from the shell, you end up ignoring the redirection by forcing it to the
|
||||||
|
// console.
|
||||||
|
//
|
||||||
|
// To fix *that*, we have to detect whether there was a pre-existing stdout
|
||||||
|
// or not. We can check GetStdHandle(), which returns 0 for "should be
|
||||||
|
// console" and nonzero for "already pointing at a file."
|
||||||
|
//
|
||||||
|
// Be careful though! As soon as you run AttachConsole(), it resets *all*
|
||||||
|
// the GetStdHandle() handles to point them at the console instead, thus
|
||||||
|
// throwing away the original file redirects. So we have to GetStdHandle()
|
||||||
|
// *before* AttachConsole().
|
||||||
|
//
|
||||||
|
// For some reason, powershell redirections provide a valid file handle, but
|
||||||
|
// writing to that handle doesn't write to the file. I haven't found a way
|
||||||
|
// to work around that. (Windows 10.0.17763.379)
|
||||||
|
//
|
||||||
|
// Net result is as follows.
|
||||||
|
// Before:
|
||||||
|
// SHELL NON-REDIRECTED REDIRECTED
|
||||||
|
// explorer.exe no console n/a
|
||||||
|
// cmd.exe broken works
|
||||||
|
// powershell broken broken
|
||||||
|
// WSL bash broken works
|
||||||
|
// After
|
||||||
|
// SHELL NON-REDIRECTED REDIRECTED
|
||||||
|
// explorer.exe no console n/a
|
||||||
|
// cmd.exe works works
|
||||||
|
// powershell works broken
|
||||||
|
// WSL bash works works
|
||||||
|
//
|
||||||
|
// We don't seem to make anything worse, at least.
|
||||||
|
func attachToParentConsole() (attached bool, err error) {
|
||||||
|
// get std handles before we attempt to attach to parent console
|
||||||
|
stdin, _ := syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
|
||||||
|
stdout, _ := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||||
|
stderr, _ := syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
|
||||||
|
|
||||||
|
// attempt to attach to parent console
|
||||||
|
err = procAttachConsole.Find()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
r1, _, err := procAttachConsole.Call(windowsAttachParentProcess)
|
||||||
|
if r1 == 0 {
|
||||||
|
// possible errors:
|
||||||
|
// ERROR_ACCESS_DENIED: already attached to console
|
||||||
|
// ERROR_INVALID_HANDLE: process does not have console
|
||||||
|
// ERROR_INVALID_PARAMETER: process does not exist
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get std handles after we attached to console
|
||||||
|
var invalid syscall.Handle
|
||||||
|
con := invalid
|
||||||
|
|
||||||
|
if stdin == invalid {
|
||||||
|
stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE)
|
||||||
|
}
|
||||||
|
if stdout == invalid {
|
||||||
|
stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
|
||||||
|
con = stdout
|
||||||
|
}
|
||||||
|
if stderr == invalid {
|
||||||
|
stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE)
|
||||||
|
con = stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
// correct output mode
|
||||||
|
if con != invalid {
|
||||||
|
// Make sure the console is configured to convert
|
||||||
|
// \n to \r\n, like Go programs expect.
|
||||||
|
h := windows.Handle(con)
|
||||||
|
var st uint32
|
||||||
|
err := windows.GetConsoleMode(h, &st)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to get console mode: %s\n", err)
|
||||||
|
} else {
|
||||||
|
err = windows.SetConsoleMode(h, st&^windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to set console mode: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix std handles to correct values (ie. redirects)
|
||||||
|
if stdin != invalid {
|
||||||
|
os.Stdin = os.NewFile(uintptr(stdin), "stdin")
|
||||||
|
log.Printf("fixed os.Stdin after attaching to parent console\n")
|
||||||
|
}
|
||||||
|
if stdout != invalid {
|
||||||
|
os.Stdout = os.NewFile(uintptr(stdout), "stdout")
|
||||||
|
log.Printf("fixed os.Stdout after attaching to parent console\n")
|
||||||
|
}
|
||||||
|
if stderr != invalid {
|
||||||
|
os.Stderr = os.NewFile(uintptr(stderr), "stderr")
|
||||||
|
log.Printf("fixed os.Stderr after attaching to parent console\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("attached to parent console")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideWindow(cmd *exec.Cmd) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portmaster/updates"
|
"github.com/safing/portmaster/updates"
|
||||||
@@ -22,7 +22,7 @@ func getFile(opts *Options) (*updates.File, error) {
|
|||||||
|
|
||||||
// download
|
// download
|
||||||
if opts.AllowDownload {
|
if opts.AllowDownload {
|
||||||
fmt.Printf("%s downloading %s...\n", logPrefix, opts.Identifier)
|
log.Printf("downloading %s...\n", opts.Identifier)
|
||||||
|
|
||||||
// download indexes
|
// download indexes
|
||||||
err = updates.UpdateIndexes()
|
err = updates.UpdateIndexes()
|
||||||
@@ -39,7 +39,7 @@ func getFile(opts *Options) (*updates.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait for 30 seconds
|
// wait for 30 seconds
|
||||||
fmt.Printf("%s waiting for download of %s (by Portmaster Core) to complete...\n", logPrefix, opts.Identifier)
|
log.Printf("waiting for download of %s (by Portmaster Core) to complete...\n", opts.Identifier)
|
||||||
|
|
||||||
// try every 0.5 secs
|
// try every 0.5 secs
|
||||||
for tries := 0; tries < 60; tries++ {
|
for tries := 0; tries < 60; tries++ {
|
||||||
|
|||||||
209
pmctl/install_windows.go
Normal file
209
pmctl/install_windows.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Based on the offical Go examples from
|
||||||
|
// https://github.com/golang/sys/blob/master/windows/svc/example
|
||||||
|
// by The Go Authors.
|
||||||
|
// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"golang.org/x/sys/windows/svc/mgr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(installCmd)
|
||||||
|
installCmd.AddCommand(installService)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(uninstallCmd)
|
||||||
|
uninstallCmd.AddCommand(uninstallService)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installCmd = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "Install system integrations",
|
||||||
|
}
|
||||||
|
|
||||||
|
var uninstallCmd = &cobra.Command{
|
||||||
|
Use: "uninstall",
|
||||||
|
Short: "Uninstall system integrations",
|
||||||
|
}
|
||||||
|
|
||||||
|
var installService = &cobra.Command{
|
||||||
|
Use: "core-service",
|
||||||
|
Short: "Install Portmaster Core Windows Service",
|
||||||
|
RunE: installWindowsService,
|
||||||
|
}
|
||||||
|
|
||||||
|
var uninstallService = &cobra.Command{
|
||||||
|
Use: "core-service",
|
||||||
|
Short: "Uninstall Portmaster Core Windows Service",
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// non-nil dummy to override db flag requirement
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RunE: uninstallWindowsService,
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExePath() (string, error) {
|
||||||
|
// get own filepath
|
||||||
|
prog := os.Args[0]
|
||||||
|
p, err := filepath.Abs(prog)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// check if the path is valid
|
||||||
|
fi, err := os.Stat(p)
|
||||||
|
if err == nil {
|
||||||
|
if !fi.Mode().IsDir() {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("%s is directory", p)
|
||||||
|
}
|
||||||
|
// check if we have a .exe extension, add and check if not
|
||||||
|
if filepath.Ext(p) == "" {
|
||||||
|
p += ".exe"
|
||||||
|
fi, err := os.Stat(p)
|
||||||
|
if err == nil {
|
||||||
|
if !fi.Mode().IsDir() {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("%s is directory", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceExecCommand(exePath string, escape bool) []string {
|
||||||
|
return []string{
|
||||||
|
maybeEscape(exePath, escape),
|
||||||
|
"run",
|
||||||
|
"core-service",
|
||||||
|
"--db",
|
||||||
|
maybeEscape(dataRoot.Path, escape),
|
||||||
|
"--input-signals",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeEscape(s string, escape bool) string {
|
||||||
|
if escape {
|
||||||
|
return windows.EscapeArg(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceConfig(exePath string) mgr.Config {
|
||||||
|
return mgr.Config{
|
||||||
|
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
|
||||||
|
StartType: mgr.StartAutomatic,
|
||||||
|
ErrorControl: mgr.ErrorNormal,
|
||||||
|
BinaryPathName: strings.Join(getServiceExecCommand(exePath, true), " "),
|
||||||
|
DisplayName: "Portmaster Core",
|
||||||
|
Description: "Portmaster Application Firewall - Core Service",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRecoveryActions() (recoveryActions []mgr.RecoveryAction, resetPeriod uint32) {
|
||||||
|
return []mgr.RecoveryAction{
|
||||||
|
//mgr.RecoveryAction{
|
||||||
|
// Type: mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand
|
||||||
|
// Delay: 1 * time.Minute, // the time to wait before performing the specified action
|
||||||
|
//},
|
||||||
|
// mgr.RecoveryAction{
|
||||||
|
// Type: mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand
|
||||||
|
// Delay: 1 * time.Minute, // the time to wait before performing the specified action
|
||||||
|
// },
|
||||||
|
mgr.RecoveryAction{
|
||||||
|
Type: mgr.ServiceRestart, // one of NoAction, ComputerReboot, ServiceRestart or RunCommand
|
||||||
|
Delay: 1 * time.Minute, // the time to wait before performing the specified action
|
||||||
|
},
|
||||||
|
}, 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
func installWindowsService(cmd *cobra.Command, args []string) error {
|
||||||
|
// get exe path
|
||||||
|
exePath, err := getExePath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get exe path: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to Windows service manager
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to service manager: %s", err)
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
// open service
|
||||||
|
created := false
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
// create service
|
||||||
|
cmd := getServiceExecCommand(exePath, false)
|
||||||
|
s, err = m.CreateService(serviceName, cmd[0], getServiceConfig(exePath), cmd[1:]...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create service: %s", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
created = true
|
||||||
|
} else {
|
||||||
|
// update service
|
||||||
|
s.UpdateConfig(getServiceConfig(exePath))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update service: %s", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// update recovery actions
|
||||||
|
err = s.SetRecoveryActions(getRecoveryActions())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update recovery actions: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if created {
|
||||||
|
log.Printf("created service %s\n", serviceName)
|
||||||
|
} else {
|
||||||
|
log.Printf("updated service %s\n", serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstallWindowsService(cmd *cobra.Command, args []string) error {
|
||||||
|
// connect to Windows service manager
|
||||||
|
m, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer m.Disconnect()
|
||||||
|
|
||||||
|
// open service
|
||||||
|
s, err := m.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %s is not installed", serviceName)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
_, err = s.Control(svc.Stop)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to stop service: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete service
|
||||||
|
err = s.Delete()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete service: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("uninstalled service %s\n", serviceName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
70
pmctl/lock.go
Normal file
70
pmctl/lock.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
processInfo "github.com/shirou/gopsutil/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkAndCreateInstanceLock(name string) (pid int32, err error) {
|
||||||
|
lockFilePath := filepath.Join(dataRoot.Path, fmt.Sprintf("%s-lock.pid", name))
|
||||||
|
|
||||||
|
// read current pid file
|
||||||
|
data, err := ioutil.ReadFile(lockFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// create new lock
|
||||||
|
return 0, createInstanceLock(lockFilePath)
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// file exists!
|
||||||
|
parsedPid, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to parse existing lock pid file (ignoring): %s\n", err)
|
||||||
|
return 0, createInstanceLock(lockFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if process exists
|
||||||
|
p, err := processInfo.NewProcess(int32(parsedPid))
|
||||||
|
if err == nil {
|
||||||
|
// TODO: remove this workaround as soon as NewProcess really returns an error on windows when the process does not exist
|
||||||
|
// Issue: https://github.com/shirou/gopsutil/issues/729
|
||||||
|
_, err = p.Name()
|
||||||
|
if err == nil {
|
||||||
|
// process exists
|
||||||
|
return p.Pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// else create new lock
|
||||||
|
return 0, createInstanceLock(lockFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createInstanceLock(lockFilePath string) error {
|
||||||
|
// check data root dir
|
||||||
|
err := dataRoot.Ensure()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to check data root dir: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create lock file
|
||||||
|
err = ioutil.WriteFile(lockFilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteInstanceLock(name string) error {
|
||||||
|
lockFilePath := filepath.Join(dataRoot.Path, fmt.Sprintf("%s-lock.pid", name))
|
||||||
|
return os.Remove(lockFilePath)
|
||||||
|
}
|
||||||
142
pmctl/logs.go
Normal file
142
pmctl/logs.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/container"
|
||||||
|
"github.com/safing/portbase/database/record"
|
||||||
|
"github.com/safing/portbase/formats/dsd"
|
||||||
|
"github.com/safing/portbase/info"
|
||||||
|
"github.com/safing/portmaster/updates"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeLogFile(logFilePath string, identifier string, updateFile *updates.File) *os.File {
|
||||||
|
logFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE, 0444)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to create log file %s: %s\n", logFilePath, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create header, so that the portmaster can view log files as a database
|
||||||
|
meta := record.Meta{}
|
||||||
|
meta.Update()
|
||||||
|
meta.SetAbsoluteExpiry(time.Now().Add(720 * time.Hour).Unix()) // one month
|
||||||
|
|
||||||
|
// manually marshal
|
||||||
|
// version
|
||||||
|
c := container.New([]byte{1})
|
||||||
|
// meta
|
||||||
|
metaSection, err := dsd.Dump(meta, dsd.JSON)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to serialize header for log file %s: %s\n", logFilePath, err)
|
||||||
|
finalizeLogFile(logFile, logFilePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.AppendAsBlock(metaSection)
|
||||||
|
// log file data type (string) and newline for better manual viewing
|
||||||
|
c.Append([]byte("S\n"))
|
||||||
|
c.Append([]byte(fmt.Sprintf("executing %s version %s on %s %s\n", identifier, updateFile.Version(), runtime.GOOS, runtime.GOARCH)))
|
||||||
|
|
||||||
|
_, err = logFile.Write(c.CompileData())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to write header for log file %s: %s\n", logFilePath, err)
|
||||||
|
finalizeLogFile(logFile, logFilePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return logFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeLogFile(logFile *os.File, logFilePath string) {
|
||||||
|
err := logFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to close log file %s: %s\n", logFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check file size
|
||||||
|
stat, err := os.Stat(logFilePath)
|
||||||
|
if err == nil {
|
||||||
|
// delete if file is smaller than
|
||||||
|
if stat.Size() < 200 { // header + info is about 150 bytes
|
||||||
|
err := os.Remove(logFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to delete empty log file %s: %s\n", logFilePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initControlLogFile() *os.File {
|
||||||
|
// check logging dir
|
||||||
|
logFileBasePath := filepath.Join(logsRoot.Path, "fstree", "control")
|
||||||
|
err := logsRoot.EnsureAbsPath(logFileBasePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open log file
|
||||||
|
logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.log", time.Now().UTC().Format("2006-02-01-15-04-05")))
|
||||||
|
return initializeLogFile(logFilePath, "control/portmaster-control", updates.NewFile("", info.Version(), false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func logControlError(cErr error) {
|
||||||
|
// check if error present
|
||||||
|
if cErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check logging dir
|
||||||
|
logFileBasePath := filepath.Join(logsRoot.Path, "fstree", "control")
|
||||||
|
err := logsRoot.EnsureAbsPath(logFileBasePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open log file
|
||||||
|
logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.error.log", time.Now().UTC().Format("2006-02-01-15-04-05")))
|
||||||
|
errorFile := initializeLogFile(logFilePath, "control/portmaster-control", updates.NewFile("", info.Version(), false))
|
||||||
|
if errorFile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write error and close
|
||||||
|
fmt.Fprintln(errorFile, cErr.Error())
|
||||||
|
errorFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func logControlStack() {
|
||||||
|
// check logging dir
|
||||||
|
logFileBasePath := filepath.Join(logsRoot.Path, "fstree", "control")
|
||||||
|
err := logsRoot.EnsureAbsPath(logFileBasePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to check/create log file folder %s: %s\n", logFileBasePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open log file
|
||||||
|
logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.stack.log", time.Now().UTC().Format("2006-02-01-15-04-05")))
|
||||||
|
errorFile := initializeLogFile(logFilePath, "control/portmaster-control", updates.NewFile("", info.Version(), false))
|
||||||
|
if errorFile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write error and close
|
||||||
|
pprof.Lookup("goroutine").WriteTo(errorFile, 1)
|
||||||
|
errorFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAndLogControlError(wrappedFunc func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
err := wrappedFunc(cmd, args)
|
||||||
|
if err != nil {
|
||||||
|
logControlError(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
194
pmctl/main.go
194
pmctl/main.go
@@ -2,51 +2,66 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/core/structure"
|
||||||
|
"github.com/safing/portmaster/updates"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/utils"
|
||||||
|
|
||||||
"github.com/safing/portbase/info"
|
"github.com/safing/portbase/info"
|
||||||
"github.com/safing/portbase/log"
|
portlog "github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/updates"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
logPrefix = "[control]"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
updateStoragePath string
|
dataDir string
|
||||||
databaseRootDir *string
|
databaseDir string
|
||||||
|
dataRoot *utils.DirStructure
|
||||||
|
logsRoot *utils.DirStructure
|
||||||
|
|
||||||
|
showShortVersion bool
|
||||||
|
showFullVersion bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "portmaster-control",
|
Use: "portmaster-control",
|
||||||
Short: "contoller for all portmaster components",
|
Short: "Controller for all portmaster components",
|
||||||
PersistentPreRunE: initPmCtl,
|
PersistentPreRunE: cmdSetup,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if showShortVersion {
|
||||||
|
fmt.Println(info.Version())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if showFullVersion {
|
||||||
|
fmt.Println(info.FullVersion())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return cmd.Help()
|
return cmd.Help()
|
||||||
},
|
},
|
||||||
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
databaseRootDir = rootCmd.PersistentFlags().String("db", "", "set database directory")
|
// Let cobra ignore if we are running as "GUI" or not
|
||||||
err := rootCmd.MarkPersistentFlagRequired("db")
|
cobra.MousetrapHelpText = ""
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
rootCmd.PersistentFlags().StringVar(&dataDir, "data", "", "set data directory")
|
||||||
}
|
rootCmd.PersistentFlags().StringVar(&databaseDir, "db", "", "alias to --data (deprecated)")
|
||||||
|
rootCmd.MarkPersistentFlagDirname("data")
|
||||||
|
rootCmd.MarkPersistentFlagDirname("db")
|
||||||
|
rootCmd.Flags().BoolVar(&showFullVersion, "version", false, "print version")
|
||||||
|
rootCmd.Flags().BoolVar(&showShortVersion, "ver", false, "print version number only")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
// set meta info
|
||||||
|
info.Set("Portmaster Control", "0.2.11", "AGPLv3", true)
|
||||||
// not using portbase logger
|
|
||||||
log.SetLogLevel(log.CriticalLevel)
|
|
||||||
|
|
||||||
// for debugging
|
// for debugging
|
||||||
// log.Start()
|
// log.Start()
|
||||||
@@ -57,66 +72,105 @@ func main() {
|
|||||||
// os.Exit(1)
|
// os.Exit(1)
|
||||||
// }()
|
// }()
|
||||||
|
|
||||||
// set meta info
|
// catch interrupt for clean shutdown
|
||||||
info.Set("Portmaster Control", "0.2.1", "AGPLv3", true)
|
signalCh := make(chan os.Signal)
|
||||||
|
signal.Notify(
|
||||||
// check if meta info is ok
|
signalCh,
|
||||||
err := info.CheckVersion()
|
os.Interrupt,
|
||||||
if err != nil {
|
os.Kill,
|
||||||
fmt.Printf("%s compile error: please compile using the provided build script\n", logPrefix)
|
syscall.SIGHUP,
|
||||||
os.Exit(1)
|
syscall.SIGINT,
|
||||||
}
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT,
|
||||||
// react to version flag
|
)
|
||||||
if info.PrintVersion() {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start root command
|
// start root command
|
||||||
if err := rootCmd.Execute(); err != nil {
|
go func() {
|
||||||
os.Exit(1)
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// for debugging windows service (no stdout/err)
|
||||||
|
// go func() {
|
||||||
|
// time.Sleep(10 * time.Second)
|
||||||
|
// // initiateShutdown(nil)
|
||||||
|
// // logControlStack()
|
||||||
|
// }()
|
||||||
|
|
||||||
|
// wait for signals
|
||||||
|
for sig := range signalCh {
|
||||||
|
if childIsRunning.IsSet() {
|
||||||
|
log.Printf("got %s signal (ignoring), waiting for child to exit...\n", sig)
|
||||||
|
} else {
|
||||||
|
log.Printf("got %s signal, exiting... (not executing anything)\n", sig)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initPmCtl(cmd *cobra.Command, args []string) error {
|
func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
// check if we are running in a console (try to attach to parent console if available)
|
||||||
// transform from db base path to updates path
|
runningInConsole, err = attachToParentConsole()
|
||||||
if *databaseRootDir != "" {
|
|
||||||
updates.SetDatabaseRoot(*databaseRootDir)
|
|
||||||
updateStoragePath = filepath.Join(*databaseRootDir, "updates")
|
|
||||||
} else {
|
|
||||||
return errors.New("please supply the database directory using the --db flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we are root/admin for self upgrade
|
|
||||||
userInfo, err := user.Current()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
log.Printf("failed to attach to parent console: %s\n", err)
|
||||||
}
|
os.Exit(1)
|
||||||
switch runtime.GOOS {
|
|
||||||
case "linux":
|
|
||||||
if userInfo.Username != "root" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case "windows":
|
|
||||||
if !strings.HasSuffix(userInfo.Username, "SYSTEM") { // is this correct?
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = removeOldBin()
|
// check if meta info is ok
|
||||||
|
err = info.CheckVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%s warning: failed to remove old upgrade: %s\n", logPrefix, err)
|
fmt.Println("compile error: please compile using the provided build script")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
update := checkForUpgrade()
|
// set up logging
|
||||||
if update != nil {
|
log.SetFlags(log.Ldate | log.Ltime | log.LUTC)
|
||||||
err = doSelfUpgrade(update)
|
log.SetPrefix("[control] ")
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
// not using portbase logger
|
||||||
|
portlog.SetLogLevel(portlog.CriticalLevel)
|
||||||
|
|
||||||
|
// data directory
|
||||||
|
if !showShortVersion && !showFullVersion {
|
||||||
|
// set data root
|
||||||
|
// backwards compatibility
|
||||||
|
if dataDir == "" {
|
||||||
|
dataDir = databaseDir
|
||||||
|
}
|
||||||
|
// check data dir
|
||||||
|
if dataDir == "" {
|
||||||
|
return errors.New("please set the data directory using --data=/path/to/data/dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove redundant escape characters and quotes
|
||||||
|
dataDir = strings.Trim(dataDir, `\"`)
|
||||||
|
// initialize structure
|
||||||
|
err = structure.Initialize(dataDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s failed to upgrade self: %s", logPrefix, err)
|
return fmt.Errorf("failed to initialize data root: %s", err)
|
||||||
|
}
|
||||||
|
dataRoot = structure.Root()
|
||||||
|
// manually set updates root (no modules)
|
||||||
|
updates.SetDataRoot(structure.Root())
|
||||||
|
}
|
||||||
|
|
||||||
|
// logs and warning
|
||||||
|
if !showShortVersion && !showFullVersion && !strings.Contains(cmd.CommandPath(), " show ") {
|
||||||
|
// set up logs root
|
||||||
|
logsRoot = structure.NewRootDir("logs", 0777)
|
||||||
|
err = logsRoot.Ensure()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize logs root: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn about CTRL-C on windows
|
||||||
|
if runningInConsole && onWindows {
|
||||||
|
log.Println("WARNING: portmaster-control is marked as a GUI application in order to get rid of the console window.")
|
||||||
|
log.Println("WARNING: CTRL-C will immediately kill without clean shutdown.")
|
||||||
}
|
}
|
||||||
fmt.Println("upgraded portmaster-control")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
413
pmctl/run.go
413
pmctl/run.go
@@ -3,20 +3,35 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tevino/abool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
runningInConsole bool
|
||||||
|
onWindows = runtime.GOOS == "windows"
|
||||||
|
|
||||||
|
childIsRunning = abool.NewBool(false)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options for starting component
|
// Options for starting component
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Identifier string
|
Identifier string // component identifier
|
||||||
AllowDownload bool
|
ShortIdentifier string // populated automatically
|
||||||
|
SuppressArgs bool // do not use any args
|
||||||
|
AllowDownload bool // allow download of component if it is not yet available
|
||||||
|
AllowHidingWindow bool // allow hiding the window of the subprocess
|
||||||
|
NoOutput bool // do not use stdout/err if logging to file is available (did not fail to open log file)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -35,9 +50,10 @@ var runCore = &cobra.Command{
|
|||||||
Use: "core",
|
Use: "core",
|
||||||
Short: "Run the Portmaster Core",
|
Short: "Run the Portmaster Core",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return run(cmd, &Options{
|
return handleRun(cmd, &Options{
|
||||||
Identifier: "core/portmaster-core",
|
Identifier: "core/portmaster-core",
|
||||||
AllowDownload: true,
|
AllowDownload: true,
|
||||||
|
AllowHidingWindow: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
@@ -50,9 +66,10 @@ var runApp = &cobra.Command{
|
|||||||
Use: "app",
|
Use: "app",
|
||||||
Short: "Run the Portmaster App",
|
Short: "Run the Portmaster App",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return run(cmd, &Options{
|
return handleRun(cmd, &Options{
|
||||||
Identifier: "app/portmaster-app",
|
Identifier: "app/portmaster-app",
|
||||||
AllowDownload: false,
|
AllowDownload: false,
|
||||||
|
AllowHidingWindow: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
@@ -65,9 +82,10 @@ var runNotifier = &cobra.Command{
|
|||||||
Use: "notifier",
|
Use: "notifier",
|
||||||
Short: "Run the Portmaster Notifier",
|
Short: "Run the Portmaster Notifier",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return run(cmd, &Options{
|
return handleRun(cmd, &Options{
|
||||||
Identifier: "notifier/portmaster-notifier",
|
Identifier: "notifier/portmaster-notifier",
|
||||||
AllowDownload: false,
|
AllowDownload: false,
|
||||||
|
AllowHidingWindow: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
@@ -76,7 +94,40 @@ var runNotifier = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(cmd *cobra.Command, opts *Options) error {
|
func handleRun(cmd *cobra.Command, opts *Options) (err error) {
|
||||||
|
err = run(cmd, opts)
|
||||||
|
initiateShutdown(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(cmd *cobra.Command, opts *Options) (err error) {
|
||||||
|
|
||||||
|
// parse identifier
|
||||||
|
opts.ShortIdentifier = path.Dir(opts.Identifier)
|
||||||
|
|
||||||
|
// check for concurrent error (eg. service)
|
||||||
|
shutdownLock.Lock()
|
||||||
|
alreadyDead := shutdownInitiated
|
||||||
|
shutdownLock.Unlock()
|
||||||
|
if alreadyDead {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for duplicate instances
|
||||||
|
if opts.ShortIdentifier == "core" {
|
||||||
|
pid, _ := checkAndCreateInstanceLock(opts.ShortIdentifier)
|
||||||
|
if pid != 0 {
|
||||||
|
return fmt.Errorf("another instance of Portmaster Core is already running: PID %d", pid)
|
||||||
|
}
|
||||||
|
defer deleteInstanceLock(opts.ShortIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify service after some time
|
||||||
|
go func() {
|
||||||
|
// assume that after 5 seconds service has finished starting
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
startupComplete <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
// get original arguments
|
// get original arguments
|
||||||
var args []string
|
var args []string
|
||||||
@@ -84,115 +135,247 @@ func run(cmd *cobra.Command, opts *Options) error {
|
|||||||
return cmd.Help()
|
return cmd.Help()
|
||||||
}
|
}
|
||||||
args = os.Args[3:]
|
args = os.Args[3:]
|
||||||
|
if opts.SuppressArgs {
|
||||||
|
args = nil
|
||||||
|
}
|
||||||
|
|
||||||
// adapt identifier
|
// adapt identifier
|
||||||
if windows() {
|
if onWindows {
|
||||||
opts.Identifier += ".exe"
|
opts.Identifier += ".exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
// run
|
// setup logging
|
||||||
for {
|
// init log file
|
||||||
file, err := getFile(opts)
|
logFile := initControlLogFile()
|
||||||
if err != nil {
|
if logFile != nil {
|
||||||
return fmt.Errorf("could not get component: %s", err)
|
// don't close logFile, will be closed by system
|
||||||
|
if opts.NoOutput {
|
||||||
|
log.Println("disabling log output to stdout... bye!")
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
} else {
|
||||||
|
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
// check permission
|
|
||||||
if !windows() {
|
|
||||||
info, err := os.Stat(file.Path())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get file info on %s: %s", file.Path(), err)
|
|
||||||
}
|
|
||||||
if info.Mode() != 0755 {
|
|
||||||
err := os.Chmod(file.Path(), 0755)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set exec permissions on %s: %s", file.Path(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%s starting %s %s\n", logPrefix, file.Path(), strings.Join(args, " "))
|
|
||||||
|
|
||||||
// create command
|
|
||||||
exc := exec.Command(file.Path(), args...)
|
|
||||||
|
|
||||||
// consume stdout/stderr
|
|
||||||
stdout, err := exc.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect stdout: %s", err)
|
|
||||||
}
|
|
||||||
stderr, err := exc.StderrPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to connect stderr: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start
|
|
||||||
err = exc.Start()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to start %s: %s", opts.Identifier, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start output writers
|
|
||||||
go func() {
|
|
||||||
io.Copy(os.Stdout, stdout)
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
io.Copy(os.Stderr, stderr)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// catch interrupt for clean shutdown
|
|
||||||
signalCh := make(chan os.Signal)
|
|
||||||
signal.Notify(
|
|
||||||
signalCh,
|
|
||||||
os.Interrupt,
|
|
||||||
os.Kill,
|
|
||||||
syscall.SIGHUP,
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
syscall.SIGQUIT,
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
sig := <-signalCh
|
|
||||||
fmt.Printf("%s got %s signal (ignoring), waiting for %s to exit...\n", logPrefix, sig, opts.Identifier)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// wait for completion
|
|
||||||
err = exc.Wait()
|
|
||||||
if err != nil {
|
|
||||||
exErr, ok := err.(*exec.ExitError)
|
|
||||||
if ok {
|
|
||||||
switch exErr.ProcessState.ExitCode() {
|
|
||||||
case 0:
|
|
||||||
// clean exit
|
|
||||||
fmt.Printf("%s clean exit of %s, but with error: %s\n", logPrefix, opts.Identifier, err)
|
|
||||||
os.Exit(1)
|
|
||||||
case 1:
|
|
||||||
// error exit
|
|
||||||
fmt.Printf("%s error during execution of %s: %s\n", logPrefix, opts.Identifier, err)
|
|
||||||
os.Exit(1)
|
|
||||||
case 2357427: // Leet Speak for "restart"
|
|
||||||
// restart request
|
|
||||||
fmt.Printf("%s restarting %s\n", logPrefix, opts.Identifier)
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
fmt.Printf("%s unexpected error during execution of %s: %s\n", logPrefix, opts.Identifier, err)
|
|
||||||
os.Exit(exErr.ProcessState.ExitCode())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s unexpected error type during execution of %s: %s\n", logPrefix, opts.Identifier, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// clean exit
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s %s completed successfully\n", logPrefix, opts.Identifier)
|
// run
|
||||||
return nil
|
tries := 0
|
||||||
|
for {
|
||||||
|
// normal execution
|
||||||
|
tryAgain := false
|
||||||
|
tryAgain, err = execute(opts, args)
|
||||||
|
switch {
|
||||||
|
case tryAgain && err != nil:
|
||||||
|
// temporary? execution error
|
||||||
|
log.Printf("execution of %s failed: %s\n", opts.Identifier, err)
|
||||||
|
tries++
|
||||||
|
if tries >= 5 {
|
||||||
|
log.Println("error seems to be permanent, giving up...")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("trying again...")
|
||||||
|
case tryAgain && err == nil:
|
||||||
|
// upgrade
|
||||||
|
log.Println("restarting by request...")
|
||||||
|
case !tryAgain && err != nil:
|
||||||
|
// fatal error
|
||||||
|
return err
|
||||||
|
case !tryAgain && err == nil:
|
||||||
|
// clean exit
|
||||||
|
log.Printf("%s completed successfully\n", opts.Identifier)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func windows() bool {
|
func execute(opts *Options, args []string) (cont bool, err error) {
|
||||||
return runtime.GOOS == "windows"
|
file, err := getFile(opts)
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("could not get component: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permission
|
||||||
|
if !onWindows {
|
||||||
|
info, err := os.Stat(file.Path())
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("failed to get file info on %s: %s", file.Path(), err)
|
||||||
|
}
|
||||||
|
if info.Mode() != 0755 {
|
||||||
|
err := os.Chmod(file.Path(), 0755)
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("failed to set exec permissions on %s: %s", file.Path(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("starting %s %s\n", file.Path(), strings.Join(args, " "))
|
||||||
|
|
||||||
|
// log files
|
||||||
|
var logFile, errorFile *os.File
|
||||||
|
logFileBasePath := filepath.Join(logsRoot.Path, "fstree", opts.ShortIdentifier)
|
||||||
|
err = logsRoot.EnsureAbsPath(logFileBasePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to check/create log file dir %s: %s\n", logFileBasePath, err)
|
||||||
|
} else {
|
||||||
|
// open log file
|
||||||
|
logFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.log", time.Now().UTC().Format("2006-02-01-15-04-05")))
|
||||||
|
logFile = initializeLogFile(logFilePath, opts.Identifier, file)
|
||||||
|
if logFile != nil {
|
||||||
|
defer finalizeLogFile(logFile, logFilePath)
|
||||||
|
}
|
||||||
|
// open error log file
|
||||||
|
errorFilePath := filepath.Join(logFileBasePath, fmt.Sprintf("%s.error.log", time.Now().UTC().Format("2006-02-01-15-04-05")))
|
||||||
|
errorFile = initializeLogFile(errorFilePath, opts.Identifier, file)
|
||||||
|
if errorFile != nil {
|
||||||
|
defer finalizeLogFile(errorFile, errorFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create command
|
||||||
|
exc := exec.Command(file.Path(), args...)
|
||||||
|
|
||||||
|
if !runningInConsole && opts.AllowHidingWindow {
|
||||||
|
// Windows only:
|
||||||
|
// only hide (all) windows of program if we are not running in console and windows may be hidden
|
||||||
|
hideWindow(exc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if input signals are enabled
|
||||||
|
inputSignalsEnabled := false
|
||||||
|
for _, arg := range args {
|
||||||
|
if strings.HasSuffix(arg, "-input-signals") {
|
||||||
|
inputSignalsEnabled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume stdout/stderr
|
||||||
|
stdout, err := exc.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("failed to connect stdout: %s", err)
|
||||||
|
}
|
||||||
|
stderr, err := exc.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("failed to connect stderr: %s", err)
|
||||||
|
}
|
||||||
|
var stdin io.WriteCloser
|
||||||
|
if inputSignalsEnabled {
|
||||||
|
stdin, err = exc.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("failed to connect stdin: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start
|
||||||
|
err = exc.Start()
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("failed to start %s: %s", opts.Identifier, err)
|
||||||
|
}
|
||||||
|
childIsRunning.Set()
|
||||||
|
|
||||||
|
// start output writers
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
var logFileError error
|
||||||
|
if logFile == nil {
|
||||||
|
_, logFileError = io.Copy(os.Stdout, stdout)
|
||||||
|
} else {
|
||||||
|
if opts.NoOutput {
|
||||||
|
_, logFileError = io.Copy(logFile, stdout)
|
||||||
|
} else {
|
||||||
|
_, logFileError = io.Copy(io.MultiWriter(os.Stdout, logFile), stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if logFileError != nil {
|
||||||
|
log.Printf("failed write logs: %s\n", logFileError)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
var errorFileError error
|
||||||
|
if logFile == nil {
|
||||||
|
_, errorFileError = io.Copy(os.Stderr, stderr)
|
||||||
|
} else {
|
||||||
|
if opts.NoOutput {
|
||||||
|
_, errorFileError = io.Copy(errorFile, stderr)
|
||||||
|
} else {
|
||||||
|
_, errorFileError = io.Copy(io.MultiWriter(os.Stderr, errorFile), stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errorFileError != nil {
|
||||||
|
log.Printf("failed write error logs: %s\n", errorFileError)
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for completion
|
||||||
|
finished := make(chan error)
|
||||||
|
go func() {
|
||||||
|
// wait for output writers to complete
|
||||||
|
wg.Wait()
|
||||||
|
// wait for process to return
|
||||||
|
finished <- exc.Wait()
|
||||||
|
// update status
|
||||||
|
childIsRunning.UnSet()
|
||||||
|
// notify manager
|
||||||
|
close(finished)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// state change listeners
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-shuttingDown:
|
||||||
|
// signal process shutdown
|
||||||
|
if inputSignalsEnabled {
|
||||||
|
// for windows
|
||||||
|
_, err = stdin.Write([]byte("SIGINT\n"))
|
||||||
|
} else {
|
||||||
|
err = exc.Process.Signal(os.Interrupt)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to signal %s to shutdown: %s\n", opts.Identifier, err)
|
||||||
|
err = exc.Process.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to kill %s: %s", opts.Identifier, err)
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("killed %s", opts.Identifier)
|
||||||
|
}
|
||||||
|
// wait until shut down
|
||||||
|
select {
|
||||||
|
case <-finished:
|
||||||
|
case <-time.After(11 * time.Second): // portmaster core prints stack if not able to shutdown in 10 seconds
|
||||||
|
// kill
|
||||||
|
err = exc.Process.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to kill %s: %s", opts.Identifier, err)
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("killed %s", opts.Identifier)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
case err := <-finished:
|
||||||
|
if err != nil {
|
||||||
|
exErr, ok := err.(*exec.ExitError)
|
||||||
|
if ok {
|
||||||
|
switch exErr.ProcessState.ExitCode() {
|
||||||
|
case 0:
|
||||||
|
// clean exit
|
||||||
|
return false, fmt.Errorf("clean exit, but with error: %s", err)
|
||||||
|
case 1:
|
||||||
|
// error exit
|
||||||
|
return true, fmt.Errorf("error during execution: %s", err)
|
||||||
|
case 2357427: // Leet Speak for "restart"
|
||||||
|
// restart request
|
||||||
|
log.Printf("restarting %s\n", opts.Identifier)
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return true, fmt.Errorf("unexpected error during execution: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true, fmt.Errorf("unexpected error type during execution: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clean exit
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
pmctl/service.go
Normal file
31
pmctl/service.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
startupComplete = make(chan struct{}) // signal that the start procedure completed (is never closed, just signaled once)
|
||||||
|
shuttingDown = make(chan struct{}) // signal that we are shutting down (will be closed, may not be closed directly, use initiateShutdown)
|
||||||
|
shutdownInitiated = false // not to be used directly
|
||||||
|
shutdownError error // may not be read or written to directly
|
||||||
|
shutdownLock sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func initiateShutdown(err error) {
|
||||||
|
shutdownLock.Lock()
|
||||||
|
defer shutdownLock.Unlock()
|
||||||
|
|
||||||
|
if !shutdownInitiated {
|
||||||
|
shutdownInitiated = true
|
||||||
|
shutdownError = err
|
||||||
|
close(shuttingDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getShutdownError() error {
|
||||||
|
shutdownLock.Lock()
|
||||||
|
defer shutdownLock.Unlock()
|
||||||
|
|
||||||
|
return shutdownError
|
||||||
|
}
|
||||||
138
pmctl/service_windows.go
Normal file
138
pmctl/service_windows.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Based on the offical Go examples from
|
||||||
|
// https://github.com/golang/sys/blob/master/windows/svc/example
|
||||||
|
// by The Go Authors.
|
||||||
|
// Original LICENSE (sha256sum: 2d36597f7117c38b006835ae7f537487207d8ec407aa9d9980794b2030cbc067) can be found in vendor/pkg cache directory.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"golang.org/x/sys/windows/svc/debug"
|
||||||
|
"golang.org/x/sys/windows/svc/eventlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
runCoreService = &cobra.Command{
|
||||||
|
Use: "core-service",
|
||||||
|
Short: "Run the Portmaster Core as a Windows Service",
|
||||||
|
RunE: runAndLogControlError(func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runService(cmd, &Options{
|
||||||
|
Identifier: "core/portmaster-core",
|
||||||
|
AllowDownload: true,
|
||||||
|
AllowHidingWindow: false,
|
||||||
|
NoOutput: true,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait groups
|
||||||
|
runWg sync.WaitGroup
|
||||||
|
finishWg sync.WaitGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
runCmd.AddCommand(runCoreService)
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceName = "PortmasterCore"
|
||||||
|
|
||||||
|
type windowsService struct{}
|
||||||
|
|
||||||
|
func (ws *windowsService) Execute(args []string, changeRequests <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||||
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||||
|
changes <- svc.Status{State: svc.StartPending}
|
||||||
|
|
||||||
|
service:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-startupComplete:
|
||||||
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
case <-shuttingDown:
|
||||||
|
changes <- svc.Status{State: svc.StopPending}
|
||||||
|
break service
|
||||||
|
case c := <-changeRequests:
|
||||||
|
switch c.Cmd {
|
||||||
|
case svc.Interrogate:
|
||||||
|
changes <- c.CurrentStatus
|
||||||
|
case svc.Stop, svc.Shutdown:
|
||||||
|
initiateShutdown(nil)
|
||||||
|
default:
|
||||||
|
log.Printf("unexpected control request: #%d\n", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// define return values
|
||||||
|
if getShutdownError() != nil {
|
||||||
|
ssec = true // this error is specific to this service (ie. custom)
|
||||||
|
errno = 1 // generic error, check logs / windows events
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until everything else is finished
|
||||||
|
finishWg.Wait()
|
||||||
|
// send stopped status
|
||||||
|
changes <- svc.Status{State: svc.Stopped}
|
||||||
|
// wait a little for the status to reach Windows
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func runService(cmd *cobra.Command, opts *Options) error {
|
||||||
|
// check if we are running interactively
|
||||||
|
isDebug, err := svc.IsAnInteractiveSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not determine if running interactively: %s", err)
|
||||||
|
}
|
||||||
|
// select service run type
|
||||||
|
svcRun := svc.Run
|
||||||
|
if isDebug {
|
||||||
|
log.Printf("WARNING: running interactively, switching to debug execution (no real service).\n")
|
||||||
|
svcRun = debug.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
// open eventlog
|
||||||
|
elog, err := eventlog.Open(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open eventlog: %s", err)
|
||||||
|
}
|
||||||
|
defer elog.Close()
|
||||||
|
|
||||||
|
runWg.Add(2)
|
||||||
|
finishWg.Add(1)
|
||||||
|
|
||||||
|
// run service client
|
||||||
|
go func() {
|
||||||
|
sErr := svcRun(serviceName, &windowsService{})
|
||||||
|
initiateShutdown(sErr)
|
||||||
|
runWg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// run service
|
||||||
|
go func() {
|
||||||
|
// run slightly delayed
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
handleRun(cmd, opts)
|
||||||
|
finishWg.Done()
|
||||||
|
runWg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
runWg.Wait()
|
||||||
|
|
||||||
|
err = getShutdownError()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s service experienced an error: %s\n", serviceName, err)
|
||||||
|
elog.Error(1, fmt.Sprintf("%s experienced an error: %s", serviceName, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
90
pmctl/show.go
Normal file
90
pmctl/show.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(showCmd)
|
||||||
|
showCmd.AddCommand(showCore)
|
||||||
|
showCmd.AddCommand(showApp)
|
||||||
|
showCmd.AddCommand(showNotifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showCmd = &cobra.Command{
|
||||||
|
Use: "show",
|
||||||
|
Short: "Show the command to run a Portmaster component yourself",
|
||||||
|
}
|
||||||
|
|
||||||
|
var showCore = &cobra.Command{
|
||||||
|
Use: "core",
|
||||||
|
Short: "Show command to run the Portmaster Core",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return show(cmd, &Options{
|
||||||
|
Identifier: "core/portmaster-core",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var showApp = &cobra.Command{
|
||||||
|
Use: "app",
|
||||||
|
Short: "Show command to run the Portmaster App",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return show(cmd, &Options{
|
||||||
|
Identifier: "app/portmaster-app",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var showNotifier = &cobra.Command{
|
||||||
|
Use: "notifier",
|
||||||
|
Short: "Show command to run the Portmaster Notifier",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return show(cmd, &Options{
|
||||||
|
Identifier: "notifier/portmaster-notifier",
|
||||||
|
SuppressArgs: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func show(cmd *cobra.Command, opts *Options) error {
|
||||||
|
// get original arguments
|
||||||
|
var args []string
|
||||||
|
if len(os.Args) < 4 {
|
||||||
|
return cmd.Help()
|
||||||
|
}
|
||||||
|
args = os.Args[3:]
|
||||||
|
if opts.SuppressArgs {
|
||||||
|
args = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapt identifier
|
||||||
|
if onWindows {
|
||||||
|
opts.Identifier += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := getFile(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get component: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s %s\n", file.Path(), strings.Join(args, " "))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
40
pmctl/snoretoast_windows.go
Normal file
40
pmctl/snoretoast_windows.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
showCmd.AddCommand(showSnoreToast)
|
||||||
|
runCmd.AddCommand(runSnoreToast)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showSnoreToast = &cobra.Command{
|
||||||
|
Use: "notifier-snoretoast",
|
||||||
|
Short: "Show command to run the Notifier component SnoreToast",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return show(cmd, &Options{
|
||||||
|
Identifier: "notifier/portmaster-snoretoast",
|
||||||
|
SuppressArgs: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var runSnoreToast = &cobra.Command{
|
||||||
|
Use: "notifier-snoretoast",
|
||||||
|
Short: "Run the Notifier component SnoreToast",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return handleRun(cmd, &Options{
|
||||||
|
Identifier: "notifier/portmaster-snoretoast",
|
||||||
|
AllowDownload: false,
|
||||||
|
AllowHidingWindow: true,
|
||||||
|
SuppressArgs: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
124
pmctl/upgrade.go
124
pmctl/upgrade.go
@@ -1,124 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/safing/portbase/info"
|
|
||||||
"github.com/safing/portmaster/updates"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
oldBinSuffix = "-old"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkForUpgrade() (update *updates.File) {
|
|
||||||
info := info.GetInfo()
|
|
||||||
file, err := updates.GetLocalPlatformFile("control/portmaster-control")
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if info.Version != file.Version() {
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func doSelfUpgrade(file *updates.File) error {
|
|
||||||
|
|
||||||
// FIXME: fix permissions if needed
|
|
||||||
|
|
||||||
// get destination
|
|
||||||
dst, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dst, err = filepath.EvalSymlinks(dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// mv destination
|
|
||||||
err = os.Rename(dst, dst+oldBinSuffix)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// hard link
|
|
||||||
err = os.Link(file.Path(), dst)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s failed to hardlink self upgrade: %s, will copy...\n", logPrefix, err)
|
|
||||||
err = copyFile(file.Path(), dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check permission
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
info, err := os.Stat(dst)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get file info on %s: %s", dst, err)
|
|
||||||
}
|
|
||||||
if info.Mode() != 0755 {
|
|
||||||
err := os.Chmod(dst, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set permissions on %s: %s", dst, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(srcPath, dstPath string) (err error) {
|
|
||||||
srcFile, err := os.Open(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
|
|
||||||
dstFile, err := os.Create(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
closeErr := dstFile.Close()
|
|
||||||
if err == nil {
|
|
||||||
err = closeErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = io.Copy(dstFile, srcFile)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = dstFile.Sync()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeOldBin() error {
|
|
||||||
// get location
|
|
||||||
dst, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dst, err = filepath.EvalSymlinks(dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete old
|
|
||||||
err = os.Remove(dst + oldBinSuffix)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("removed previous portmaster-control")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package proc
|
package proc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package proc
|
package proc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package proc
|
package proc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package proc
|
package proc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package proc
|
package proc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -264,7 +262,13 @@ func loadProcess(ctx context.Context, pid int) (*Process, error) {
|
|||||||
|
|
||||||
pInfo, err := processInfo.NewProcess(int32(pid))
|
pInfo, err := processInfo.NewProcess(int32(pid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// TODO: remove this workaround as soon as NewProcess really returns an error on windows when the process does not exist
|
||||||
|
// Issue: https://github.com/shirou/gopsutil/issues/729
|
||||||
|
_, err = pInfo.Name()
|
||||||
|
if err != nil {
|
||||||
|
// process does not exists
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UID
|
// UID
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
// spec: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
|
// spec: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
modules.Register("profile:index", nil, start, stop, "profile", "database")
|
modules.Register("profile:index", nil, start, stop, "core", "profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() (err error) {
|
func start() (err error) {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/safing/portbase/api"
|
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
modules.Register("ui", prep, nil, nil, "updates", "api")
|
modules.Register("ui", prep, nil, nil, "core", "updates")
|
||||||
api.SetDefaultAPIListenAddress("127.0.0.1:817")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func RedirectToBase(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, r.URL.ResolveReference(u).String(), http.StatusPermanentRedirect)
|
http.Redirect(w, r, r.URL.ResolveReference(u).String(), http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/google/renameio"
|
"github.com/google/renameio"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -38,13 +37,13 @@ func fetchFile(realFilepath, updateFilepath string, tries int) error {
|
|||||||
|
|
||||||
// check destination dir
|
// check destination dir
|
||||||
dirPath := filepath.Dir(realFilepath)
|
dirPath := filepath.Dir(realFilepath)
|
||||||
err = utils.EnsureDirectory(dirPath, 0755)
|
err = updateStorage.EnsureAbsPath(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not create updates folder: %s", dirPath)
|
return fmt.Errorf("could not create updates folder: %s", dirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// open file for writing
|
// open file for writing
|
||||||
atomicFile, err := renameio.TempFile(downloadTmpPath, realFilepath)
|
atomicFile, err := renameio.TempFile(tmpStorage.Path, realFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not create temp file for download: %s", err)
|
return fmt.Errorf("could not create temp file for download: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var versionRegex = regexp.MustCompile("_v[0-9]+-[0-9]+-[0-9]+b?")
|
var (
|
||||||
|
fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+b?`)
|
||||||
|
rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+b?\*?$`)
|
||||||
|
)
|
||||||
|
|
||||||
// GetIdentifierAndVersion splits the given file path into its identifier and version.
|
// GetIdentifierAndVersion splits the given file path into its identifier and version.
|
||||||
func GetIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) {
|
func GetIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) {
|
||||||
// extract version
|
// extract version
|
||||||
rawVersion := versionRegex.FindString(versionedPath)
|
rawVersion := fileVersionRegex.FindString(versionedPath)
|
||||||
if rawVersion == "" {
|
if rawVersion == "" {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
}
|
}
|
||||||
|
|||||||
51
updates/filename_test.go
Normal file
51
updates/filename_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testRegexMatch(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) {
|
||||||
|
if testRegex.MatchString(testString) != shouldMatch {
|
||||||
|
if shouldMatch {
|
||||||
|
t.Errorf("regex %s should match %s", testRegex, testString)
|
||||||
|
} else {
|
||||||
|
t.Errorf("regex %s should not match %s", testRegex, testString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegexFind(t *testing.T, testRegex *regexp.Regexp, testString string, shouldMatch bool) {
|
||||||
|
if (testRegex.FindString(testString) != "") != shouldMatch {
|
||||||
|
if shouldMatch {
|
||||||
|
t.Errorf("regex %s should find %s", testRegex, testString)
|
||||||
|
} else {
|
||||||
|
t.Errorf("regex %s should not find %s", testRegex, testString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexes(t *testing.T) {
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.1.2", true)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.1.2*", true)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.1.2b", true)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.1.2b*", true)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "12.13.14", true)
|
||||||
|
|
||||||
|
testRegexMatch(t, rawVersionRegex, "v0.1.2", false)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.", false)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.1", false)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "0.1.", false)
|
||||||
|
testRegexMatch(t, rawVersionRegex, ".1.2", false)
|
||||||
|
testRegexMatch(t, rawVersionRegex, ".1.", false)
|
||||||
|
testRegexMatch(t, rawVersionRegex, "012345", false)
|
||||||
|
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3", true)
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2-3.exe", true)
|
||||||
|
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false)
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file_v1.2.3", false)
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file_1-2-3", false)
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file_v1-2", false)
|
||||||
|
testRegexFind(t, fileVersionRegex, "/path/to/file-v1-2-3", false)
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
@@ -75,7 +74,7 @@ func loadOrFetchFile(identifier string, fetch bool) (*File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// build final filepath
|
// build final filepath
|
||||||
realFilePath := filepath.Join(updateStoragePath, filepath.FromSlash(versionedFilePath))
|
realFilePath := filepath.Join(updateStorage.Path, filepath.FromSlash(versionedFilePath))
|
||||||
if _, err := os.Stat(realFilePath); err == nil {
|
if _, err := os.Stat(realFilePath); err == nil {
|
||||||
// file exists
|
// file exists
|
||||||
updateUsedStatus(identifier, version)
|
updateUsedStatus(identifier, version)
|
||||||
@@ -83,7 +82,7 @@ func loadOrFetchFile(identifier string, fetch bool) (*File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check download dir
|
// check download dir
|
||||||
err := utils.EnsureDirectory(downloadTmpPath, 0755)
|
err := tmpStorage.Ensure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not prepare tmp directory for download: %s", err)
|
return nil, fmt.Errorf("could not prepare tmp directory for download: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/utils"
|
||||||
|
|
||||||
|
semver "github.com/hashicorp/go-version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -26,14 +29,14 @@ func LoadLatest() error {
|
|||||||
|
|
||||||
// all
|
// all
|
||||||
prefix := "all"
|
prefix := "all"
|
||||||
new, err1 := ScanForLatest(filepath.Join(updateStoragePath, prefix), false)
|
new, err1 := ScanForLatest(filepath.Join(updateStorage.Path, prefix), false)
|
||||||
for key, val := range new {
|
for key, val := range new {
|
||||||
newLocalUpdates[filepath.ToSlash(filepath.Join(prefix, key))] = val
|
newLocalUpdates[filepath.ToSlash(filepath.Join(prefix, key))] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
// os_platform
|
// os_platform
|
||||||
prefix = fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
|
prefix = fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
|
||||||
new, err2 := ScanForLatest(filepath.Join(updateStoragePath, prefix), false)
|
new, err2 := ScanForLatest(filepath.Join(updateStorage.Path, prefix), false)
|
||||||
for key, val := range new {
|
for key, val := range new {
|
||||||
newLocalUpdates[filepath.ToSlash(filepath.Join(prefix, key))] = val
|
newLocalUpdates[filepath.ToSlash(filepath.Join(prefix, key))] = val
|
||||||
}
|
}
|
||||||
@@ -70,11 +73,11 @@ func ScanForLatest(baseDir string, hardFail bool) (latest map[string]string, las
|
|||||||
filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
lastError = err
|
lastError = fmt.Errorf("updates: could not read %s: %s", path, err)
|
||||||
if hardFail {
|
if hardFail {
|
||||||
return err
|
return lastError
|
||||||
}
|
}
|
||||||
log.Warningf("updates: could not read %s", path)
|
log.Warning(lastError.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -95,9 +98,24 @@ func ScanForLatest(baseDir string, hardFail bool) (latest map[string]string, las
|
|||||||
// add/update index
|
// add/update index
|
||||||
storedVersion, ok := latest[identifierPath]
|
storedVersion, ok := latest[identifierPath]
|
||||||
if ok {
|
if ok {
|
||||||
// FIXME: this will fail on multi-digit version segments!
|
parsedVersion, err := semver.NewVersion(version)
|
||||||
// FIXME: use https://github.com/hashicorp/go-version
|
if err != nil {
|
||||||
if version > storedVersion {
|
lastError = fmt.Errorf("updates: could not parse version of %s: %s", path, err)
|
||||||
|
if hardFail {
|
||||||
|
return lastError
|
||||||
|
}
|
||||||
|
log.Warning(lastError.Error())
|
||||||
|
}
|
||||||
|
parsedStoredVersion, err := semver.NewVersion(storedVersion)
|
||||||
|
if err != nil {
|
||||||
|
lastError = fmt.Errorf("updates: could not parse version of %s: %s", path, err)
|
||||||
|
if hardFail {
|
||||||
|
return lastError
|
||||||
|
}
|
||||||
|
log.Warning(lastError.Error())
|
||||||
|
}
|
||||||
|
// compare
|
||||||
|
if parsedVersion.GreaterThan(parsedStoredVersion) {
|
||||||
latest[identifierPath] = version
|
latest[identifierPath] = version
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -120,7 +138,7 @@ func ScanForLatest(baseDir string, hardFail bool) (latest map[string]string, las
|
|||||||
|
|
||||||
// LoadIndexes loads the current update indexes from disk.
|
// LoadIndexes loads the current update indexes from disk.
|
||||||
func LoadIndexes() error {
|
func LoadIndexes() error {
|
||||||
data, err := ioutil.ReadFile(filepath.Join(updateStoragePath, "stable.json"))
|
data, err := ioutil.ReadFile(filepath.Join(updateStorage.Path, "stable.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -148,3 +166,39 @@ func LoadIndexes() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSymlinks creates a directory structure with unversions symlinks to the given updates list.
|
||||||
|
func CreateSymlinks(symlinkRoot, updateStorage *utils.DirStructure, updatesList map[string]string) error {
|
||||||
|
err := os.RemoveAll(symlinkRoot.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to wipe symlink root: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = symlinkRoot.Ensure()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create symlink root: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for identifier, version := range updatesList {
|
||||||
|
targetPath := filepath.Join(updateStorage.Path, filepath.FromSlash(GetVersionedPath(identifier, version)))
|
||||||
|
linkPath := filepath.Join(symlinkRoot.Path, filepath.FromSlash(identifier))
|
||||||
|
linkPathDir := filepath.Dir(linkPath)
|
||||||
|
|
||||||
|
err = symlinkRoot.EnsureAbsPath(linkPathDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create dir for link: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeTargetPath, err := filepath.Rel(linkPathDir, targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get relative target path: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Symlink(relativeTargetPath, linkPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to link %s: %s", identifier, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,25 +3,30 @@ package updates
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/core/structure"
|
||||||
|
|
||||||
"github.com/safing/portbase/database"
|
|
||||||
"github.com/safing/portbase/info"
|
"github.com/safing/portbase/info"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
"github.com/safing/portbase/utils"
|
"github.com/safing/portbase/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
updateStoragePath string
|
isWindows = runtime.GOOS == "windows"
|
||||||
downloadTmpPath string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetDatabaseRoot tells the updates module where the database is - and where to put its stuff.
|
var (
|
||||||
func SetDatabaseRoot(path string) {
|
updateStorage *utils.DirStructure
|
||||||
if updateStoragePath == "" {
|
tmpStorage *utils.DirStructure
|
||||||
updateStoragePath = filepath.Join(path, "updates")
|
)
|
||||||
downloadTmpPath = filepath.Join(updateStoragePath, "tmp")
|
|
||||||
|
// SetDataRoot sets the data root from which the updates module derives its paths.
|
||||||
|
func SetDataRoot(root *utils.DirStructure) {
|
||||||
|
if root != nil && updateStorage == nil {
|
||||||
|
updateStorage = root.ChildDir("updates", 0755)
|
||||||
|
tmpStorage = updateStorage.ChildDir("tmp", 0777)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,19 +35,12 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
dbRoot := database.GetDatabaseRoot()
|
SetDataRoot(structure.Root())
|
||||||
if dbRoot == "" {
|
if updateStorage == nil {
|
||||||
return errors.New("database root is not set")
|
return errors.New("update storage path is not set")
|
||||||
}
|
|
||||||
updateStoragePath = filepath.Join(dbRoot, "updates")
|
|
||||||
downloadTmpPath = filepath.Join(updateStoragePath, "tmp")
|
|
||||||
|
|
||||||
err := utils.EnsureDirectory(updateStoragePath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = utils.EnsureDirectory(downloadTmpPath, 0700)
|
err := updateStorage.Ensure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -61,7 +59,13 @@ func start() error {
|
|||||||
err = LoadIndexes()
|
err = LoadIndexes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
log.Infof("updates: stable.json does not yet exist, waiting for first update cycle")
|
// download indexes
|
||||||
|
log.Infof("updates: downloading update index...")
|
||||||
|
|
||||||
|
err = UpdateIndexes()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("updates: failed to download update index: %s", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -79,5 +83,5 @@ func start() error {
|
|||||||
|
|
||||||
func stop() error {
|
func stop() error {
|
||||||
// delete download tmp dir
|
// delete download tmp dir
|
||||||
return os.RemoveAll(downloadTmpPath)
|
return os.RemoveAll(tmpStorage.Path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,29 +7,33 @@ import (
|
|||||||
"github.com/safing/portbase/notifications"
|
"github.com/safing/portbase/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
const coreIdentifier = "core/portmaster"
|
const coreIdentifier = "core/portmaster-core"
|
||||||
|
|
||||||
var lastNotified time.Time
|
var lastNotified time.Time
|
||||||
|
|
||||||
func updateNotifier() {
|
func updateNotifier() {
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(5 * time.Minute)
|
||||||
for {
|
for {
|
||||||
|
ident := coreIdentifier
|
||||||
|
if isWindows {
|
||||||
|
ident += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
_, version, _, ok := getLatestFilePath(coreIdentifier)
|
file, err := GetLocalPlatformFile(ident)
|
||||||
if ok {
|
if err == nil {
|
||||||
status.Lock()
|
status.Lock()
|
||||||
liveVersion := status.Core.Version
|
liveVersion := status.Core.Version
|
||||||
status.Unlock()
|
status.Unlock()
|
||||||
|
|
||||||
if version != liveVersion {
|
if file.Version() != liveVersion {
|
||||||
|
|
||||||
// create notification
|
// create notification
|
||||||
(¬ifications.Notification{
|
(¬ifications.Notification{
|
||||||
ID: "updates-core-update-available",
|
ID: "updates-core-update-available",
|
||||||
Message: fmt.Sprintf("There is an update available for the Portmaster core (v%s), please restart the Portmaster to apply the update.", version),
|
Message: fmt.Sprintf("There is an update available for the Portmaster core (v%s), please restart the Portmaster to apply the update.", file.Version()),
|
||||||
Type: notifications.Info,
|
Type: notifications.Info,
|
||||||
Expires: time.Now().Add(1 * time.Minute).Unix(),
|
Expires: time.Now().Add(1 * time.Minute).Unix(),
|
||||||
}).Init().Save()
|
}).Save()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func updater() {
|
func updater() {
|
||||||
@@ -25,6 +25,14 @@ func updater() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("updates: downloading updates failed: %s", err)
|
log.Warningf("updates: downloading updates failed: %s", err)
|
||||||
}
|
}
|
||||||
|
err = runFileUpgrades()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: failed to upgrade portmaster-control: %s", err)
|
||||||
|
}
|
||||||
|
err = cleanOldUpgradedFiles()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: failed to clean old upgraded files: %s", err)
|
||||||
|
}
|
||||||
time.Sleep(1 * time.Hour)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,13 +83,13 @@ func UpdateIndexes() (err error) {
|
|||||||
updatesLock.Unlock()
|
updatesLock.Unlock()
|
||||||
|
|
||||||
// check dir
|
// check dir
|
||||||
err = utils.EnsureDirectory(updateStoragePath, 0755)
|
err = updateStorage.Ensure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// save stable index
|
// save stable index
|
||||||
err = ioutil.WriteFile(filepath.Join(updateStoragePath, "stable.json"), data, 0644)
|
err = ioutil.WriteFile(filepath.Join(updateStorage.Path, "stable.json"), data, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("updates: failed to save new version of stable.json: %s", err)
|
log.Warningf("updates: failed to save new version of stable.json: %s", err)
|
||||||
}
|
}
|
||||||
@@ -107,6 +115,7 @@ func DownloadUpdates() (err error) {
|
|||||||
markPlatformFileForDownload("control/portmaster-control.exe")
|
markPlatformFileForDownload("control/portmaster-control.exe")
|
||||||
markPlatformFileForDownload("app/portmaster-app.exe")
|
markPlatformFileForDownload("app/portmaster-app.exe")
|
||||||
markPlatformFileForDownload("notifier/portmaster-notifier.exe")
|
markPlatformFileForDownload("notifier/portmaster-notifier.exe")
|
||||||
|
markPlatformFileForDownload("notifier/portmaster-snoretoast.exe")
|
||||||
} else {
|
} else {
|
||||||
markPlatformFileForDownload("core/portmaster-core")
|
markPlatformFileForDownload("core/portmaster-core")
|
||||||
markPlatformFileForDownload("control/portmaster-control")
|
markPlatformFileForDownload("control/portmaster-control")
|
||||||
@@ -115,6 +124,12 @@ func DownloadUpdates() (err error) {
|
|||||||
}
|
}
|
||||||
updatesLock.Unlock()
|
updatesLock.Unlock()
|
||||||
|
|
||||||
|
// check download dir
|
||||||
|
err = tmpStorage.Ensure()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not prepare tmp directory for download: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
// RLock for the remaining function
|
// RLock for the remaining function
|
||||||
updatesLock.RLock()
|
updatesLock.RLock()
|
||||||
defer updatesLock.RUnlock()
|
defer updatesLock.RUnlock()
|
||||||
@@ -127,7 +142,7 @@ func DownloadUpdates() (err error) {
|
|||||||
|
|
||||||
log.Tracef("updates: updating %s to %s", identifier, newVersion)
|
log.Tracef("updates: updating %s to %s", identifier, newVersion)
|
||||||
filePath := GetVersionedPath(identifier, newVersion)
|
filePath := GetVersionedPath(identifier, newVersion)
|
||||||
realFilePath := filepath.Join(updateStoragePath, filePath)
|
realFilePath := filepath.Join(updateStorage.Path, filePath)
|
||||||
for tries := 0; tries < 3; tries++ {
|
for tries := 0; tries < 3; tries++ {
|
||||||
err = fetchFile(realFilePath, filePath, tries)
|
err = fetchFile(realFilePath, filePath, tries)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -142,5 +157,11 @@ func DownloadUpdates() (err error) {
|
|||||||
}
|
}
|
||||||
log.Tracef("updates: finished updating existing files")
|
log.Tracef("updates: finished updating existing files")
|
||||||
|
|
||||||
|
// remove tmp folder after we are finished
|
||||||
|
err = os.RemoveAll(tmpStorage.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("updates: failed to remove tmp dir %s after downloading updates: %s", updateStorage.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
186
updates/upgrader.go
Normal file
186
updates/upgrader.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/renameio"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
|
||||||
|
processInfo "github.com/shirou/gopsutil/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
upgradedSuffix = "-upgraded"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runFileUpgrades() error {
|
||||||
|
filename := "portmaster-control"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
filename += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
// get newest portmaster-control
|
||||||
|
newFile, err := GetPlatformFile("control/" + filename) // identifier, use forward slash!
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update portmaster-control in data root
|
||||||
|
rootControlPath := filepath.Join(filepath.Dir(updateStorage.Path), filename)
|
||||||
|
err = upgradeFile(rootControlPath, newFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("updates: upgraded %s", rootControlPath)
|
||||||
|
|
||||||
|
// upgrade parent process, if it's portmaster-control
|
||||||
|
parent, err := processInfo.NewProcess(int32(os.Getppid()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get parent process for upgrade checks: %s", err)
|
||||||
|
}
|
||||||
|
parentName, err := parent.Name()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get parent process name for upgrade checks: %s", err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(parentName, filename) {
|
||||||
|
log.Tracef("updates: parent process does not seem to be portmaster-control, name is %s", parentName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parentPath, err := parent.Exe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get parent process path for upgrade: %s", err)
|
||||||
|
}
|
||||||
|
err = upgradeFile(parentPath, newFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("updates: upgraded %s", parentPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeFile(fileToUpgrade string, file *File) error {
|
||||||
|
fileExists := false
|
||||||
|
_, err := os.Stat(fileToUpgrade)
|
||||||
|
if err == nil {
|
||||||
|
// file exists and is accessible
|
||||||
|
fileExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the tmp dir exists
|
||||||
|
err = tmpStorage.Ensure()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create directory for upgrade process: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileExists {
|
||||||
|
// get current version
|
||||||
|
var currentVersion string
|
||||||
|
cmd := exec.Command(fileToUpgrade, "--ver")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
// abort if version matches
|
||||||
|
currentVersion = strings.Trim(strings.TrimSpace(string(out)), "*")
|
||||||
|
if currentVersion == file.Version() {
|
||||||
|
// already up to date!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warningf("updates: failed to run %s to get version for upgrade check: %s", fileToUpgrade, err)
|
||||||
|
currentVersion = "0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// test currentVersion for sanity
|
||||||
|
if !rawVersionRegex.MatchString(currentVersion) {
|
||||||
|
log.Tracef("updates: version string returned by %s is invalid: %s", fileToUpgrade, currentVersion)
|
||||||
|
currentVersion = "0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// try removing old version
|
||||||
|
err = os.Remove(fileToUpgrade)
|
||||||
|
if err != nil {
|
||||||
|
// maybe we're on windows and it's in use, try moving
|
||||||
|
err = os.Rename(fileToUpgrade, filepath.Join(
|
||||||
|
tmpStorage.Path,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s-%d%s",
|
||||||
|
GetVersionedPath(filepath.Base(fileToUpgrade), currentVersion),
|
||||||
|
time.Now().UTC().Unix(),
|
||||||
|
upgradedSuffix,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to move file that needs upgrade: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy upgrade
|
||||||
|
// TODO: handle copy failure
|
||||||
|
err = copyFile(file.Path(), fileToUpgrade)
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
// try again
|
||||||
|
err = copyFile(file.Path(), fileToUpgrade)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permissions
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
info, err := os.Stat(fileToUpgrade)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get file info on %s: %s", fileToUpgrade, err)
|
||||||
|
}
|
||||||
|
if info.Mode() != 0755 {
|
||||||
|
err := os.Chmod(fileToUpgrade, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set permissions on %s: %s", fileToUpgrade, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(srcPath, dstPath string) (err error) {
|
||||||
|
// open file for writing
|
||||||
|
atomicDstFile, err := renameio.TempFile(tmpStorage.Path, dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create temp file for atomic copy: %s", err)
|
||||||
|
}
|
||||||
|
defer atomicDstFile.Cleanup()
|
||||||
|
|
||||||
|
// open source
|
||||||
|
srcFile, err := os.Open(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// copy data
|
||||||
|
_, err = io.Copy(atomicDstFile, srcFile)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize file
|
||||||
|
err = atomicDstFile.CloseAtomicallyReplace()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updates: failed to finalize copy to file %s: %s", dstPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanOldUpgradedFiles() error {
|
||||||
|
return os.RemoveAll(tmpStorage.Path)
|
||||||
|
}
|
||||||
@@ -1,23 +1,38 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/utils"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
updatesStorage *utils.DirStructure
|
||||||
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "uptool",
|
Use: "uptool",
|
||||||
Short: "helper tool for the update process",
|
Short: "helper tool for the update process",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cmd.Usage()
|
cmd.Usage()
|
||||||
},
|
},
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
absPath, err := filepath.Abs(".")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatesStorage = utils.NewDirStructure(absPath, 0755)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
updates/uptool/update.go
Normal file
44
updates/uptool/update.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/updates"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateCmd = &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Update scans the current directory and updates the index and symlink structure",
|
||||||
|
RunE: update,
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
latest, err := updates.ScanForLatest(".", true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(latest, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile("stable.json", data, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updates.CreateSymlinks(updatesStorage.ChildDir("latest", 0755), updatesStorage, latest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user