From 72c7592cdd23d4ba955a9bf9922fd343342c78a6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 31 Dec 2020 00:00:48 +0100 Subject: [PATCH] Update API endpoints and handlers --- core/api.go | 43 +++++++++++++ core/core.go | 4 ++ core/events.go | 14 ++-- firewall/api.go | 21 ++++-- resolver/main.go | 14 +++- resolver/namerecord.go | 16 ++++- ui/serve.go | 142 ++++++++++++++++++++++++----------------- 7 files changed, 182 insertions(+), 72 deletions(-) create mode 100644 core/api.go diff --git a/core/api.go b/core/api.go new file mode 100644 index 00000000..8029f638 --- /dev/null +++ b/core/api.go @@ -0,0 +1,43 @@ +package core + +import ( + "github.com/safing/portbase/api" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/updates" +) + +func registerActions() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "core/shutdown", + Read: api.PermitSelf, + ActionFn: shutdown, + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "core/restart", + Read: api.PermitSelf, + ActionFn: restart, + }); err != nil { + return err + } + + return nil +} + +// shutdown shuts the Portmaster down. +func shutdown(_ *api.Request) (msg string, err error) { + log.Warning("core: user requested shutdown via action") + // Do not use a worker, as this would block itself here. + go modules.Shutdown() //nolint:errcheck + return "shutdown initiated", nil +} + +// restart restarts the Portmaster. +func restart(_ *api.Request) (msg string, err error) { + log.Info("core: user requested restart via action") + updates.RestartNow() + return "restart initiated", nil +} diff --git a/core/core.go b/core/core.go index 886a8965..5fb7665b 100644 --- a/core/core.go +++ b/core/core.go @@ -55,6 +55,10 @@ func start() error { return err } + if err := registerActions(); err != nil { + return err + } + registerLogCleaner() return nil diff --git a/core/events.go b/core/events.go index 8971dc8a..608eb3c5 100644 --- a/core/events.go +++ b/core/events.go @@ -1,3 +1,5 @@ +// DEPRECATED: remove in v0.7 + package core import ( @@ -19,12 +21,12 @@ func registerEvents() { } func registerEventHooks() error { - err := module.RegisterEventHook("core", eventShutdown, "execute shutdown", shutdown) + err := module.RegisterEventHook("core", eventShutdown, "execute shutdown", shutdownHook) if err != nil { return err } - err = module.RegisterEventHook("core", eventRestart, "execute shutdown", restart) + err = module.RegisterEventHook("core", eventRestart, "execute shutdown", restartHook) if err != nil { return err } @@ -32,16 +34,16 @@ func registerEventHooks() error { return nil } -// shutdown shuts the Portmaster down. -func shutdown(ctx context.Context, _ interface{}) error { +// shutdownHook shuts the Portmaster down. +func shutdownHook(ctx context.Context, _ interface{}) error { log.Warning("core: user requested shutdown") // Do not use a worker, as this would block itself here. go modules.Shutdown() //nolint:errcheck return nil } -// restart restarts the Portmaster. -func restart(ctx context.Context, data interface{}) error { +// restartHook restarts the Portmaster. +func restartHook(ctx context.Context, data interface{}) error { log.Info("core: user requested restart") updates.RestartNow() return nil diff --git a/firewall/api.go b/firewall/api.go index 73832fbf..88c1307c 100644 --- a/firewall/api.go +++ b/firewall/api.go @@ -59,21 +59,24 @@ func startAPIAuth() { log.Tracef("filter: api port set to %d", apiPort) } -func apiAuthenticator(ctx context.Context, s *http.Server, r *http.Request) (err error) { +func apiAuthenticator(ctx context.Context, s *http.Server, r *http.Request) (token *api.AuthToken, err error) { if devMode() { - return nil + return &api.AuthToken{ + Read: api.PermitSelf, + Write: api.PermitSelf, + }, nil } // get local IP/Port localIP, localPort, err := parseHostPort(s.Addr) if err != nil { - return fmt.Errorf("failed to get local IP/Port: %s", err) + return nil, fmt.Errorf("failed to get local IP/Port: %s", err) } // get remote IP/Port remoteIP, remotePort, err := parseHostPort(r.RemoteAddr) if err != nil { - return fmt.Errorf("failed to get remote IP/Port: %s", err) + return nil, fmt.Errorf("failed to get remote IP/Port: %s", err) } log.Tracer(r.Context()).Tracef("filter: authenticating API request from %s", r.RemoteAddr) @@ -94,14 +97,20 @@ func apiAuthenticator(ctx context.Context, s *http.Server, r *http.Request) (err }, ) if !retry { - return err + break } // wait a little time.Sleep(250 * time.Millisecond) } + if err != nil { + return nil, err + } - return err + return &api.AuthToken{ + Read: api.PermitSelf, + Write: api.PermitSelf, + }, nil } func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bool, err error) { diff --git a/resolver/main.go b/resolver/main.go index cc0ebe62..543e4465 100644 --- a/resolver/main.go +++ b/resolver/main.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/safing/portbase/api" + "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portmaster/intel" @@ -76,12 +78,22 @@ func start() error { return err } + // Register api endpoint to clear DNS cache. + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "dns/clear/namecache", + Read: api.PermitUser, + ActionFn: clearNameCache, + }); err != nil { + return err + } + + // DEPRECATED: remove in v0.7 // cache clearing err = module.RegisterEventHook( "resolver", ClearNameCacheEvent, ClearNameCacheEvent, - clearNameCache, + clearNameCacheEventHandler, ) if err != nil { return err diff --git a/resolver/namerecord.go b/resolver/namerecord.go index d0faf14c..15ca2902 100644 --- a/resolver/namerecord.go +++ b/resolver/namerecord.go @@ -6,6 +6,7 @@ import ( "fmt" "sync" + "github.com/safing/portbase/api" "github.com/safing/portbase/database" "github.com/safing/portbase/database/query" "github.com/safing/portbase/database/record" @@ -104,7 +105,20 @@ func (rec *NameRecord) Save() error { return recordDatabase.PutNew(rec) } -func clearNameCache(ctx context.Context, _ interface{}) error { +// clearNameCache clears all dns caches from the database. +func clearNameCache(ar *api.Request) (msg string, err error) { + log.Warning("resolver: user requested dns cache clearing via action") + + n, err := recordDatabase.Purge(ar.Ctx(), query.New(nameRecordsKeyPrefix)) + if err != nil { + return "", err + } + + return fmt.Sprintf("cleared %d dns cache entries", n), nil +} + +// DEPRECATED: remove in v0.7 +func clearNameCacheEventHandler(ctx context.Context, _ interface{}) error { log.Debugf("resolver: dns cache clearing started...") n, err := recordDatabase.Purge(ctx, query.New(nameRecordsKeyPrefix)) if err != nil { diff --git a/ui/serve.go b/ui/serve.go index 5f11178d..1e054c81 100644 --- a/ui/serve.go +++ b/ui/serve.go @@ -24,72 +24,98 @@ var ( ) func registerRoutes() error { - api.RegisterHandleFunc("/assets/{resPath:[a-zA-Z0-9/\\._-]+}", ServeBundle("assets")).Methods("GET", "HEAD") - api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}", redirAddSlash).Methods("GET", "HEAD") - api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/", ServeBundle("")).Methods("GET", "HEAD") - api.RegisterHandleFunc("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", ServeBundle("")).Methods("GET", "HEAD") - api.RegisterHandleFunc("/", redirectToDefault) + // Server assets. + api.RegisterHandler( + "/assets/{resPath:[a-zA-Z0-9/\\._-]+}", + &bundleServer{defaultModuleName: "assets"}, + ) + + // Add slash to plain module namespaces. + api.RegisterHandler( + "/ui/modules/{moduleName:[a-z]+}", + api.WrapInAuthHandler(redirAddSlash, api.PermitAnyone, api.NotSupported), + ) + + // Serve modules. + srv := &bundleServer{} + api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/", srv) + api.RegisterHandler("/ui/modules/{moduleName:[a-z]+}/{resPath:[a-zA-Z0-9/\\._-]+}", srv) + + // Redirect "/" to default module. + api.RegisterHandler( + "/", + api.WrapInAuthHandler(redirectToDefault, api.PermitAnyone, api.NotSupported), + ) return nil } -// ServeBundle serves bundles. -func ServeBundle(defaultModuleName string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { +type bundleServer struct { + defaultModuleName string +} - // log.Tracef("ui: request for %s", r.RequestURI) +func (bs *bundleServer) ReadPermission(*http.Request) api.Permission { return api.PermitAnyone } - vars := api.GetMuxVars(r) - moduleName, ok := vars["moduleName"] - if !ok { - moduleName = defaultModuleName - if moduleName == "" { - http.Error(w, "missing module name", http.StatusBadRequest) - return - } - } +func (bs *bundleServer) WritePermission(*http.Request) api.Permission { return api.NotSupported } - resPath, ok := vars["resPath"] - if !ok || strings.HasSuffix(resPath, "/") { - resPath = "index.html" - } - - appsLock.RLock() - bundle, ok := apps[moduleName] - appsLock.RUnlock() - if ok { - ServeFileFromBundle(w, r, moduleName, bundle, resPath) - return - } - - // get file from update system - zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName)) - if err != nil { - if err == updater.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) - return - } - - bundle = &resources.BundleSequence{newBundle} - appsLock.Lock() - apps[moduleName] = bundle - appsLock.Unlock() - - ServeFileFromBundle(w, r, moduleName, bundle, resPath) +func (bs *bundleServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Get request context. + ar := api.GetAPIRequest(r) + if ar == nil { + log.Errorf("ui: missing api request context") + http.Error(w, "Internal server error.", http.StatusInternalServerError) + return } + + moduleName, ok := ar.URLVars["moduleName"] + if !ok { + moduleName = bs.defaultModuleName + if moduleName == "" { + http.Error(w, "missing module name", http.StatusBadRequest) + return + } + } + + resPath, ok := ar.URLVars["resPath"] + if !ok || strings.HasSuffix(resPath, "/") { + resPath = "index.html" + } + + appsLock.RLock() + bundle, ok := apps[moduleName] + appsLock.RUnlock() + if ok { + ServeFileFromBundle(w, r, moduleName, bundle, resPath) + return + } + + // get file from update system + zipFile, err := updates.GetFile(fmt.Sprintf("ui/modules/%s.zip", moduleName)) + if err != nil { + if err == updater.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) + return + } + + bundle = &resources.BundleSequence{newBundle} + appsLock.Lock() + apps[moduleName] = bundle + appsLock.Unlock() + + ServeFileFromBundle(w, r, moduleName, bundle, resPath) } // ServeFileFromBundle serves a file from the given bundle.