/* Copyright (c) 2015-2016 Christopher Young Distributable under the terms of The "BSD New"" License that can be found in the LICENSE file, herein included as part of this header. managementinterface.go: Web interfaces (JSON and websocket), web server for web interface HTML. */ package main import ( "encoding/hex" "encoding/json" "fmt" humanize "github.com/dustin/go-humanize" "golang.org/x/net/websocket" "io" "io/ioutil" "log" "net/http" "os" "os/exec" "strings" "syscall" "text/template" "time" ) type SettingMessage struct { Setting string `json:"setting"` Value bool `json:"state"` } // Weather updates channel. var weatherUpdate *uibroadcaster var trafficUpdate *uibroadcaster /* The /weather websocket starts off by sending the current buffer of weather messages, then sends updates as they are received. */ func handleWeatherWS(conn *websocket.Conn) { // Subscribe the socket to receive updates. weatherUpdate.AddSocket(conn) // Connection closes when function returns. Since uibroadcast is writing and we don't need to read anything (for now), just keep it busy. for { buf := make([]byte, 1024) _, err := conn.Read(buf) if err != nil { break } if buf[0] != 0 { // Dummy. continue } time.Sleep(1 * time.Second) } } // Works just as weather updates do. func handleTrafficWS(conn *websocket.Conn) { trafficMutex.Lock() for _, traf := range traffic { if !traf.Position_valid { // Don't send unless a valid position exists. continue } trafficJSON, _ := json.Marshal(&traf) conn.Write(trafficJSON) } // Subscribe the socket to receive updates. trafficUpdate.AddSocket(conn) trafficMutex.Unlock() // Connection closes when function returns. Since uibroadcast is writing and we don't need to read anything (for now), just keep it busy. for { buf := make([]byte, 1024) _, err := conn.Read(buf) if err != nil { break } if buf[0] != 0 { // Dummy. continue } time.Sleep(1 * time.Second) } } func handleStatusWS(conn *websocket.Conn) { // log.Printf("Web client connected.\n") timer := time.NewTicker(1 * time.Second) for { // The below is not used, but should be if something needs to be streamed from the web client ever in the future. /* var msg SettingMessage err := websocket.JSON.Receive(conn, &msg) if err == io.EOF { break } else if err != nil { log.Printf("handleStatusWS: %s\n", err.Error()) } else { // Use 'msg'. } */ // Send status. <-timer.C update, _ := json.Marshal(&globalStatus) _, err := conn.Write(update) if err != nil { // log.Printf("Web client disconnected.\n") break } } } func handleSituationWS(conn *websocket.Conn) { timer := time.NewTicker(100 * time.Millisecond) for { <-timer.C situationJSON, _ := json.Marshal(&mySituation) _, err := conn.Write(situationJSON) if err != nil { break } } } // AJAX call - /getStatus. Responds with current global status // a webservice call for the same data available on the websocket but when only a single update is needed func handleStatusRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) statusJSON, _ := json.Marshal(&globalStatus) fmt.Fprintf(w, "%s\n", statusJSON) } // AJAX call - /getSituation. Responds with current situation (lat/lon/gdspeed/track/pitch/roll/heading/etc.) func handleSituationRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) situationJSON, _ := json.Marshal(&mySituation) fmt.Fprintf(w, "%s\n", situationJSON) } // AJAX call - /getTowers. Responds with all ADS-B ground towers that have sent messages that we were able to parse, along with its stats. func handleTowersRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) towersJSON, err := json.Marshal(&ADSBTowers) if err != nil { log.Printf("Error sending tower JSON data: %s\n", err.Error()) } // for testing purposes, we can return a fixed reply // towersJSON = []byte(`{"(38.490880,-76.135554)":{"Lat":38.49087953567505,"Lng":-76.13555431365967,"Signal_strength_last_minute":100,"Signal_strength_max":67,"Messages_last_minute":1,"Messages_total":1059},"(38.978698,-76.309276)":{"Lat":38.97869825363159,"Lng":-76.30927562713623,"Signal_strength_last_minute":495,"Signal_strength_max":32,"Messages_last_minute":45,"Messages_total":83},"(39.179285,-76.668413)":{"Lat":39.17928457260132,"Lng":-76.66841268539429,"Signal_strength_last_minute":50,"Signal_strength_max":24,"Messages_last_minute":1,"Messages_total":16},"(39.666309,-74.315300)":{"Lat":39.66630935668945,"Lng":-74.31529998779297,"Signal_strength_last_minute":9884,"Signal_strength_max":35,"Messages_last_minute":4,"Messages_total":134}}`) fmt.Fprintf(w, "%s\n", towersJSON) } // AJAX call - /getSettings. Responds with all stratux.conf data. func handleSettingsGetRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) settingsJSON, _ := json.Marshal(&globalSettings) fmt.Fprintf(w, "%s\n", settingsJSON) } // AJAX call - /setSettings. receives via POST command, any/all stratux.conf data. func handleSettingsSetRequest(w http.ResponseWriter, r *http.Request) { // define header in support of cross-domain AJAX setNoCache(w) setJSONHeaders(w) w.Header().Set("Access-Control-Allow-Method", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") // for an OPTION method request, we return header without processing. // this insures we are recognized as supporting cross-domain AJAX REST calls if r.Method == "POST" { // raw, _ := httputil.DumpRequest(r, true) // log.Printf("handleSettingsSetRequest:raw: %s\n", raw) decoder := json.NewDecoder(r.Body) for { var msg map[string]interface{} // support arbitrary JSON err := decoder.Decode(&msg) if err == io.EOF { break } else if err != nil { log.Printf("handleSettingsSetRequest:error: %s\n", err.Error()) } else { for key, val := range msg { // log.Printf("handleSettingsSetRequest:json: testing for key:%s of type %s\n", key, reflect.TypeOf(val)) switch key { case "UAT_Enabled": globalSettings.UAT_Enabled = val.(bool) case "ES_Enabled": globalSettings.ES_Enabled = val.(bool) case "GPS_Enabled": globalSettings.GPS_Enabled = val.(bool) case "AHRS_Enabled": globalSettings.AHRS_Enabled = val.(bool) case "DEBUG": globalSettings.DEBUG = val.(bool) case "ReplayLog": v := val.(bool) if v != globalSettings.ReplayLog { // Don't mark the files unless there is a change. globalSettings.ReplayLog = v } case "PPM": globalSettings.PPM = int(val.(float64)) case "WatchList": globalSettings.WatchList = val.(string) case "OwnshipModeS": // Expecting a hex string less than 6 characters (24 bits) long. if len(val.(string)) > 6 { // Too long. continue } // Pad string, must be 6 characters long. vals := strings.ToUpper(val.(string)) for len(vals) < 6 { vals = "0" + vals } hexn, err := hex.DecodeString(vals) if err != nil { // Number not valid. log.Printf("handleSettingsSetRequest:OwnshipModeS: %s\n", err.Error()) continue } globalSettings.OwnshipModeS = fmt.Sprintf("%02X%02X%02X", hexn[0], hexn[1], hexn[2]) default: log.Printf("handleSettingsSetRequest:json: unrecognized key:%s\n", key) } } saveSettings() } } // while it may be redundent, we return the latest settings settingsJSON, _ := json.Marshal(&globalSettings) fmt.Fprintf(w, "%s\n", settingsJSON) } } func handleShutdownRequest(w http.ResponseWriter, r *http.Request) { syscall.Sync() syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF) } func doReboot() { syscall.Sync() syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART) } func handleRebootRequest(w http.ResponseWriter, r *http.Request) { doReboot() } // AJAX call - /getClients. Responds with all connected clients. func handleClientsGetRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) clientsJSON, _ := json.Marshal(&outSockets) fmt.Fprintf(w, "%s\n", clientsJSON) } func delayReboot() { time.Sleep(1 * time.Second) doReboot() } // Upload an update file. func handleUpdatePostRequest(w http.ResponseWriter, r *http.Request) { setNoCache(w) setJSONHeaders(w) r.ParseMultipartForm(1024 * 1024 * 32) // ~32MB update. file, handler, err := r.FormFile("update_file") if err != nil { log.Printf("Update failed from %s (%s).\n", r.RemoteAddr, err.Error()) return } defer file.Close() // Special hardware builds. Don't allow an update unless the filename contains the hardware build name. if (len(globalStatus.HardwareBuild) > 0) && !strings.Contains(handler.Filename, globalStatus.HardwareBuild) { w.WriteHeader(404) return } updateFile := fmt.Sprintf("/root/%s", handler.Filename) f, err := os.OpenFile(updateFile, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { log.Printf("Update failed from %s (%s).\n", r.RemoteAddr, err.Error()) return } defer f.Close() io.Copy(f, file) log.Printf("%s uploaded %s for update.\n", r.RemoteAddr, updateFile) // Successful update upload. Now reboot. go delayReboot() } func setNoCache(w http.ResponseWriter) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") } func setJSONHeaders(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Type", "application/json") } func defaultServer(w http.ResponseWriter, r *http.Request) { // setNoCache(w) http.FileServer(http.Dir("/var/www")).ServeHTTP(w, r) } func handleroPartitionRebuild(w http.ResponseWriter, r *http.Request) { out, err := exec.Command("/usr/sbin/rebuild_ro_part.sh").Output() var ret_err error if err != nil { ret_err = fmt.Errorf("Rebuild RO Partition error: %s", err.Error()) } else { ret_err = fmt.Errorf("Rebuild RO Partition success: %s", out) } addSystemError(ret_err) } // https://gist.github.com/alexisrobert/982674. // Copyright (c) 2010-2014 Alexis ROBERT . const dirlisting_tpl = ` Index of {{.Name}}

