Merge branch 'main' of https://git.neunzweinull.com/jan/CCTV
This commit is contained in:
commit
bf0bc3ceb8
@ -34,8 +34,13 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
defer internal.CloseLog()
|
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("/upload", internal.AuthMiddleware(internal.UploadHandler))
|
||||||
http.HandleFunc("/health", internal.HealthHandler)
|
http.HandleFunc("/health", internal.HealthHandler)
|
||||||
|
|
||||||
|
http.HandleFunc("/uploads/", internal.AuthClientMiddleware(internal.HandleFileServer("/uploads", "/uploads/")))
|
||||||
|
|
||||||
http.ListenAndServe(":8080", nil)
|
http.ListenAndServe(":8080", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,20 @@ func LoadEnv() error {
|
|||||||
return scanner.Err()
|
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() {
|
func setDefaultEnvs() {
|
||||||
if _, ok := os.LookupEnv(Prefix + "UploadDir"); !ok {
|
if _, ok := os.LookupEnv(Prefix + "UploadDir"); !ok {
|
||||||
os.Setenv(Prefix+"UploadDir", "uploads/")
|
os.Setenv(Prefix+"UploadDir", "uploads/")
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
|||||||
module git.neunzweinull.com/jan/CCTV
|
module git.neunzweinull.com/jan/CCTV
|
||||||
|
|
||||||
go 1.22.2
|
go 1.22.2
|
||||||
|
|
||||||
|
require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
|
|||||||
2
go.sum
Normal file
2
go.sum
Normal file
@ -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=
|
||||||
@ -66,6 +66,14 @@ func LogSuccessUpload(ip string, fileName string) {
|
|||||||
fmt.Print(logEntry)
|
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() {
|
func CloseLog() {
|
||||||
logFile.Close()
|
logFile.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -18,8 +21,18 @@ var (
|
|||||||
UploadDir = ""
|
UploadDir = ""
|
||||||
failedAttempts = make(map[string]int)
|
failedAttempts = make(map[string]int)
|
||||||
bannedIPs = make(map[string]time.Time)
|
bannedIPs = make(map[string]time.Time)
|
||||||
|
JWT_Secret = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type picture struct {
|
||||||
|
picture string
|
||||||
|
}
|
||||||
|
|
||||||
func SetShit() {
|
func SetShit() {
|
||||||
if maxAttempts, err := strconv.Atoi(os.Getenv("CCTV_MaxFailedAttempts")); err == nil {
|
if maxAttempts, err := strconv.Atoi(os.Getenv("CCTV_MaxFailedAttempts")); err == nil {
|
||||||
MaxFailedAttempts = maxAttempts
|
MaxFailedAttempts = maxAttempts
|
||||||
@ -27,9 +40,156 @@ func SetShit() {
|
|||||||
if banDuration, err := time.ParseDuration(os.Getenv("CCTV_BanDuration")); err == nil {
|
if banDuration, err := time.ParseDuration(os.Getenv("CCTV_BanDuration")); err == nil {
|
||||||
BanDuration = banDuration * time.Minute
|
BanDuration = banDuration * time.Minute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JWT_Secret = os.Getenv("CCTV_JWT_Secret")
|
||||||
UploadDir = os.Getenv("CCTV_UploadDir")
|
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" {
|
||||||
|
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 {
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := picture{picture: newestPicture.Name()}
|
||||||
|
|
||||||
|
tmpl := template.Must(template.ParseFiles("views/dash.html"))
|
||||||
|
tmpl.Execute(w, p)
|
||||||
|
}
|
||||||
|
|
||||||
func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
err := r.ParseMultipartForm(10 << 20)
|
err := r.ParseMultipartForm(10 << 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -118,6 +278,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) {
|
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|||||||
10
views/dash.html
Normal file
10
views/dash.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>CCTV</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Latest Picture</h1>
|
||||||
|
<img src="{{.picture}}" alt="Latest Picture">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
views/index.html
Normal file
17
views/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>CCTV</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>CCTV</h1>
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form action="/login" method="post">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user