diff --git a/gosuper/gosuper/api.go b/gosuper/gosuper/api.go index abd3fa41..5a56e8f1 100644 --- a/gosuper/gosuper/api.go +++ b/gosuper/gosuper/api.go @@ -4,31 +4,18 @@ import ( "encoding/json" "fmt" "log" - "net" "net/http" - "os" - "regexp" - "strconv" "time" "resin-supervisor/gosuper/systemd" ) -// Compile the expression once, usually at init time. -// Use raw strings to avoid having to quote the backslashes. -var dockerMatch = regexp.MustCompile(`(balena[0-9]+)|(docker[0-9]+)|(rce[0-9]+)|(tun[0-9]+)|(resin-vpn)|(resin-dns)`) - // APIResponse The api response sent from go supervisor type APIResponse struct { Data interface{} Error string } -//PurgeBody struct for the ApplicationId interfact -type PurgeBody struct { - ApplicationId interface{} -} - // VPNBody interface for post request received by VPN control end point type VPNBody struct { Enable bool @@ -57,24 +44,6 @@ func parseJSONBody(destination interface{}, request *http.Request) error { return decoder.Decode(&destination) } -func parsePurgeBody(request *http.Request) (appId string, err error) { - var body PurgeBody - if err = parseJSONBody(&body, request); err != nil { - return - } - switch v := body.ApplicationId.(type) { - case string: - appId = v - case float64: - if v != 0 { - appId = strconv.Itoa(int(v)) - } - default: - log.Printf("Invalid appId type %T\n", v) - } - return -} - func responseSenders(writer http.ResponseWriter) (sendResponse func(interface{}, string, int), sendError func(error)) { sendResponse = func(data interface{}, errorMsg string, statusCode int) { jsonResponse(writer, APIResponse{data, errorMsg}, statusCode) @@ -85,35 +54,6 @@ func responseSenders(writer http.ResponseWriter) (sendResponse func(interface{}, return } -// PurgeHandler Purges the data of the appID's application in the /data partition -func PurgeHandler(writer http.ResponseWriter, request *http.Request) { - log.Println("Purging /data") - sendResponse, sendError := responseSenders(writer) - sendBadRequest := func(errorMsg string) { - sendResponse("Error", errorMsg, http.StatusBadRequest) - } - - if appId, err := parsePurgeBody(request); err != nil { - sendBadRequest("Invalid request") - } else if appId == "" { - sendBadRequest("applicationId is required") - } else if !IsValidAppId(appId) { - sendBadRequest(fmt.Sprintf("Invalid applicationId '%s'", appId)) - } else if _, err = os.Stat(ResinDataPath + appId); err != nil { - if os.IsNotExist(err) { - sendResponse("Error", fmt.Sprintf("Invalid applicationId '%s': Directory does not exist", appId), http.StatusNotFound) - } else { - sendError(err) - } - } else if err = os.RemoveAll(ResinDataPath + appId); err != nil { - sendError(err) - } else if err = os.Mkdir(ResinDataPath+appId, 0755); err != nil { - sendError(err) - } else { - sendResponse("OK", "", http.StatusOK) - } -} - func inASecond(theFunc func()) { time.Sleep(time.Duration(time.Second)) theFunc() @@ -145,55 +85,6 @@ func ShutdownHandler(writer http.ResponseWriter, request *http.Request) { go inASecond(func() { systemd.Logind.PowerOff(false) }) } -// This function returns all active IPs of the interfaces that arent docker/rce and loopback -func ipAddress() (ipAddresses []string, err error) { - ifaces, err := net.Interfaces() - if err != nil { - return ipAddresses, err - } - for _, iface := range ifaces { - if (iface.Flags&net.FlagUp == 0) || (iface.Flags&net.FlagLoopback != 0) || dockerMatch.MatchString(iface.Name) { - continue // Interface down or Interface is loopback or Interface is a docker IP - } - addrs, err := iface.Addrs() - if err != nil { - return ipAddresses, err - } - for _, addr := range addrs { - var ip net.IP - switch v := addr.(type) { - case *net.IPNet: - ip = v.IP - case *net.IPAddr: - ip = v.IP - default: - log.Printf("Warning: Unrecognised type %T\n", v) - continue - } - if ip == nil { - continue - } - if ip = ip.To4(); ip == nil { - continue // This isnt an IPv4 Addresss - } - ipAddresses = append(ipAddresses, ip.String()) - } - } - return -} - -//IPAddressHandler is used to reply back with an array of the IPaddress used by the system. -func IPAddressHandler(writer http.ResponseWriter, request *http.Request) { - sendResponse, sendError := responseSenders(writer) - if ipAddr, err := ipAddress(); err != nil { - sendError(err) - } else { - payload := make(map[string][]string) - payload["IPAddresses"] = ipAddr - sendResponse(payload, "", http.StatusOK) - } -} - //VPNControl is used to control VPN service status with dbus func VPNControl(writer http.ResponseWriter, request *http.Request) { sendResponse, sendError := responseSenders(writer) @@ -224,11 +115,76 @@ func VPNControl(writer http.ResponseWriter, request *http.Request) { sendResponse("OK", "", http.StatusAccepted) } +func getUnitStatus(unitName string) (state bool, err error) { + if systemd.Dbus == nil { + err = fmt.Errorf("Systemd dbus unavailable, cannot get unit status.") + return + } + if activeState, e := systemd.Dbus.GetUnitProperty(unitName, "ActiveState"); e != nil { + err = fmt.Errorf("Unable to get unit status: %v", e) + return + } else { + state = activeState.Value.String() == `"active"` + return + } +} + +func unitStatusHandler(serviceName string, writer http.ResponseWriter, request *http.Request) { + sendResponse, sendError := responseSenders(writer) + if status, err := getUnitStatus(serviceName); err != nil { + sendError(fmt.Errorf("Unable to get VPN status: %v", err)) + return + } else { + sendResponse(status, "", http.StatusOK) + } +} + +func VPNStatus(writer http.ResponseWriter, request *http.Request) { + unitStatusHandler("openvpn-resin.service", writer, request) +} + +func logToDisplayServiceName() (serviceName string, err error) { + serviceName = "resin-info@tty1.service" + serviceNameOld := "tty-replacement.service" + if systemd.Dbus == nil { + err = fmt.Errorf("Systemd dbus unavailable, cannot get log to display service.") + return + } + if loaded, e := systemd.Dbus.GetUnitProperty(serviceName, "LoadState"); e != nil { + err = fmt.Errorf("Unable to get log to display load status: %v", e) + return + } else if loaded.Value.String() == `"not-found"` { + // If the resin-info service is not found, we're on an older OS + // which uses a different service name + serviceName = serviceNameOld + } + if loaded, e := systemd.Dbus.GetUnitProperty(serviceName, "LoadState"); e != nil { + err = fmt.Errorf("Unable to get log to display load status: %v", e) + return + } else if loaded.Value.String() == `"not-found"` { + // We might be in a different OS that just doesn't have the service + serviceName = "" + return + } + return +} + +func LogToDisplayStatus(writer http.ResponseWriter, request *http.Request) { + sendResponse, sendError := responseSenders(writer) + serviceName, err := logToDisplayServiceName() + if err != nil { + sendError(err) + return + } else if serviceName == "" { + sendResponse("Error", "Not found", http.StatusNotFound) + return + } + unitStatusHandler(serviceName, writer, request) +} + //LogToDisplayControl is used to control tty-replacement service status with dbus func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) { sendResponse, sendError := responseSenders(writer) - serviceName := "resin-info@tty1.service" - serviceNameOld := "tty-replacement.service" var body LogToDisplayBody if err := parseJSONBody(&body, request); err != nil { log.Println(err) @@ -241,20 +197,19 @@ func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) { return } - if loaded, err := systemd.Dbus.GetUnitProperty(serviceName, "LoadState"); err != nil { - sendError(fmt.Errorf("Unable to get log to display load status: %v", err)) + serviceName, err := logToDisplayServiceName() + if err != nil { + sendError(err) + return + } else if serviceName == "" { + sendResponse("Error", "Not found", http.StatusNotFound) return - } else if loaded.Value.String() == `"not-found"` { - // If the resin-info service is not found, we're on an older OS - // which uses a different service name - serviceName = serviceNameOld } - if activeState, err := systemd.Dbus.GetUnitProperty(serviceName, "ActiveState"); err != nil { + if status, err := getUnitStatus(serviceName); err != nil { sendError(fmt.Errorf("Unable to get log to display status: %v", err)) return } else { - status := activeState.Value.String() == `"active"` enable := body.Enable if status == enable { // Nothing to do, return Data = false to signal nothing was changed diff --git a/gosuper/gosuper/main.go b/gosuper/gosuper/main.go index 579fe2da..b62aaae0 100644 --- a/gosuper/gosuper/main.go +++ b/gosuper/gosuper/main.go @@ -20,12 +20,12 @@ func setupApi(router *mux.Router) { }) apiv1 := router.PathPrefix("/v1").Subrouter() - apiv1.HandleFunc("/ipaddr", IPAddressHandler).Methods("GET") - apiv1.HandleFunc("/purge", PurgeHandler).Methods("POST") apiv1.HandleFunc("/reboot", RebootHandler).Methods("POST") apiv1.HandleFunc("/shutdown", ShutdownHandler).Methods("POST") apiv1.HandleFunc("/vpncontrol", VPNControl).Methods("POST") - apiv1.HandleFunc("/set-log-to-display", LogToDisplayControl).Methods("POST") + apiv1.HandleFunc("/vpncontrol", VPNStatus).Methods("GET") + apiv1.HandleFunc("/log-to-display", LogToDisplayControl).Methods("POST") + apiv1.HandleFunc("/log-to-display", LogToDisplayStatus).Methods("GET") apiv1.HandleFunc("/restart-service", RestartService).Methods("POST") } diff --git a/gosuper/gosuper/main_test.go b/gosuper/gosuper/main_test.go index 70289c09..b8d03e3c 100644 --- a/gosuper/gosuper/main_test.go +++ b/gosuper/gosuper/main_test.go @@ -1,69 +1,10 @@ package main import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" "strings" "testing" ) -var purgeTests = []struct { - JsonInput string - AppId string - JsonResponse string - IsSuccess bool - HttpStatus int -}{ - {`{"applicationId": "1"}`, "1", `{"Data":"OK","Error":""}`, true, http.StatusOK}, - {`{"applicationId": 1}`, "1", `{"Data":"OK","Error":""}`, true, http.StatusOK}, - {`{"applicationId": "hi"}`, "1", `{"Data":"Error","Error":"Invalid applicationId 'hi'"}`, false, http.StatusBadRequest}, - {`{"applicationId": "2"}`, "1", `{"Data":"Error","Error":"Invalid applicationId '2': Directory does not exist"}`, false, http.StatusNotFound}, - {`{}`, "1", `{"Data":"Error","Error":"applicationId is required"}`, false, http.StatusBadRequest}, -} - -func TestPurge(t *testing.T) { - for i, testCase := range purgeTests { - t.Logf("Testing Purge case #%d", i) - request, err := http.NewRequest("POST", "/v1/purge", strings.NewReader(testCase.JsonInput)) - if err != nil { - t.Fatal(err) - } - request.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") - writer := httptest.NewRecorder() - ResinDataPath = "test-data/" - dataPath := ResinDataPath + testCase.AppId - testFile := dataPath + "/test" - - if err = os.MkdirAll(dataPath, 0755); err != nil { - t.Fatal("Could not create test directory for purge") - } else if err = ioutil.WriteFile(testFile, []byte("test"), 0777); err != nil { - t.Fatal("Could not create test file for purge") - } - - PurgeHandler(writer, request) - - if writer.Code != testCase.HttpStatus { - t.Errorf("Purge didn't return %v, got %v", testCase.HttpStatus, writer.Code) - } - if !strings.EqualFold(writer.Body.String(), testCase.JsonResponse) { - t.Errorf(`Purge response didn't match the expected JSON, expected "%s" got: "%s"`, testCase.JsonResponse, writer.Body.String()) - } - - if dirContents, err := ioutil.ReadDir(dataPath); err != nil { - t.Errorf("Could not read the data path after purge: %s", err) - } else { - fileCount := len(dirContents) - if fileCount > 0 && testCase.IsSuccess { - t.Error("Data directory not empty after purge") - } else if fileCount == 0 && !testCase.IsSuccess { - t.Error("Data directory empty after purge (but it failed)") - } - } - } -} - func TestReadConfig(t *testing.T) { if config, err := ReadConfig("config_for_test.json"); err != nil { t.Error(err)