From 22cdce32bc0e5f36e8f36aa56408791630ec1835 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 Feb 2025 23:40:36 +0100 Subject: [PATCH 1/3] some init website stuff not working header does not get set propperly --- cmd/main.go | 3 ++ internal/server.go | 101 +++++++++++++++++++++++++++++++++++++++++++++ views/dash.html | 10 +++++ views/index.html | 17 ++++++++ 4 files changed, 131 insertions(+) create mode 100644 views/dash.html create mode 100644 views/index.html diff --git a/cmd/main.go b/cmd/main.go index 41bccfb..bbb762c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,6 +34,9 @@ func init() { func main() { defer internal.CloseLog() + http.HandleFunc("/", internal.LandingHandler) + http.HandleFunc("/login", internal.LoginHandler) + http.HandleFunc("/dash", internal.AuthClientMiddleware(internal.DashboardHandler)) http.HandleFunc("/upload", internal.AuthMiddleware(internal.UploadHandler)) http.HandleFunc("/health", internal.HealthHandler) diff --git a/internal/server.go b/internal/server.go index b0424ea..5d1efb1 100644 --- a/internal/server.go +++ b/internal/server.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strconv" "strings" + "text/template" "time" ) @@ -30,6 +31,106 @@ func SetShit() { UploadDir = os.Getenv("CCTV_UploadDir") } +func LandingHandler(w http.ResponseWriter, r *http.Request) { + content, err := os.ReadFile("views/index.html") + if err != nil { + http.Error(w, "Error reading file", http.StatusInternalServerError) + return + } + fmt.Fprint(w, string(content)) +} + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + ip := getClientIP(r) + + mu.Lock() + + if banTime, banned := bannedIPs[ip]; banned { + fmt.Print("Banned IP: ", ip, " until ", banTime, "\n") + if time.Now().Before(banTime) { + bannedIPs[ip] = time.Now().Add(BanDuration) + http.Error(w, "You are banned", http.StatusForbidden) + mu.Unlock() + return + } + delete(bannedIPs, ip) + } + + mu.Unlock() + + username := r.FormValue("username") + password := r.FormValue("password") + + if username == "admin" && password == "admin" { + w.Header().Set("Authorization", "Bearer "+"hihi") + http.Redirect(w, r, "/dash", http.StatusSeeOther) + return + } else { + LogFailedAttempt(ip) + + mu.Lock() + failedAttempts[ip]++ + if failedAttempts[ip] >= MaxFailedAttempts { + bannedIPs[ip] = time.Now().Add(BanDuration) + delete(failedAttempts, ip) + + LogBan(ip) + } + mu.Unlock() + + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + +} + +func AuthClientMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + fmt.Print(authHeader) + if !strings.HasPrefix(authHeader, "Bearer ") || strings.TrimPrefix(authHeader, "Bearer ") != "hihi" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + next(w, r) + } +} + +func DashboardHandler(w http.ResponseWriter, r *http.Request) { + // get the newest picture name + pictures, err := os.ReadDir(UploadDir) + if err != nil { + http.Error(w, "Error reading directory", http.StatusInternalServerError) + return + } + + if len(pictures) == 0 { + http.Error(w, "No pictures found", http.StatusNotFound) + return + } + + // get the newest picture + newestPicture := pictures[0] + for _, picture := range pictures { + pictureInfo, err := picture.Info() + if err != nil { + http.Error(w, "Error getting file info", http.StatusInternalServerError) + return + } + newestPictureInfo, err := newestPicture.Info() + if err != nil { + http.Error(w, "Error getting file info", http.StatusInternalServerError) + return + } + if pictureInfo.ModTime().After(newestPictureInfo.ModTime()) { + newestPicture = picture + } + } + + tmpl := template.Must(template.ParseFiles("views/dash.html")) + tmpl.Execute(w, newestPicture.Name()) +} + func UploadHandler(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(10 << 20) if err != nil { diff --git a/views/dash.html b/views/dash.html new file mode 100644 index 0000000..7eeebe1 --- /dev/null +++ b/views/dash.html @@ -0,0 +1,10 @@ + + + + CCTV + + +

Latest Picture

+ Latest Picture + + \ No newline at end of file diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..e24832e --- /dev/null +++ b/views/index.html @@ -0,0 +1,17 @@ + + + + CCTV + + +

CCTV

+

Login

+
+ + + + + +
+ + \ No newline at end of file From b31761e6e8a256d1dbc55620355a89df35d5ab80 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 26 Feb 2025 20:36:44 +0100 Subject: [PATCH 2/3] Auth with Cookies works, User and Password is still hard coded --- cmd/main.go | 2 ++ config/env.go | 14 ++++++++++ go.mod | 2 ++ go.sum | 2 ++ internal/log.go | 8 ++++++ internal/server.go | 70 +++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 go.sum diff --git a/cmd/main.go b/cmd/main.go index bbb762c..3b5ba58 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -40,5 +40,7 @@ func main() { http.HandleFunc("/upload", internal.AuthMiddleware(internal.UploadHandler)) http.HandleFunc("/health", internal.HealthHandler) + http.HandleFunc("/uploads/", internal.AuthClientMiddleware(internal.HandleFileServer("/uploads", "/uploads/"))) + http.ListenAndServe(":8080", nil) } diff --git a/config/env.go b/config/env.go index 195bbc3..91212e8 100644 --- a/config/env.go +++ b/config/env.go @@ -53,6 +53,20 @@ func LoadEnv() error { return scanner.Err() } +func requiredEnvs() { + if _, ok := os.LookupEnv(Prefix + "BearerToken"); !ok { + panic("BearerToken is required") + } + + if _, ok := os.LookupEnv(Prefix + "UploadDir"); !ok { + panic("UploadDir is required") + } + + if _, ok := os.LookupEnv(Prefix + "JWT_Secret"); !ok { + panic("JWT_Secret is required") + } +} + func setDefaultEnvs() { if _, ok := os.LookupEnv(Prefix + "UploadDir"); !ok { os.Setenv(Prefix+"UploadDir", "uploads/") diff --git a/go.mod b/go.mod index eaab518..9640221 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.neunzweinull.com/jan/CCTV go 1.22.2 + +require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f56d3e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= diff --git a/internal/log.go b/internal/log.go index 2dcc9ca..041463c 100644 --- a/internal/log.go +++ b/internal/log.go @@ -66,6 +66,14 @@ func LogSuccessUpload(ip string, fileName string) { fmt.Print(logEntry) } +func LogFileServerRequest(ip string, fileName string) { + logEntry := fmt.Sprintf("[%s] File server request for %s from IP: %s\n", time.Now().Format(time.RFC3339), fileName, ip) + logFile.WriteString(logEntry) + logFile.Sync() + + fmt.Print(logEntry) +} + func CloseLog() { logFile.Close() } diff --git a/internal/server.go b/internal/server.go index 5d1efb1..dcead48 100644 --- a/internal/server.go +++ b/internal/server.go @@ -11,6 +11,8 @@ import ( "strings" "text/template" "time" + + "github.com/golang-jwt/jwt/v5" ) var ( @@ -19,8 +21,14 @@ var ( UploadDir = "" failedAttempts = make(map[string]int) bannedIPs = make(map[string]time.Time) + JWT_Secret = "" ) +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + func SetShit() { if maxAttempts, err := strconv.Atoi(os.Getenv("CCTV_MaxFailedAttempts")); err == nil { MaxFailedAttempts = maxAttempts @@ -28,6 +36,8 @@ func SetShit() { if banDuration, err := time.ParseDuration(os.Getenv("CCTV_BanDuration")); err == nil { BanDuration = banDuration * time.Minute } + + JWT_Secret = os.Getenv("CCTV_JWT_Secret") UploadDir = os.Getenv("CCTV_UploadDir") } @@ -62,7 +72,28 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { password := r.FormValue("password") if username == "admin" && password == "admin" { - w.Header().Set("Authorization", "Bearer "+"hihi") + exTime := time.Now().Add(5 * time.Minute) + claims := &Claims{ + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(exTime), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(JWT_Secret)) + if err != nil { + http.Error(w, "Error signing token", http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: tokenString, + Expires: exTime, + SameSite: http.SameSiteStrictMode, + Secure: true, + }) http.Redirect(w, r, "/dash", http.StatusSeeOther) return } else { @@ -85,13 +116,35 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { func AuthClientMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - fmt.Print(authHeader) - if !strings.HasPrefix(authHeader, "Bearer ") || strings.TrimPrefix(authHeader, "Bearer ") != "hihi" { + authCookie, err := r.Cookie("token") + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + claims := &Claims{} + token, err := jwt.ParseWithClaims(authCookie.Value, claims, func(token *jwt.Token) (interface{}, error) { + return []byte(JWT_Secret), nil + }) + + if err != nil || !token.Valid { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } + exTime := time.Now().Add(5 * time.Minute) + claims.ExpiresAt = jwt.NewNumericDate(exTime) + tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(JWT_Secret)) + if err == nil { + http.SetCookie(w, &http.Cookie{ + Name: "token", + Value: tokenString, + Expires: exTime, + SameSite: http.SameSiteStrictMode, + Secure: true, + }) + } + next(w, r) } } @@ -218,6 +271,15 @@ func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { } +func HandleFileServer(dir, prefix string) http.HandlerFunc { + fs := http.FileServer(http.Dir(dir)) + realHandler := http.StripPrefix(prefix, fs).ServeHTTP + return func(w http.ResponseWriter, r *http.Request) { + LogFileServerRequest(getClientIP(r), r.URL.Path) + realHandler(w, r) + } +} + func HealthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } From 882548344a5082dc21ebc3de3f5107568782b79a Mon Sep 17 00:00:00 2001 From: Jan Barfuss Date: Sun, 2 Mar 2025 11:06:52 +0100 Subject: [PATCH 3/3] dashboard handler --- internal/server.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/server.go b/internal/server.go index dcead48..a09c93e 100644 --- a/internal/server.go +++ b/internal/server.go @@ -29,6 +29,10 @@ type Claims struct { jwt.RegisteredClaims } +type picture struct { + picture string +} + func SetShit() { if maxAttempts, err := strconv.Atoi(os.Getenv("CCTV_MaxFailedAttempts")); err == nil { MaxFailedAttempts = maxAttempts @@ -180,8 +184,10 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { } } + p := picture{picture: newestPicture.Name()} + tmpl := template.Must(template.ParseFiles("views/dash.html")) - tmpl.Execute(w, newestPicture.Name()) + tmpl.Execute(w, p) } func UploadHandler(w http.ResponseWriter, r *http.Request) {