package updates import ( "bytes" "io" "net/http" "os" "path/filepath" "strings" "github.com/ghodss/yaml" "github.com/safing/portmaster/base/api" "github.com/safing/portmaster/base/log" "github.com/safing/portmaster/base/utils" ) const ( apiPathCheckForUpdates = "updates/check" ) func registerAPIEndpoints() error { if err := api.RegisterEndpoint(api.Endpoint{ Name: "Check for Updates", Description: "Checks if new versions are available. If automatic updates are enabled, they are also downloaded and applied.", Parameters: []api.Parameter{{ Method: http.MethodPost, Field: "download", Value: "", Description: "Force downloading and applying of all updates, regardless of auto-update settings.", }}, Path: apiPathCheckForUpdates, Write: api.PermitUser, ActionFunc: func(r *api.Request) (msg string, err error) { // Check if we should also download regardless of settings. downloadAll := r.URL.Query().Has("download") // Trigger update task. err = TriggerUpdate(true, downloadAll) if err != nil { return "", err } // Report how we triggered. if downloadAll { return "downloading all updates...", nil } return "checking for updates...", nil }, }); err != nil { return err } if err := api.RegisterEndpoint(api.Endpoint{ Name: "Get Resource", Description: "Returns the requested resource from the udpate system", Path: `updates/get/{identifier:[A-Za-z0-9/\.\-_]{1,255}}`, Read: api.PermitUser, ReadMethod: http.MethodGet, HandlerFunc: func(w http.ResponseWriter, r *http.Request) { // Get identifier from URL. var identifier string if ar := api.GetAPIRequest(r); ar != nil { identifier = ar.URLVars["identifier"] } if identifier == "" { http.Error(w, "no resource speicified", http.StatusBadRequest) return } // Get resource. resource, err := registry.GetFile(identifier) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } // Open file for reading. file, err := os.Open(resource.Path()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer file.Close() //nolint:errcheck,gosec // Assign file to reader var reader io.Reader = file // Add version to header. w.Header().Set("Resource-Version", resource.Version()) // Set Content-Type. contentType, _ := utils.MimeTypeByExtension(filepath.Ext(resource.Path())) w.Header().Set("Content-Type", contentType) // Check if the content type may be returned. accept := r.Header.Get("Accept") if accept != "" { mimeTypes := strings.Split(accept, ",") // First, clean mime types. for i, mimeType := range mimeTypes { mimeType = strings.TrimSpace(mimeType) mimeType, _, _ = strings.Cut(mimeType, ";") mimeTypes[i] = mimeType } // Second, check if we may return anything. var acceptsAny bool for _, mimeType := range mimeTypes { switch mimeType { case "*", "*/*": acceptsAny = true } } // Third, check if we can convert. if !acceptsAny { var converted bool sourceType, _, _ := strings.Cut(contentType, ";") findConvertiblePair: for _, mimeType := range mimeTypes { switch { case sourceType == "application/yaml" && mimeType == "application/json": yamlData, err := io.ReadAll(reader) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } jsonData, err := yaml.YAMLToJSON(yamlData) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } reader = bytes.NewReader(jsonData) converted = true break findConvertiblePair } } // If we could not convert to acceptable format, return an error. if !converted { http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable) return } } } // Write file. w.WriteHeader(http.StatusOK) if r.Method != http.MethodHead { _, err = io.Copy(w, reader) if err != nil { log.Errorf("updates: failed to serve resource file: %s", err) return } } }, }); err != nil { return err } return nil }