/* * Companion server GPSLogger Android app[0]. * * usage: gpxtrack [-u USER] -r DIR -l SOCKET * * In GPSLogger, create a profile saving data to a custom URL, using * method POST and configured as so: * * $URL/log?name=%DESC&lon=%LON&lat=%LAT&ele=%ALT&time=%TIME * * The server will log all GPS points received on /log to a GPX * file, one per day, in $rootdir/static/gpx * * The index.html.tmpl can then be used to list GPX files stored * on the server. Anything else will be served as-is from $rootdir/static/. * * Here is an example index.html.tmpl using leaflet and leaflet-gpx: * * * * * * * * * * *
* * * * * [0]: https://f-droid.org/packages/com.mendhak.gpslogger * * by wgs */ package main import ( "flag" "html/template" "io/ioutil" "log" "net" "net/http" "net/http/fcgi" "os" "os/signal" "os/user" "strconv" "syscall" "time" "github.com/tkrajina/gpxgo/gpx" ) type webserver struct { filedir string // Static assets datadir string // GPX directory tmpldir string // Templates } var verbose bool var socket string var www webserver func privDrop(username string, socket net.Listener) error { u, err := user.Lookup(username) if err != nil { return err } uid, _ := strconv.Atoi(u.Uid) gid, _ := strconv.Atoi(u.Gid) /* Change owner in case of unix sockets */ if socket.Addr().Network() == "unix" { os.Chown(socket.Addr().String(), uid, gid) } if verbose { log.Printf("Dropping privileges to %s (%d,%d)", username, uid, gid) } syscall.Setuid(uid) syscall.Setgid(gid) return nil } /* Check wether a specific point (timestamp) exists in the given GPX file */ func hasGpxPoint(g *gpx.GPX, p *gpx.GPXPoint) bool { found := false for _, track := range g.Tracks { for _, seg := range track.Segments { for _, point := range seg.Points { if point.Timestamp == p.Timestamp { found = true } } } } return found } func loadGPX(path string) *gpx.GPX { gpxfile, err := gpx.ParseFile(path) if err != nil { /* Create a new GPX instance */ newgpx := gpx.GPX{ Version: "1.1", Creator: "gpxtrack", } return &newgpx } return gpxfile } func saveGPX(g *gpx.GPX) { path := time.Now().Format(www.datadir + "/2006-01-02.gpx") bytes, err := g.ToXml(gpx.ToXmlParams{Indent: true}) if err != nil { log.Fatal(err) } err = os.WriteFile(path, bytes, 0644) if err != nil { log.Fatal(err) } } func logGPX(w http.ResponseWriter, r *http.Request) { var p gpx.GPXPoint if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } gpx := loadGPX(time.Now().Format(www.datadir + "/2006-01-02.gpx")) r.ParseForm() p.Name = r.Form.Get("name") p.Timestamp, _ = time.Parse("2006-01-02T15:04:05.000Z", r.Form.Get("time")) p.Point.Longitude, _ = strconv.ParseFloat(r.Form.Get("lon"), 5) p.Point.Latitude, _ = strconv.ParseFloat(r.Form.Get("lat"), 5) ele, _ := strconv.ParseFloat(r.Form.Get("ele"), 2) p.Point.Elevation.SetValue(ele) /* Skip already logged points (based on timestamp) */ if hasGpxPoint(gpx, &p) { return } if verbose { log.Printf("New GPS trackpoint: %f,%f (%s)", p.Point.Longitude, p.Point.Latitude, p.Timestamp) } gpx.Name = p.Name // Name GPX after latest point gpx.AppendPoint(&p) saveGPX(gpx) } func indexPage(w http.ResponseWriter, r *http.Request) { var d []string files, err := ioutil.ReadDir(www.datadir) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) log.Print(err) return } t, err := template.ParseFiles(www.tmpldir + "/index.html.tmpl") if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) log.Print(err) return } /* Insert files in reverse name order, to list latest GPX first */ for _, v := range files { d = append([]string{v.Name()}, d...) } err = t.Execute(w, d) if err != nil { log.Print(err) } } func serveHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodDelete: if r.URL.Path[0:5] == "/gpx/" { err := os.Remove(www.filedir + r.URL.Path) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) log.Print(err) } } else { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } case http.MethodGet: if r.URL.Path == "/" || r.URL.Path == "index.html" { indexPage(w, r) } else { http.ServeFile(w, r, www.filedir + "/" + r.URL.Path) } default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func main() { var err error var listener net.Listener var socket string var root string var chroot string var user string flag.BoolVar(&verbose, "v", false, "Verbose logging") flag.StringVar(&socket, "l", "0.0.0.0:8080", "Address to listen to") flag.StringVar(&root, "r", "/var/www/htdocs", "Root directory") flag.StringVar(&chroot, "c", "", "Directory to chroot into") flag.StringVar(&user, "u", "", "User to run as") flag.Parse() // Setup working directories relative to the root dir www.tmpldir = root www.filedir = root + "/static" www.datadir = www.filedir + "/gpx" if chroot != "" { if verbose { log.Printf("Changing root to %s", chroot); } syscall.Chroot(chroot) } // Good enough socket detection check if socket[0] == '/' { /* Remove any stale socket */ os.Remove(socket) if listener, err = net.Listen("unix", socket); err != nil { log.Fatal(err) } defer listener.Close() /* * Ensure unix socket is removed on exit. * Note: this might not work when dropping privileges… */ defer os.Remove(socket) sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, os.Kill, syscall.SIGTERM) go func() { _ = <-sigs listener.Close() if err = os.Remove(socket); err != nil { log.Fatal(err) } os.Exit(0) }() } else { if listener, err = net.Listen("tcp", socket); err != nil { log.Fatal(err) } defer listener.Close() } /* Drop privileges to the given user (if any) */ if user != "" { if privDrop(user, listener) != nil { log.Fatal("Cannot drop privilege to %s", user) } } http.HandleFunc("/", serveHTTP) http.HandleFunc("/log", logGPX) if verbose { log.Printf("Listening on %s", socket) } if listener.Addr().Network() == "unix" { err = fcgi.Serve(listener, nil) log.Fatal(err) /* NOTREACHED */ } err = http.Serve(listener, nil) log.Fatal(err) /* NOTREACHED */ }