Index of {{.Name}}

{{range .Children_files}} {{end}}
NameLast ModifiedSize (bytes)Options
{{.Name}}{{.Mtime}}{{.Size}}Download
{{.ServerUA}}
` type fileInfo struct { Name string Mtime string Size string } // Manages directory listings type dirlisting struct { Name string Children_files []fileInfo ServerUA string } func viewLogs(w http.ResponseWriter, r *http.Request) { names, err := ioutil.ReadDir("/var/log/stratux/") if err != nil { return } fi := make([]fileInfo, 0) for _, val := range names { if val.Name()[0] == '.' { continue } // Remove hidden files from listing if !val.IsDir() { mtime := val.ModTime().Format("2006-Jan-02 15:04:05") sz := humanize.Comma(val.Size()) fi = append(fi, fileInfo{Name: val.Name(), Mtime: mtime, Size: sz}) } } tpl, err := template.New("tpl").Parse(dirlisting_tpl) if err != nil { return } data := dirlisting{Name: r.URL.Path, ServerUA: "Stratux " + stratuxVersion + "/" + stratuxBuild, Children_files: fi} err = tpl.Execute(w, data) if err != nil { log.Printf("viewLogs() error: %s\n", err.Error()) } } func managementInterface() { weatherUpdate = NewUIBroadcaster() trafficUpdate = NewUIBroadcaster() http.HandleFunc("/", defaultServer) http.Handle("/logs/", http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log")))) http.HandleFunc("/view_logs/", viewLogs) http.HandleFunc("/status", func(w http.ResponseWriter, req *http.Request) { s := websocket.Server{ Handler: websocket.Handler(handleStatusWS)} s.ServeHTTP(w, req) }) http.HandleFunc("/situation", func(w http.ResponseWriter, req *http.Request) { s := websocket.Server{ Handler: websocket.Handler(handleSituationWS)} s.ServeHTTP(w, req) }) http.HandleFunc("/weather", func(w http.ResponseWriter, req *http.Request) { s := websocket.Server{ Handler: websocket.Handler(handleWeatherWS)} s.ServeHTTP(w, req) }) http.HandleFunc("/traffic", func(w http.ResponseWriter, req *http.Request) { s := websocket.Server{ Handler: websocket.Handler(handleTrafficWS)} s.ServeHTTP(w, req) }) http.HandleFunc("/getStatus", handleStatusRequest) http.HandleFunc("/getSituation", handleSituationRequest) http.HandleFunc("/getTowers", handleTowersRequest) http.HandleFunc("/getSettings", handleSettingsGetRequest) http.HandleFunc("/setSettings", handleSettingsSetRequest) http.HandleFunc("/shutdown", handleShutdownRequest) http.HandleFunc("/reboot", handleRebootRequest) http.HandleFunc("/getClients", handleClientsGetRequest) http.HandleFunc("/updateUpload", handleUpdatePostRequest) http.HandleFunc("/roPartitionRebuild", handleroPartitionRebuild) err := http.ListenAndServe(managementAddr, nil) if err != nil { log.Printf("managementInterface ListenAndServe: %s\n", err.Error()) } }