diff options
| author | Mitch Riedstra <mitch@riedstra.us> | 2021-03-04 19:44:02 -0500 |
|---|---|---|
| committer | Mitch Riedstra <mitch@riedstra.us> | 2021-03-04 19:48:27 -0500 |
| commit | b9bb17044a8c2b47c7e96660e27ab645f82bec9d (patch) | |
| tree | 6c5bff2c5eaaebfc1ce9b01119308dcc39a75253 | |
| parent | 3b6f5647b0689abf04be73c3cf00297051753435 (diff) | |
| download | steam-export-b9bb17044a8c2b47c7e96660e27ab645f82bec9d.tar.gz steam-export-b9bb17044a8c2b47c7e96660e27ab645f82bec9d.tar.xz | |
Further refactoring.
| -rw-r--r-- | cmd/web/app.go | 75 | ||||
| -rw-r--r-- | cmd/web/delete.go | 47 | ||||
| -rw-r--r-- | cmd/web/download.go | 61 | ||||
| -rw-r--r-- | cmd/web/flagSliceString.go | 12 | ||||
| -rw-r--r-- | cmd/web/handlers.go | 207 | ||||
| -rw-r--r-- | cmd/web/index.go | 39 | ||||
| -rw-r--r-- | cmd/web/install.go | 52 | ||||
| -rw-r--r-- | cmd/web/main.go | 171 | ||||
| -rw-r--r-- | cmd/web/serve-self.go | 27 | ||||
| -rw-r--r-- | cmd/web/util.go | 89 |
10 files changed, 377 insertions, 403 deletions
diff --git a/cmd/web/app.go b/cmd/web/app.go new file mode 100644 index 0000000..8f6df89 --- /dev/null +++ b/cmd/web/app.go @@ -0,0 +1,75 @@ +package main + +import ( + "sync" + "html/template" + "time" + + "riedstra.dev/mitch/steam-export/steam" +) + +type steamLib struct { + steam.Library + sync.Mutex +} + +type statusInfo struct { + sync.RWMutex + Running bool + Error error + Url string + Transferred int64 + Size int64 + Start *time.Time +} + + +type App struct { + Library *steamLib + Status *statusInfo + + Templates *template.Template + + // Sending to this channel triggers the downloader in the background + download chan string +} + +func NewApp(libPath string) (*App, error) { + lib, err := steam.NewLibrary(libPath) + if err != nil { + return nil, err + } + + a := &App{ + Library: &steamLib{}, + Status: &statusInfo{}, + download: make(chan string), + } + + a.Library.Library = *lib + + a.Templates = template.Must(template.New("index").Parse(indexTemplate)) + + return a, nil +} + +func (a *App) LibrarySet(path string) { + Logger.Println("Starting library reload") + a.Library.Lock() + defer a.Library.Unlock() + var err error + l2, err := steam.NewLibrary(path) + if err != nil { + Logger.Printf("Error reopening lib: %s", err) + return + } + a.Library.Library = *l2 + Logger.Println("Done reloading lib") +} + +func (a *App) LibraryReload() { + cur := a.Library.Folder + a.LibrarySet(cur) + return +} + diff --git a/cmd/web/delete.go b/cmd/web/delete.go deleted file mode 100644 index 7fa003e..0000000 --- a/cmd/web/delete.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "fmt" - "net/http" -) - -func (a App) HandleDelete(w http.ResponseWriter, r *http.Request) { - if unauthorizedIfNotLocal(w, r) { - return - } - - err := r.ParseForm() - if err != nil { - Logger.Printf("Installer: While parsing form: %s", err) - http.Error(w, fmt.Sprintf("Invalid form: %s", err), 400) - return - } - - game := r.PostForm.Get("name") - - if game == "" { - Logger.Println("Deleter: No game specified") - http.Error(w, "Game param required", 400) - return - } - - a.Library.Lock() - g, ok := a.Library.Games[game] - a.Library.Unlock() - if !ok { - Logger.Printf("Missing: %s", game) - http.Error(w, "Game is missing", 404) - return - } - - err = g.Delete() - if err != nil { - Logger.Printf("Error removing game: %s", err) - http.Error(w, fmt.Sprintf("Error removing game: %s", err), 500) - return - } - Logger.Printf("Removed game: %s", game) - - a.LibraryReload() - http.Redirect(w, r, "/", 302) -} diff --git a/cmd/web/download.go b/cmd/web/download.go deleted file mode 100644 index 0238c8c..0000000 --- a/cmd/web/download.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "time" - - "github.com/gorilla/mux" -) - -func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - game := vars["game"] - - a.Library.Lock() - g, ok := a.Library.Games[game] - a.Library.Unlock() - if !ok { - Logger.Printf("Missing: %s", game) - http.Error(w, "Game is missing", 404) - return - } - - w.Header().Add("Content-type", "application/tar") - w.Header().Add("Estimated-size", fmt.Sprintf("%d", g.Size)) - - Logger.Printf("Client %s is downloading: %s", r.RemoteAddr, game) - - // Invert the writer so we can break up the copy and get progress - // information in here - rdr, pwrtr := io.Pipe() - go func() { - err := g.Package(pwrtr) - if err != nil { - Logger.Println("Error in package writing: ", err) - } - }() - - var total int64 - start := time.Now() - for { - n, err := io.CopyN(w, rdr, 256*1024*1024) - if err == io.EOF { - break - } - if err != nil { - Logger.Printf("Client %s Error Sending game: %s", r.RemoteAddr, err) - // Headers already sent, don't bother sending an error - return - } - total += n - mb := float64(total / 1024 / 1024) - rate := mb / time.Since(start).Seconds() - - Logger.Printf("Client %s is downloading %s: %0.1f%% done %.2f mb/s", - r.RemoteAddr, game, float64(total)/float64(g.Size)*100, rate) - } - - Logger.Printf("Client %s finished downloading: %s", r.RemoteAddr, game) -} diff --git a/cmd/web/flagSliceString.go b/cmd/web/flagSliceString.go deleted file mode 100644 index ec06966..0000000 --- a/cmd/web/flagSliceString.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -type FlagSliceString []string - -func (f *FlagSliceString) String() string { - return "" -} - -func (f *FlagSliceString) Set(val string) error { - *f = append(*f, val) - return nil -} diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go new file mode 100644 index 0000000..b8fa705 --- /dev/null +++ b/cmd/web/handlers.go @@ -0,0 +1,207 @@ +package main + +import ( + "fmt" + "strings" + "net/url" + "os" + "encoding/json" + "io" + "net/http" + "time" + + "github.com/gorilla/mux" + "riedstra.dev/mitch/steam-export/steam" +) + +func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) { + // During rendering of the template I believe it's + // mutating during the sort of keys, so Lib no longer + // is an RWMutex and we're just locking this as if + // we're writing to it + a.Library.Lock() + defer a.Library.Unlock() + a.Status.Lock() + defer a.Status.Unlock() + + err := a.Templates.ExecuteTemplate(w, "index", + struct { + Lib *steam.Library + Info *statusInfo + Local bool + HostIP string + Port string + Version string + }{ + &a.Library.Library, + a.Status, + isLocal(r.RemoteAddr), + getHostIP(), + getPort(), + Version, + }) + if err != nil { + Logger.Printf("While Rendering template: %s", err) + } + Logger.Printf("Client %s Index page", r.RemoteAddr) +} + +func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + Logger.Printf("Installer: While parsing form: %s", err) + http.Error(w, fmt.Sprintf("Invalid form: %s", err), 400) + return + } + + uri := r.Form.Get("uri") + + if strings.HasPrefix(uri, "http") { + _, err := url.Parse(uri) + if err != nil { + Logger.Printf("Installer: While parsing url: %s", err) + http.Error(w, fmt.Sprintf("Invalid url: %s", err), 400) + return + } + } else { + fi, err := os.Stat(uri) + if err != nil || !fi.Mode().IsRegular() { + Logger.Printf("Installer: While parsing url/path: %s", err) + http.Error(w, fmt.Sprintf("Invalid uri/path: %s", err), 400) + return + } + } + + Logger.Printf("Installer: Sending request for: %s to channel", uri) + a.download <- uri + + http.Redirect(w, r, "/", 302) +} + +func (a *App) HandleDownload(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + game := vars["game"] + + a.Library.Lock() + g, ok := a.Library.Games[game] + a.Library.Unlock() + if !ok { + Logger.Printf("Missing: %s", game) + http.Error(w, "Game is missing", 404) + return + } + + w.Header().Add("Content-type", "application/tar") + w.Header().Add("Estimated-size", fmt.Sprintf("%d", g.Size)) + + Logger.Printf("Client %s is downloading: %s", r.RemoteAddr, game) + + // Invert the writer so we can break up the copy and get progress + // information in here + rdr, pwrtr := io.Pipe() + go func() { + err := g.Package(pwrtr) + if err != nil { + Logger.Println("Error in package writing: ", err) + } + }() + + var total int64 + start := time.Now() + for { + n, err := io.CopyN(w, rdr, 256*1024*1024) + if err == io.EOF { + break + } + if err != nil { + Logger.Printf("Client %s Error Sending game: %s", r.RemoteAddr, err) + // Headers already sent, don't bother sending an error + return + } + total += n + mb := float64(total / 1024 / 1024) + rate := mb / time.Since(start).Seconds() + + Logger.Printf("Client %s is downloading %s: %0.1f%% done %.2f mb/s", + r.RemoteAddr, game, float64(total)/float64(g.Size)*100, rate) + } + + Logger.Printf("Client %s finished downloading: %s", r.RemoteAddr, game) +} + +func (a *App) HandleDelete(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + Logger.Printf("Installer: While parsing form: %s", err) + http.Error(w, fmt.Sprintf("Invalid form: %s", err), 400) + return + } + + game := r.PostForm.Get("name") + + if game == "" { + Logger.Println("Deleter: No game specified") + http.Error(w, "Game param required", 400) + return + } + + a.Library.Lock() + g, ok := a.Library.Games[game] + a.Library.Unlock() + if !ok { + Logger.Printf("Missing: %s", game) + http.Error(w, "Game is missing", 404) + return + } + + err = g.Delete() + if err != nil { + Logger.Printf("Error removing game: %s", err) + http.Error(w, fmt.Sprintf("Error removing game: %s", err), 500) + return + } + Logger.Printf("Removed game: %s", game) + + a.LibraryReload() + http.Redirect(w, r, "/", 302) +} + +func (a *App) HandleStats(w http.ResponseWriter, r *http.Request) { + a.Status.RLock() + defer a.Status.RUnlock() + + w.Header().Add("Content-type", "application/json") + + enc := json.NewEncoder(w) + + err := enc.Encode(a.Status) + if err != nil { + Logger.Println("While encoding Status: ", err) + } + return +} + +func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + Logger.Printf("Setlib: While parsing form: %s", err) + http.Error(w, fmt.Sprintf("Invalid form: %s", err), 400) + return + } + + a.LibrarySet(r.Form.Get("path")) + + http.Redirect(w, r, "/", 302) +} + +func HandleQuit(w http.ResponseWriter, r *http.Request) { + Logger.Println("Quit was called, exiting") + w.Header().Add("Content-type", "text/plain") + w.Write([]byte("Shutting down...")) + go func() { + time.Sleep(time.Second * 2) + os.Exit(0) + }() + return +} + diff --git a/cmd/web/index.go b/cmd/web/index.go deleted file mode 100644 index 7291af0..0000000 --- a/cmd/web/index.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "net/http" - - "riedstra.dev/mitch/steam-export/steam" -) - -func (a *App) HandleIndex(w http.ResponseWriter, r *http.Request) { - // During rendering of the template I believe it's - // mutating during the sort of keys, so Lib no longer - // is an RWMutex and we're just locking this as if - // we're writing to it - a.Library.Lock() - defer a.Library.Unlock() - a.Status.Lock() - defer a.Status.Unlock() - - err := a.Templates.ExecuteTemplate(w, "index", - struct { - Lib *steam.Library - Info *statusInfo - Local bool - HostIP string - Port string - Version string - }{ - &a.Library.Library, - a.Status, - isLocal(r.RemoteAddr), - getHostIP(), - getPort(), - Version, - }) - if err != nil { - Logger.Printf("While Rendering template: %s", err) - } - Logger.Printf("Client %s Index page", r.RemoteAddr) -} diff --git a/cmd/web/install.go b/cmd/web/install.go index ce8aa8d..dc85a89 100644 --- a/cmd/web/install.go +++ b/cmd/web/install.go @@ -1,32 +1,15 @@ package main import ( - "encoding/json" "fmt" "io" "net/http" - "net/url" "os" "strconv" "strings" "time" ) -func (a *App) HandleStats(w http.ResponseWriter, r *http.Request) { - a.Status.RLock() - defer a.Status.RUnlock() - - w.Header().Add("Content-type", "application/json") - - enc := json.NewEncoder(w) - - err := enc.Encode(a.Status) - if err != nil { - Logger.Println("While encoding Status: ", err) - } - return -} - func (a *App) installHttp(u string) error { Logger.Println("Installer: loading from url") resp, err := http.Get(u) @@ -129,38 +112,3 @@ func (a *App) installer() { } } -func (a *App) HandleInstall(w http.ResponseWriter, r *http.Request) { - if unauthorizedIfNotLocal(w, r) { - return - } - - err := r.ParseForm() - if err != nil { - Logger.Printf("Installer: While parsing form: %s", err) - http.Error(w, fmt.Sprintf("Invalid form: %s", err), 400) - return - } - - uri := r.Form.Get("uri") - - if strings.HasPrefix(uri, "http") { - _, err := url.Parse(uri) - if err != nil { - Logger.Printf("Installer: While parsing url: %s", err) - http.Error(w, fmt.Sprintf("Invalid url: %s", err), 400) - return - } - } else { - fi, err := os.Stat(uri) - if err != nil || !fi.Mode().IsRegular() { - Logger.Printf("Installer: While parsing url/path: %s", err) - http.Error(w, fmt.Sprintf("Invalid uri/path: %s", err), 400) - return - } - } - - Logger.Printf("Installer: Sending request for: %s to channel", uri) - a.download <- uri - - http.Redirect(w, r, "/", 302) -} diff --git a/cmd/web/main.go b/cmd/web/main.go index f0c7489..875b2d8 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -4,64 +4,15 @@ import ( "embed" "flag" "fmt" - "html/template" "log" "math/rand" - "net" "net/http" "os" - "strings" - "sync" "time" "github.com/gorilla/mux" - "riedstra.dev/mitch/steam-export/steam" ) -type App struct { - Library *steamLib - Status *statusInfo - - Templates *template.Template - - // Sending to this channel triggers the downloader in the background - download chan string -} - -func NewApp(libPath string) (*App, error) { - lib, err := steam.NewLibrary(libPath) - if err != nil { - return nil, err - } - - a := &App{ - Library: &steamLib{}, - Status: &statusInfo{}, - download: make(chan string), - } - - a.Library.Library = *lib - - a.Templates = template.Must(template.New("index").Parse(indexTemplate)) - - return a, nil -} - -type steamLib struct { - steam.Library - sync.Mutex -} - -type statusInfo struct { - sync.RWMutex - Running bool - Error error - Url string - Transferred int64 - Size int64 - Start *time.Time -} - var ( Version = "Development" Logger = log.New(os.Stderr, "", log.LstdFlags) @@ -73,116 +24,6 @@ var ( indexTemplate string ) -func (a *App) LibrarySet(path string) { - Logger.Println("Starting library reload") - a.Library.Lock() - defer a.Library.Unlock() - var err error - l2, err := steam.NewLibrary(path) - if err != nil { - Logger.Printf("Error reopening lib: %s", err) - return - } - a.Library.Library = *l2 - Logger.Println("Done reloading lib") -} - -func (a *App) LibraryReload() { - cur := a.Library.Folder - a.LibrarySet(cur) - return -} - -func (a *App) HandleSetLib(w http.ResponseWriter, r *http.Request) { - if unauthorizedIfNotLocal(w, r) { - return - } - - err := r.ParseForm() - if err != nil { - Logger.Printf("Setlib: While parsing form: %s", err) - http.Error(w, fmt.Sprintf("Invalid form: %s", err), 400) - return - } - - a.LibrarySet(r.Form.Get("path")) - - http.Redirect(w, r, "/", 302) -} - -func (a *App) HandleQuit(w http.ResponseWriter, r *http.Request) { - if unauthorizedIfNotLocal(w, r) { - return - } - - Logger.Println("Quit was called, exiting") - w.Header().Add("Content-type", "text/plain") - w.Write([]byte("Shutting down...")) - go func() { - time.Sleep(time.Second * 2) - os.Exit(0) - }() - return -} - -func unauthorizedIfNotLocal(w http.ResponseWriter, r *http.Request) bool { - if !isLocal(r.RemoteAddr) { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - Logger.Printf("Unauthorized request from: %s for %s", - r.RemoteAddr, r.RequestURI) - return true - } - return false -} - -func isLocal(addr string) bool { - _, localNet, _ := net.ParseCIDR("127.0.0.1/8") - return localNet.Contains(net.ParseIP(strings.Split(addr, ":")[0])) -} - -// getHostIP attempts to guess the IP address of the current machine and -// returns that. Simply bails at the first non sane looking IP and returns it. -// Not ideal but it should work well enough most of the time -func getHostIP() string { - iFaces, err := net.Interfaces() - if err != nil { - return "127.0.0.1" - } - - // RFC 3927 - _, ipv4LinkLocal, _ := net.ParseCIDR("169.254.0.0/16") - - for _, iFace := range iFaces { - addrs, err := iFace.Addrs() - if err != nil { - return "127.0.0.1" - } - - for _, a := range addrs { - n, ok := a.(*net.IPNet) - if !ok { - continue - } - - if n.IP.To4() != nil && !n.IP.IsLoopback() && !ipv4LinkLocal.Contains(n.IP.To4()) { - return n.IP.String() - } - } - } - - return "127.0.0.1" -} - -func getPort() string { - s := strings.Split(Listen, ":") - - if len(s) != 2 { - return Listen - } - - return s[1] -} - func main() { fl := flag.NewFlagSet("steam-export", flag.ExitOnError) debug := fl.Bool("d", false, "Print line numbers in log") @@ -203,13 +44,13 @@ func main() { r := mux.NewRouter() - r.HandleFunc("/quit", a.HandleQuit) - r.HandleFunc("/setLib", a.HandleSetLib) - r.HandleFunc("/delete", a.HandleDelete) - r.HandleFunc("/install", a.HandleInstall) + r.HandleFunc("/quit", HandleQuit) + r.Handle("/setLib", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleSetLib))) + r.Handle("/delete", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleDelete))) + r.Handle("/install", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleInstall))) r.HandleFunc("/steam-export-web.exe", serveSelf) r.HandleFunc("/download/{game}", a.HandleDownload) - r.HandleFunc("/status", a.HandleStats) + r.Handle("/status", UnauthorizedIfNotLocal(http.HandlerFunc(a.HandleStats))) r.PathPrefix("/static").Handler( http.FileServer(http.FS(embeddedStatic))) r.HandleFunc("/", a.HandleIndex) @@ -226,7 +67,7 @@ func main() { if err != nil { Logger.Printf("Encountered: %s", err) rand.Seed(time.Now().UnixNano()) - Listen = fmt.Sprintf(":%d", rand.Intn(63000)+1024) + Listen = fmt.Sprintf(":%d", rand.Intn(9000)+1024) Logger.Printf("Trying: %s", Listen) s.Addr = Listen } diff --git a/cmd/web/serve-self.go b/cmd/web/serve-self.go deleted file mode 100644 index 8abb830..0000000 --- a/cmd/web/serve-self.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "io" - "net/http" - "os" -) - -func serveSelf(w http.ResponseWriter, r *http.Request) { - s, err := os.Executable() - if err != nil { - Logger.Println("While trying to get my executable path: ", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - fh, err := os.Open(s) - if err != nil { - Logger.Println("While opening my own executable for reading: ", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - _, err = io.Copy(w, fh) - fh.Close() - return -} diff --git a/cmd/web/util.go b/cmd/web/util.go new file mode 100644 index 0000000..1252c66 --- /dev/null +++ b/cmd/web/util.go @@ -0,0 +1,89 @@ +package main + +import ( + "io" + "os" + "net/http" + "net" + "strings" +) + +func UnauthorizedIfNotLocal(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !isLocal(r.RemoteAddr) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + Logger.Printf("Unauthorized request from: %s for %s", + r.RemoteAddr, r.RequestURI) + return + } + h.ServeHTTP(w, r) + }) +} + +func isLocal(addr string) bool { + _, localNet, _ := net.ParseCIDR("127.0.0.1/8") + return localNet.Contains(net.ParseIP(strings.Split(addr, ":")[0])) +} + +// getHostIP attempts to guess the IP address of the current machine and +// returns that. Simply bails at the first non sane looking IP and returns it. +// Not ideal but it should work well enough most of the time +func getHostIP() string { + iFaces, err := net.Interfaces() + if err != nil { + return "127.0.0.1" + } + + // RFC 3927 + _, ipv4LinkLocal, _ := net.ParseCIDR("169.254.0.0/16") + + for _, iFace := range iFaces { + addrs, err := iFace.Addrs() + if err != nil { + return "127.0.0.1" + } + + for _, a := range addrs { + n, ok := a.(*net.IPNet) + if !ok { + continue + } + + if n.IP.To4() != nil && !n.IP.IsLoopback() && !ipv4LinkLocal.Contains(n.IP.To4()) { + return n.IP.String() + } + } + } + + return "127.0.0.1" +} + +func getPort() string { + s := strings.Split(Listen, ":") + + if len(s) != 2 { + return Listen + } + + return s[1] +} + +func serveSelf(w http.ResponseWriter, r *http.Request) { + s, err := os.Executable() + if err != nil { + Logger.Println("While trying to get my executable path: ", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + fh, err := os.Open(s) + if err != nil { + Logger.Println("While opening my own executable for reading: ", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + _, err = io.Copy(w, fh) + fh.Close() + return +} |
