Neale Pickett
·
2025-06-29
betsyd.go
1package main
2
3import (
4 "flag"
5 "fmt"
6 "log"
7 "net/http"
8 "os"
9 "path/filepath"
10 "time"
11)
12
13var CheckinWindow time.Duration
14var StateDirectory string
15var WebDirectory string
16var ListenAddress string
17
18func logfile(req *http.Request) string {
19 id := req.PathValue("id")
20 if id == "" {
21 return ""
22 }
23 if len(id) > 40 {
24 return ""
25 }
26 name := filepath.Clean("/" + id + ".log")
27 return filepath.Join(StateDirectory, name)
28}
29
30func checkin(w http.ResponseWriter, req *http.Request) {
31 now := time.Now()
32 stamp := now.Format(time.RFC3339)
33
34 name := logfile(req)
35 if name == "" {
36 http.Error(w, "Invalid ID", http.StatusBadRequest)
37 return
38 }
39
40 f, err := os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
41 if err != nil {
42 http.Error(w, err.Error(), http.StatusInternalServerError)
43 return
44 }
45 defer f.Close()
46
47 fmt.Fprintln(f, stamp)
48 fmt.Fprintln(w, stamp)
49}
50
51func status(w http.ResponseWriter, req *http.Request) {
52 now := time.Now()
53
54 name := logfile(req)
55 if name == "" {
56 http.Error(w, "Invalid ID", http.StatusBadRequest)
57 return
58 }
59
60 info, err := os.Stat(name)
61 if err != nil {
62 http.Error(w, err.Error(), http.StatusNotFound)
63 return
64 }
65
66 checkin := info.ModTime()
67 if checkin.Add(CheckinWindow).Before(now) {
68 w.WriteHeader(http.StatusNotFound)
69 fmt.Fprintln(w, "Checkin window expired", checkin)
70 return
71 }
72
73 fmt.Fprintln(w, "Checkin OK", checkin)
74}
75
76func main() {
77 flag.DurationVar(&CheckinWindow, "checkin", 24 * time.Hour, "Checkin interval")
78 flag.StringVar(&WebDirectory, "web", "web", "Path to static web content")
79 flag.StringVar(&StateDirectory, "state", "state", "Path to stored state")
80 flag.StringVar(&ListenAddress, "listen", ":8080", "Listen address")
81 flag.Parse()
82
83 http.HandleFunc("GET /state/{id}", status)
84 http.HandleFunc("POST /state/{id}", checkin)
85 http.Handle("GET /log/", http.StripPrefix("/log", http.FileServer(http.Dir(StateDirectory))))
86 http.Handle("/", http.FileServer(http.Dir(WebDirectory)))
87 log.Println("Listening on", ListenAddress)
88 log.Fatal(http.ListenAndServe(ListenAddress, nil))
89}