mirror of
https://github.com/safing/portmaster
synced 2025-09-04 03:29:12 +00:00
Add updates module and fix issues
This commit is contained in:
parent
bde81d815d
commit
20af9efecc
22 changed files with 953 additions and 96 deletions
|
@ -1,6 +1,7 @@
|
||||||
package firewall
|
package firewall
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -57,14 +58,14 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check domain list
|
// check domain list
|
||||||
permitted, ok := profileSet.CheckDomain(fqdn)
|
permitted, reason, ok := profileSet.CheckEndpoint(fqdn, 0, 0, false)
|
||||||
if ok {
|
if ok {
|
||||||
if permitted {
|
if permitted {
|
||||||
log.Infof("firewall: accepting connection %s, domain is whitelisted", connection)
|
log.Infof("firewall: accepting connection %s, endpoint is whitelisted: %s", connection, reason)
|
||||||
connection.Accept("domain is whitelisted")
|
connection.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("firewall: denying connection %s, domain is blacklisted", connection)
|
log.Infof("firewall: denying connection %s, endpoint is blacklisted", connection)
|
||||||
connection.Deny("domain is blacklisted")
|
connection.Deny("endpoint is blacklisted")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -207,6 +208,8 @@ func DecideOnConnection(connection *network.Connection, pkt packet.Packet) {
|
||||||
connection.Deny("peer to peer connections (to an IP) not allowed")
|
connection.Deny("peer to peer connections (to an IP) not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check network scope
|
// check network scope
|
||||||
|
@ -269,6 +272,13 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
|
||||||
// Profile.ConnectPorts
|
// Profile.ConnectPorts
|
||||||
// Profile.ListenPorts
|
// Profile.ListenPorts
|
||||||
|
|
||||||
|
// grant self
|
||||||
|
if connection.Process().Pid == os.Getpid() {
|
||||||
|
log.Infof("firewall: granting own link %s", connection)
|
||||||
|
connection.Accept("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// check if there is a profile
|
// check if there is a profile
|
||||||
profileSet := connection.Process().ProfileSet()
|
profileSet := connection.Process().ProfileSet()
|
||||||
if profileSet == nil {
|
if profileSet == nil {
|
||||||
|
@ -278,7 +288,18 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
|
||||||
}
|
}
|
||||||
profileSet.Update(status.CurrentSecurityLevel())
|
profileSet.Update(status.CurrentSecurityLevel())
|
||||||
|
|
||||||
// get remote Port
|
// get host
|
||||||
|
var domainOrIP string
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(connection.Domain, "."):
|
||||||
|
domainOrIP = connection.Domain
|
||||||
|
case connection.Direction:
|
||||||
|
domainOrIP = pkt.GetIPHeader().Src.String()
|
||||||
|
default:
|
||||||
|
domainOrIP = pkt.GetIPHeader().Dst.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// get protocol / destination port
|
||||||
protocol := pkt.GetIPHeader().Protocol
|
protocol := pkt.GetIPHeader().Protocol
|
||||||
var dstPort uint16
|
var dstPort uint16
|
||||||
tcpUDPHeader := pkt.GetTCPUDPHeader()
|
tcpUDPHeader := pkt.GetTCPUDPHeader()
|
||||||
|
@ -286,12 +307,12 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
|
||||||
dstPort = tcpUDPHeader.DstPort
|
dstPort = tcpUDPHeader.DstPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// check port list
|
// check endpoints list
|
||||||
permitted, ok := profileSet.CheckPort(connection.Direction, uint8(protocol), dstPort)
|
permitted, reason, ok := profileSet.CheckEndpoint(domainOrIP, uint8(protocol), dstPort, connection.Direction)
|
||||||
if ok {
|
if ok {
|
||||||
if permitted {
|
if permitted {
|
||||||
log.Infof("firewall: accepting link %s", link)
|
log.Infof("firewall: accepting link %s, endpoint is whitelisted: %s", link, reason)
|
||||||
link.Accept("port whitelisted")
|
link.Accept(fmt.Sprintf("port whitelisted: %s", reason))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("firewall: denying link %s: port %d is blacklisted", link, dstPort)
|
log.Infof("firewall: denying link %s: port %d is blacklisted", link, dstPort)
|
||||||
link.Deny("port blacklisted")
|
link.Deny("port blacklisted")
|
||||||
|
@ -301,16 +322,16 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
|
||||||
|
|
||||||
switch profileSet.GetProfileMode() {
|
switch profileSet.GetProfileMode() {
|
||||||
case profile.Whitelist:
|
case profile.Whitelist:
|
||||||
log.Infof("firewall: denying link %s: port %d is not whitelisted", link, dstPort)
|
log.Infof("firewall: denying link %s: endpoint %d is not whitelisted", link, dstPort)
|
||||||
link.Deny("port is not whitelisted")
|
link.Deny("endpoint is not whitelisted")
|
||||||
return
|
return
|
||||||
case profile.Prompt:
|
case profile.Prompt:
|
||||||
log.Infof("firewall: accepting link %s: port %d is blacklisted", link, dstPort)
|
log.Infof("firewall: accepting link %s: endpoint %d is blacklisted", link, dstPort)
|
||||||
link.Accept("port permitted (prompting is not yet implemented)")
|
link.Accept("endpoint permitted (prompting is not yet implemented)")
|
||||||
return
|
return
|
||||||
case profile.Blacklist:
|
case profile.Blacklist:
|
||||||
log.Infof("firewall: accepting link %s: port %d is not blacklisted", link, dstPort)
|
log.Infof("firewall: accepting link %s: endpoint %d is not blacklisted", link, dstPort)
|
||||||
link.Accept("port is not blacklisted")
|
link.Accept("endpoint is not blacklisted")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1
main.go
1
main.go
|
@ -20,6 +20,7 @@ import (
|
||||||
_ "github.com/Safing/portbase/database/storage/badger"
|
_ "github.com/Safing/portbase/database/storage/badger"
|
||||||
_ "github.com/Safing/portmaster/firewall"
|
_ "github.com/Safing/portmaster/firewall"
|
||||||
_ "github.com/Safing/portmaster/nameserver"
|
_ "github.com/Safing/portmaster/nameserver"
|
||||||
|
_ "github.com/Safing/portmaster/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -68,7 +68,7 @@ func (p *Process) Save() {
|
||||||
// Delete deletes a process from the storage and propagates the change.
|
// Delete deletes a process from the storage and propagates the change.
|
||||||
func (p *Process) Delete() {
|
func (p *Process) Delete() {
|
||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Lock()
|
defer p.Unlock()
|
||||||
|
|
||||||
processesLock.Lock()
|
processesLock.Lock()
|
||||||
delete(processes, p.Pid)
|
delete(processes, p.Pid)
|
||||||
|
@ -79,7 +79,10 @@ func (p *Process) Delete() {
|
||||||
go dbController.PushUpdate(p)
|
go dbController.PushUpdate(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.DeactivateProfileSet(p.profileSet)
|
// TODO: this should not be necessary, as processes should always have a profileSet.
|
||||||
|
if p.profileSet != nil {
|
||||||
|
profile.DeactivateProfileSet(p.profileSet)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanProcessStorage cleans the storage from old processes.
|
// CleanProcessStorage cleans the storage from old processes.
|
||||||
|
|
|
@ -56,8 +56,10 @@ func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkRe
|
||||||
isDomain := strings.HasSuffix(domainOrIP, ".")
|
isDomain := strings.HasSuffix(domainOrIP, ".")
|
||||||
|
|
||||||
for _, entry := range e {
|
for _, entry := range e {
|
||||||
if ok, reason := entry.Matches(domainOrIP, protocol, port, isDomain, cachedGetDomainOfIP); ok {
|
if entry != nil {
|
||||||
return entry.Permit, reason, true
|
if ok, reason := entry.Matches(domainOrIP, protocol, port, isDomain, cachedGetDomainOfIP); ok {
|
||||||
|
return entry.Permit, reason, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ func New() *Profile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeProfileKey creates the correct key for a profile with the given namespace and ID.
|
||||||
func MakeProfileKey(namespace, ID string) string {
|
func MakeProfileKey(namespace, ID string) string {
|
||||||
return fmt.Sprintf("core:profiles/%s/%s", namespace, ID)
|
return fmt.Sprintf("core:profiles/%s/%s", namespace, ID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,13 +47,15 @@ func getSpecialProfile(ID string) (*Profile, error) {
|
||||||
|
|
||||||
func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
|
func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
|
||||||
for _, ep := range p.ServiceEndpoints {
|
for _, ep := range p.ServiceEndpoints {
|
||||||
if ep.DomainOrIP == "" &&
|
if ep != nil {
|
||||||
ep.Wildcard == true &&
|
if ep.DomainOrIP == "" &&
|
||||||
ep.Protocol == 0 &&
|
ep.Wildcard == true &&
|
||||||
ep.StartPort == 0 &&
|
ep.Protocol == 0 &&
|
||||||
ep.EndPort == 0 &&
|
ep.StartPort == 0 &&
|
||||||
ep.Permit == false {
|
ep.EndPort == 0 &&
|
||||||
return false
|
ep.Permit == false {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
66
ui/launch.go
Normal file
66
ui/launch.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/modules"
|
||||||
|
"github.com/Safing/portmaster/updates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
launchUI bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&launchUI, "ui", false, "launch user interface and exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
func launchUIByFlag() error {
|
||||||
|
if !launchUI {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := updates.ReloadLatest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
osAndPlatform := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
switch osAndPlatform {
|
||||||
|
case "linux_amd64":
|
||||||
|
|
||||||
|
file, err := updates.GetPlatformFile("app/portmaster-ui")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ui currently not available: %s - you may need to first start portmaster and wait for it to fetch the update index", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permission
|
||||||
|
info, err := os.Stat(file.Path())
|
||||||
|
if info.Mode() != 0755 {
|
||||||
|
fmt.Printf("%v\n", info.Mode())
|
||||||
|
err := os.Chmod(file.Path(), 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set exec permissions on %s: %s", file.Path(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exec
|
||||||
|
cmd := exec.Command(file.Path())
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start ui: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gracefully exit portmaster
|
||||||
|
return modules.ErrCleanExit
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errors.New("this os/platform is no UI support yet")
|
||||||
|
}
|
||||||
|
}
|
11
ui/module.go
11
ui/module.go
|
@ -5,13 +5,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
modules.Register("ui", prep, start, stop, "database", "api")
|
modules.Register("ui", prep, nil, nil, "updates", "api")
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
func prep() error {
|
||||||
return nil
|
err := launchUIByFlag()
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func stop() error {
|
return registerRoutes()
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
113
ui/serve.go
113
ui/serve.go
|
@ -6,16 +6,16 @@ import (
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
resources "github.com/cookieo9/resources-go"
|
resources "github.com/cookieo9/resources-go"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/Safing/portbase/api"
|
"github.com/Safing/portbase/api"
|
||||||
"github.com/Safing/portbase/database"
|
|
||||||
"github.com/Safing/portbase/log"
|
"github.com/Safing/portbase/log"
|
||||||
|
"github.com/Safing/portmaster/updates"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -25,66 +25,80 @@ var (
|
||||||
assetsLock sync.RWMutex
|
assetsLock sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func start() error {
|
func registerRoutes() error {
|
||||||
basePath := path.Join(database.GetDatabaseRoot(), "updates", "files", "apps")
|
api.RegisterHandleFunc("/assets/{resPath:[a-zA-Z0-9/\\._-]+}", ServeBundle("assets")).Methods("GET", "HEAD")
|
||||||
|
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}", redirAddSlash).Methods("GET", "HEAD")
|
||||||
serveUIRouter := mux.NewRouter()
|
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/", ServeBundle("")).Methods("GET", "HEAD")
|
||||||
serveUIRouter.HandleFunc("/assets/{resPath:[a-zA-Z0-9/\\._-]+}", ServeAssets(basePath))
|
api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", ServeBundle("")).Methods("GET", "HEAD")
|
||||||
serveUIRouter.HandleFunc("/app/{appName:[a-z]+}/", ServeApps(basePath))
|
api.RegisterHandleFunc("/", RedirectToBase)
|
||||||
serveUIRouter.HandleFunc("/app/{appName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", ServeApps(basePath))
|
|
||||||
serveUIRouter.HandleFunc("/", RedirectToControl)
|
|
||||||
|
|
||||||
api.RegisterAdditionalRoute("/assets/", serveUIRouter)
|
|
||||||
api.RegisterAdditionalRoute("/app/", serveUIRouter)
|
|
||||||
api.RegisterAdditionalRoute("/", serveUIRouter)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeApps serves app files.
|
// ServeBundle serves bundles.
|
||||||
func ServeApps(basePath string) func(w http.ResponseWriter, r *http.Request) {
|
func ServeBundle(defaultModuleName string) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// log.Tracef("ui: request for %s", r.RequestURI)
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
appName, ok := vars["appName"]
|
moduleName, ok := vars["moduleName"]
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "missing app name", http.StatusBadRequest)
|
moduleName = defaultModuleName
|
||||||
return
|
if moduleName == "" {
|
||||||
|
http.Error(w, "missing module name", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resPath, ok := vars["resPath"]
|
resPath, ok := vars["resPath"]
|
||||||
if !ok {
|
if !ok || strings.HasSuffix(resPath, "/") {
|
||||||
http.Error(w, "missing resource path", http.StatusBadRequest)
|
resPath = "index.html"
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appsLock.RLock()
|
appsLock.RLock()
|
||||||
bundle, ok := apps[appName]
|
bundle, ok := apps[moduleName]
|
||||||
appsLock.RUnlock()
|
appsLock.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
ServeFileFromBundle(w, r, bundle, resPath)
|
ServeFileFromBundle(w, r, moduleName, bundle, resPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newBundle, err := resources.OpenZip(path.Join(basePath, fmt.Sprintf("%s.zip", appName)))
|
// get file from update system
|
||||||
|
zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == updates.ErrNotFound {
|
||||||
|
log.Tracef("ui: requested module %s does not exist", moduleName)
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
log.Tracef("ui: error loading module %s: %s", moduleName, err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// open bundle
|
||||||
|
newBundle, err := resources.OpenZip(zipFile.Path())
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("ui: error prepping module %s: %s", moduleName, err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bundle = &resources.BundleSequence{newBundle}
|
bundle = &resources.BundleSequence{newBundle}
|
||||||
appsLock.Lock()
|
appsLock.Lock()
|
||||||
apps[appName] = bundle
|
apps[moduleName] = bundle
|
||||||
appsLock.Unlock()
|
appsLock.Unlock()
|
||||||
|
|
||||||
ServeFileFromBundle(w, r, bundle, resPath)
|
ServeFileFromBundle(w, r, moduleName, bundle, resPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeFileFromBundle serves a file from the given bundle.
|
// ServeFileFromBundle serves a file from the given bundle.
|
||||||
func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundle *resources.BundleSequence, path string) {
|
func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName string, bundle *resources.BundleSequence, path string) {
|
||||||
readCloser, err := bundle.Open(path)
|
readCloser, err := bundle.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Tracef("ui: error opening module %s: %s", bundleName, err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -110,45 +124,16 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundle *resourc
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeAssets serves global UI assets.
|
// RedirectToBase redirects the requests to the control app
|
||||||
func ServeAssets(basePath string) func(w http.ResponseWriter, r *http.Request) {
|
func RedirectToBase(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
u, err := url.Parse("/ui/modules/base/")
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
resPath, ok := vars["resPath"]
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "missing resource path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assetsLock.RLock()
|
|
||||||
bundle := assets
|
|
||||||
assetsLock.RUnlock()
|
|
||||||
if bundle != nil {
|
|
||||||
ServeFileFromBundle(w, r, bundle, resPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
newBundle, err := resources.OpenZip(path.Join(basePath, "assets.zip"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle = &resources.BundleSequence{newBundle}
|
|
||||||
assetsLock.Lock()
|
|
||||||
assets = bundle
|
|
||||||
assetsLock.Unlock()
|
|
||||||
|
|
||||||
ServeFileFromBundle(w, r, bundle, resPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedirectToControl redirects the requests to the control app
|
|
||||||
func RedirectToControl(w http.ResponseWriter, r *http.Request) {
|
|
||||||
u, err := url.Parse("/app/control")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redirAddSlash(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect)
|
||||||
|
}
|
||||||
|
|
9
updates/doc.go
Normal file
9
updates/doc.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
// current paths:
|
||||||
|
// all/ui/assets.zip
|
||||||
|
// all/ui/modules/base.zip
|
||||||
|
// all/ui/modules/settings.zip
|
||||||
|
// all/ui/modules/profilemgr.zip
|
||||||
|
// all/ui/modules/monitor.zip
|
||||||
|
// linux_amd64/app/portmaster-ui
|
122
updates/fetch.go
Normal file
122
updates/fetch.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/renameio"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
updateURLs = []string{
|
||||||
|
"https://updates.safing.io",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func fetchFile(realFilepath, updateFilepath string, tries int) error {
|
||||||
|
// backoff when retrying
|
||||||
|
if tries > 0 {
|
||||||
|
time.Sleep(time.Duration(tries*tries) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create URL
|
||||||
|
downloadURL, err := joinURLandPath(updateURLs[tries%len(updateURLs)], updateFilepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error build url (%s + %s): %s", updateURLs[tries%len(updateURLs)], updateFilepath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create destination dir
|
||||||
|
dirPath := filepath.Dir(realFilepath)
|
||||||
|
err = os.MkdirAll(dirPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updates: could not create updates folder: %s", dirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open file for writing
|
||||||
|
atomicFile, err := renameio.TempFile(filepath.Join(updateStoragePath, "tmp"), realFilepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updates: could not create temp file for download: %s", err)
|
||||||
|
}
|
||||||
|
defer atomicFile.Cleanup()
|
||||||
|
|
||||||
|
// start file download
|
||||||
|
resp, err := http.Get(downloadURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error fetching url (%s): %s", downloadURL, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// download and write file
|
||||||
|
n, err := io.Copy(atomicFile, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed downloading %s: %s", downloadURL, err)
|
||||||
|
}
|
||||||
|
if resp.ContentLength != n {
|
||||||
|
return fmt.Errorf("download unfinished, written %d out of %d bytes.", n, resp.ContentLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalize file
|
||||||
|
err = atomicFile.CloseAtomicallyReplace()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("updates: failed to finalize file %s: %s", realFilepath, err)
|
||||||
|
}
|
||||||
|
// set permissions
|
||||||
|
err = os.Chmod(realFilepath, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: failed to set permissions on downloaded file %s: %s", realFilepath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("update: fetched %s (stored to %s)", downloadURL, realFilepath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchData(downloadPath string, tries int) ([]byte, error) {
|
||||||
|
// backoff when retrying
|
||||||
|
if tries > 0 {
|
||||||
|
time.Sleep(time.Duration(tries*tries) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create URL
|
||||||
|
downloadURL, err := joinURLandPath(updateURLs[tries%len(updateURLs)], downloadPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error build url (%s + %s): %s", updateURLs[tries%len(updateURLs)], downloadPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start file download
|
||||||
|
resp, err := http.Get(downloadURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetching url (%s): %s", downloadURL, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// download and write file
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength))
|
||||||
|
n, err := io.Copy(buf, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed downloading %s: %s", downloadURL, err)
|
||||||
|
}
|
||||||
|
if resp.ContentLength != n {
|
||||||
|
return nil, fmt.Errorf("download unfinished, written %d out of %d bytes.", n, resp.ContentLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinURLandPath(baseURL, urlPath string) (string, error) {
|
||||||
|
u, err := url.Parse(baseURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Path = path.Join(u.Path, urlPath)
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
46
updates/file.go
Normal file
46
updates/file.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
// File represents a file from the update system.
|
||||||
|
type File struct {
|
||||||
|
filepath string
|
||||||
|
version string
|
||||||
|
stable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFile(filepath string, version string, stable bool) *File {
|
||||||
|
return &File{
|
||||||
|
filepath: filepath,
|
||||||
|
version: version,
|
||||||
|
stable: stable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the filepath of the file.
|
||||||
|
func (f *File) Path() string {
|
||||||
|
return f.filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the version of the file.
|
||||||
|
func (f *File) Version() string {
|
||||||
|
return f.version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stable returns whether the file is from a stable release.
|
||||||
|
func (f *File) Stable() bool {
|
||||||
|
return f.stable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the file and returns the
|
||||||
|
func (f *File) Open() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportError reports an error back to Safing. This will not automatically blacklist the file.
|
||||||
|
func (f *File) ReportError() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blacklist notifies the update system that this file is somehow broken, and should be ignored from now on.
|
||||||
|
func (f *File) Blacklist() {
|
||||||
|
|
||||||
|
}
|
41
updates/filename.go
Normal file
41
updates/filename.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionRegex = regexp.MustCompile("_v[0-9]+-[0-9]+-[0-9]+b?")
|
||||||
|
|
||||||
|
func getIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) {
|
||||||
|
// extract version
|
||||||
|
rawVersion := versionRegex.FindString(versionedPath)
|
||||||
|
if rawVersion == "" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace - with . and trim _
|
||||||
|
version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", -1)
|
||||||
|
|
||||||
|
// put together without version
|
||||||
|
i := strings.Index(versionedPath, rawVersion)
|
||||||
|
if i < 0 {
|
||||||
|
// extracted version not in string (impossible)
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return versionedPath[:i] + versionedPath[i+len(rawVersion):], version, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVersionedPath(identifier, version string) (versionedPath string) {
|
||||||
|
// split in half
|
||||||
|
splittedFilePath := strings.SplitN(identifier, ".", 2)
|
||||||
|
// replace . with -
|
||||||
|
transformedVersion := strings.Replace(version, ".", "-", -1)
|
||||||
|
|
||||||
|
// put together
|
||||||
|
if len(splittedFilePath) == 1 {
|
||||||
|
return fmt.Sprintf("%s_v%s", splittedFilePath[0], transformedVersion)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s_v%s.%s", splittedFilePath[0], transformedVersion, splittedFilePath[1])
|
||||||
|
}
|
77
updates/get.go
Normal file
77
updates/get.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("the requested file could not be found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPlatformFile returns the latest platform specific file identified by the given identifier.
|
||||||
|
func GetPlatformFile(identifier string) (*File, error) {
|
||||||
|
identifier = filepath.Join(fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH), identifier)
|
||||||
|
// From https://golang.org/pkg/runtime/#GOARCH
|
||||||
|
// GOOS is the running program's operating system target: one of darwin, freebsd, linux, and so on.
|
||||||
|
// GOARCH is the running program's architecture target: one of 386, amd64, arm, s390x, and so on.
|
||||||
|
return loadOrFetchFile(identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFile returns the latest generic file identified by the given identifier.
|
||||||
|
func GetFile(identifier string) (*File, error) {
|
||||||
|
identifier = filepath.Join("all", identifier)
|
||||||
|
return loadOrFetchFile(identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLatestFilePath(identifier string) (versionedFilePath, version string, stable bool, ok bool) {
|
||||||
|
updatesLock.RLock()
|
||||||
|
version, ok = stableUpdates[identifier]
|
||||||
|
if !ok {
|
||||||
|
version, ok = latestUpdates[identifier]
|
||||||
|
if !ok {
|
||||||
|
log.Tracef("updates: file %s does not exist", identifier)
|
||||||
|
return "", "", false, false
|
||||||
|
// TODO: if in development mode, reload latest index to check for newly sideloaded updates
|
||||||
|
// err := reloadLatest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatesLock.RUnlock()
|
||||||
|
|
||||||
|
// TODO: Fix for stable release
|
||||||
|
return getVersionedPath(identifier, version), version, false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOrFetchFile(identifier string) (*File, error) {
|
||||||
|
versionedFilePath, version, stable, ok := getLatestFilePath(identifier)
|
||||||
|
if !ok {
|
||||||
|
// TODO: if in development mode, search updates dir for sideloaded apps
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// build final filepath
|
||||||
|
realFilePath := filepath.Join(updateStoragePath, versionedFilePath)
|
||||||
|
if _, err := os.Stat(realFilePath); err == nil {
|
||||||
|
// file exists
|
||||||
|
return newFile(realFilePath, version, stable), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// download file
|
||||||
|
log.Tracef("updates: starting download of %s", versionedFilePath)
|
||||||
|
var err error
|
||||||
|
for tries := 0; tries < 5; tries++ {
|
||||||
|
err := fetchFile(realFilePath, versionedFilePath, tries)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("updates: failed to download %s: %s, retrying (%d)", versionedFilePath, err, tries+1)
|
||||||
|
} else {
|
||||||
|
return newFile(realFilePath, version, stable), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Warningf("updates: failed to download %s: %s", versionedFilePath, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
24
updates/get_test.go
Normal file
24
updates/get_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func testBuildVersionedFilePath(t *testing.T, identifier, version, expectedVersionedFilePath string) {
|
||||||
|
updatesLock.Lock()
|
||||||
|
stableUpdates[identifier] = version
|
||||||
|
// betaUpdates[identifier] = version
|
||||||
|
updatesLock.Unlock()
|
||||||
|
|
||||||
|
versionedFilePath, _, _, ok := getLatestFilePath(identifier)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("identifier %s should exist", identifier)
|
||||||
|
}
|
||||||
|
if versionedFilePath != expectedVersionedFilePath {
|
||||||
|
t.Errorf("unexpected versionedFilePath: %s", versionedFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildVersionedFilePath(t *testing.T) {
|
||||||
|
testBuildVersionedFilePath(t, "path/to/asset.zip", "1.2.3", "path/to/asset_v1-2-3.zip")
|
||||||
|
testBuildVersionedFilePath(t, "path/to/asset.tar.gz", "1.2.3b", "path/to/asset_v1-2-3b.tar.gz")
|
||||||
|
testBuildVersionedFilePath(t, "path/to/asset", "1.2.3b", "path/to/asset_v1-2-3b")
|
||||||
|
}
|
141
updates/latest.go
Normal file
141
updates/latest.go
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stableUpdates = make(map[string]string)
|
||||||
|
betaUpdates = make(map[string]string)
|
||||||
|
latestUpdates = make(map[string]string)
|
||||||
|
updatesLock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReloadLatest reloads available updates from disk.
|
||||||
|
func ReloadLatest() error {
|
||||||
|
newLatestUpdates := make(map[string]string)
|
||||||
|
|
||||||
|
// all
|
||||||
|
new, err1 := ScanForLatest(filepath.Join(updateStoragePath, "all"), false)
|
||||||
|
for key, val := range new {
|
||||||
|
newLatestUpdates[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// os_platform
|
||||||
|
new, err2 := ScanForLatest(filepath.Join(updateStoragePath, fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)), false)
|
||||||
|
for key, val := range new {
|
||||||
|
newLatestUpdates[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if err1 != nil && err2 != nil {
|
||||||
|
return fmt.Errorf("could not load latest update versions: %s, %s", err1, err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("updates: loading latest updates:")
|
||||||
|
|
||||||
|
for key, val := range newLatestUpdates {
|
||||||
|
log.Tracef("updates: %s v%s", key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatesLock.Lock()
|
||||||
|
latestUpdates = newLatestUpdates
|
||||||
|
updatesLock.Unlock()
|
||||||
|
|
||||||
|
log.Tracef("updates: load complete")
|
||||||
|
|
||||||
|
if len(stableUpdates) == 0 {
|
||||||
|
err := loadIndexesFromDisk()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScanForLatest(baseDir string, hardFail bool) (latest map[string]string, lastError error) {
|
||||||
|
var added int
|
||||||
|
latest = make(map[string]string)
|
||||||
|
|
||||||
|
filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
lastError = err
|
||||||
|
if hardFail {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Warningf("updates: could not read %s", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
added++
|
||||||
|
}
|
||||||
|
|
||||||
|
relativePath := strings.TrimLeft(strings.TrimPrefix(path, baseDir), "/")
|
||||||
|
identifierPath, version, ok := getIdentifierAndVersion(relativePath)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add/update index
|
||||||
|
storedVersion, ok := latest[identifierPath]
|
||||||
|
if ok {
|
||||||
|
// FIXME: this will fail on multi-digit version segments!
|
||||||
|
if version > storedVersion {
|
||||||
|
latest[identifierPath] = version
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
latest[identifierPath] = version
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if lastError != nil {
|
||||||
|
if hardFail {
|
||||||
|
return nil, lastError
|
||||||
|
}
|
||||||
|
if added == 0 {
|
||||||
|
return latest, lastError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return latest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadIndexesFromDisk() error {
|
||||||
|
data, err := ioutil.ReadFile(filepath.Join(updateStoragePath, "stable.json"))
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Infof("updates: stable.json does not yet exist, waiting for first update cycle")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newStableUpdates := make(map[string]string)
|
||||||
|
err = json.Unmarshal(data, &newStableUpdates)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newStableUpdates) == 0 {
|
||||||
|
return errors.New("stable.json is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("updates: loaded stable.json")
|
||||||
|
|
||||||
|
updatesLock.Lock()
|
||||||
|
stableUpdates = newStableUpdates
|
||||||
|
updatesLock.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
73
updates/latest_test.go
Normal file
73
updates/latest_test.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testLoadLatestScope(t *testing.T, basePath, filePath, expectedIdentifier, expectedVersion string) {
|
||||||
|
fullPath := filepath.Join(basePath, filePath)
|
||||||
|
|
||||||
|
// create dir
|
||||||
|
dirPath := filepath.Dir(fullPath)
|
||||||
|
err := os.MkdirAll(dirPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create test dir: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// touch file
|
||||||
|
err = ioutil.WriteFile(fullPath, []byte{}, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create test file: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// run loadLatestScope
|
||||||
|
latest, err := ScanForLatest(basePath, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not update latest: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for key, val := range latest {
|
||||||
|
latestUpdates[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// test result
|
||||||
|
version, ok := latestUpdates[expectedIdentifier]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("identifier %s not in map", expectedIdentifier)
|
||||||
|
t.Errorf("current map: %v", latestUpdates)
|
||||||
|
}
|
||||||
|
if version != expectedVersion {
|
||||||
|
t.Errorf("unexpected version for %s: %s", filePath, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadLatestScope(t *testing.T) {
|
||||||
|
|
||||||
|
updatesLock.Lock()
|
||||||
|
defer updatesLock.Unlock()
|
||||||
|
|
||||||
|
tmpDir, err := ioutil.TempDir("", "testing_")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not create test dir: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "1.2.3")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3b.zip", "all/ui/assets.zip", "1.2.3b")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4.zip", "all/ui/assets.zip", "1.2.4")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "1.3.4")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v2-3-4.zip", "all/ui/assets.zip", "2.3.4")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-3.zip", "all/ui/assets.zip", "2.3.4")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-2-4.zip", "all/ui/assets.zip", "2.3.4")
|
||||||
|
testLoadLatestScope(t, tmpDir, "all/ui/assets_v1-3-4.zip", "all/ui/assets.zip", "2.3.4")
|
||||||
|
testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "1.2.3")
|
||||||
|
testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v2-1-1", "os_platform/portmaster/portmaster", "2.1.1")
|
||||||
|
testLoadLatestScope(t, tmpDir, "os_platform/portmaster/portmaster_v1-2-3", "os_platform/portmaster/portmaster", "2.1.1")
|
||||||
|
|
||||||
|
}
|
95
updates/main.go
Normal file
95
updates/main.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/database"
|
||||||
|
"github.com/Safing/portbase/modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
updateStoragePath string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
modules.Register("updates", prep, start, nil, "database")
|
||||||
|
}
|
||||||
|
|
||||||
|
func prep() error {
|
||||||
|
updateStoragePath = filepath.Join(database.GetDatabaseRoot(), "updates")
|
||||||
|
|
||||||
|
err := checkUpdateDirs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() error {
|
||||||
|
err := ReloadLatest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go updater()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() error {
|
||||||
|
return os.RemoveAll(filepath.Join(updateStoragePath, "tmp"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkUpdateDirs() error {
|
||||||
|
// all
|
||||||
|
err := checkDir(filepath.Join(updateStoragePath, "all"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// os_platform
|
||||||
|
err = checkDir(filepath.Join(updateStoragePath, fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// tmp
|
||||||
|
err = checkDir(filepath.Join(updateStoragePath, "tmp"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDir(dirPath string) error {
|
||||||
|
f, err := os.Stat(dirPath)
|
||||||
|
if err == nil {
|
||||||
|
// file exists
|
||||||
|
if f.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = os.Remove(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not remove file %s to place dir: %s", dirPath, err)
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(dirPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create dir %s: %s", dirPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// file does not exist
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(dirPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create dir %s: %s", dirPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// other error
|
||||||
|
return fmt.Errorf("failed to access %s: %s", dirPath, err)
|
||||||
|
}
|
88
updates/updater.go
Normal file
88
updates/updater.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package updates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func updater() {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
for {
|
||||||
|
err := checkForUpdates()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: failed to check for updates: %s", err)
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUpdates() error {
|
||||||
|
|
||||||
|
// download new index
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
for tries := 0; tries < 3; tries++ {
|
||||||
|
data, err = fetchData("stable.json", tries)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newStableUpdates := make(map[string]string)
|
||||||
|
err = json.Unmarshal(data, &newStableUpdates)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newStableUpdates) == 0 {
|
||||||
|
return errors.New("stable.json is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXINSTABLE: correct log line
|
||||||
|
log.Infof("updates: downloaded new update index: stable.json (alpha until we actually reach stable)")
|
||||||
|
|
||||||
|
// update existing files
|
||||||
|
log.Tracef("updates: updating existing files")
|
||||||
|
updatesLock.RLock()
|
||||||
|
for identifier, newVersion := range newStableUpdates {
|
||||||
|
oldVersion, ok := latestUpdates[identifier]
|
||||||
|
if ok && newVersion != oldVersion {
|
||||||
|
|
||||||
|
filePath := getVersionedPath(identifier, newVersion)
|
||||||
|
realFilePath := filepath.Join(updateStoragePath, filePath)
|
||||||
|
for tries := 0; tries < 3; tries++ {
|
||||||
|
err := fetchFile(realFilePath, filePath, tries)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed to update %s to %s: %s", identifier, newVersion, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatesLock.RUnlock()
|
||||||
|
log.Tracef("updates: finished updating existing files")
|
||||||
|
|
||||||
|
// update stable index
|
||||||
|
updatesLock.Lock()
|
||||||
|
stableUpdates = newStableUpdates
|
||||||
|
updatesLock.Unlock()
|
||||||
|
|
||||||
|
// save stable index
|
||||||
|
err = ioutil.WriteFile(filepath.Join(updateStoragePath, "stable.json"), data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("updates: failed to save new version of stable.json: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
1
updates/uptool/.gitignore
vendored
Normal file
1
updates/uptool/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
uptool
|
23
updates/uptool/root.go
Normal file
23
updates/uptool/root.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "uptool",
|
||||||
|
Short: "helper tool for the update process",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmd.Usage()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
35
updates/uptool/scan.go
Normal file
35
updates/uptool/scan.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Safing/portmaster/updates"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(scanCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanCmd = &cobra.Command{
|
||||||
|
Use: "scan",
|
||||||
|
Short: "Scan the current directory and print the result",
|
||||||
|
RunE: scan,
|
||||||
|
}
|
||||||
|
|
||||||
|
func scan(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
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue