diff --git a/.env b/.env new file mode 100644 index 0000000..6c90030 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +BearerToken=ok \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f7fc79f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM golang:1.22 as builder + +WORKDIR /app + +# Copy go module files and download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the application +COPY . . + +# Build the Go application +RUN go build -o main . + +# Stage 2: Create a minimal production image +FROM alpine:latest + +WORKDIR /app + +# Copy the built application from the builder stage +COPY --from=builder /app/main . + +# Copy the .env file (if exists) +COPY .env .env + +# Expose the necessary port (change as needed) +EXPOSE 8080 + +# Run the application +CMD ["./main"] + +# Healthcheck to ensure the app is running +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 diff --git a/README.md b/README.md index c21c50c..30e73e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # CCTV +## .env Configuration +```env +UploadDir=./upload +LOG_FILE=auth_failures.log +MaxFailedAttempts=3 +BanDuration=5 +BearerToken=jhasd083raASDasdoad§$234 +```` \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..41bccfb --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "net/http" + "time" + + "git.neunzweinull.com/jan/CCTV/config" + "git.neunzweinull.com/jan/CCTV/internal" +) + +const ( + CleanupInterval = 5 * time.Minute +) + +func init() { + //Load the env file + config.LoadEnv() + + //Initialize the log file + internal.InitLog() + + //Initialize the Upload Directory + config.InitUploadDir() + + //Start background cleanup for expired bans + go func() { + for { + time.Sleep(CleanupInterval) + internal.CleanupBans() + } + }() +} + +func main() { + defer internal.CloseLog() + + http.HandleFunc("/upload", internal.AuthMiddleware(internal.UploadHandler)) + http.HandleFunc("/health", internal.HealthHandler) + + http.ListenAndServe(":8080", nil) +} diff --git a/config/env.go b/config/env.go new file mode 100644 index 0000000..f9e4f03 --- /dev/null +++ b/config/env.go @@ -0,0 +1,62 @@ +package config + +import ( + "bufio" + "os" + "strings" + + "git.neunzweinull.com/jan/CCTV/internal" +) + +const ( + Prefix = "CCTV_" +) + +func LoadEnv() error { + // TODO: Load env from custom file + setDefaultEnvs() + + file, err := os.Open(".env") + if err != nil { + return err + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.Split(scanner.Text(), "=") + + //Ignore empty lines and comments + if len(line) < 2 || strings.HasPrefix(line[0], "#") { + continue + } + + key := strings.TrimSpace(line[0]) + value := strings.TrimSpace(line[1]) + + if key == "" || value == "" { + continue + } + + // Add prefix + key = Prefix + key + + // Remove quotes + value = strings.Trim(value, `"'`) + + os.Setenv(key, value) + + internal.SetShit() + } + + return scanner.Err() +} + +func setDefaultEnvs() { + os.Setenv(Prefix+"UploadDir", "uploads/") + os.Setenv(Prefix+"LOG_FILE", "auth_failures.log") + + os.Setenv(Prefix+"MaxFailedAttempts", "3") + os.Setenv(Prefix+"BanDuration", "5") +} diff --git a/config/upload.go b/config/upload.go new file mode 100644 index 0000000..fb6c8d0 --- /dev/null +++ b/config/upload.go @@ -0,0 +1,18 @@ +package config + +import ( + "fmt" + "os" +) + +func InitUploadDir() { + if _, err := os.Stat(os.Getenv("CCTV_UploadDir")); err == nil { + return + } + + err := os.Mkdir(os.Getenv("CCTV_UploadDir"), 0755) + if err != nil { + fmt.Println("Error creating upload directory:", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eaab518 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.neunzweinull.com/jan/CCTV + +go 1.22.2 diff --git a/internal/bans.go b/internal/bans.go new file mode 100644 index 0000000..8e05526 --- /dev/null +++ b/internal/bans.go @@ -0,0 +1,13 @@ +package internal + +import "time" + +func CleanupBans() { + mu.Lock() + defer mu.Unlock() + for ip, banTime := range bannedIPs { + if time.Now().After(banTime) { + delete(bannedIPs, ip) + } + } +} diff --git a/internal/log.go b/internal/log.go new file mode 100644 index 0000000..2dcc9ca --- /dev/null +++ b/internal/log.go @@ -0,0 +1,71 @@ +package internal + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "sync" + "time" +) + +var ( + logFile *os.File + mu sync.Mutex +) + +func InitLog() { + var err error + logFileName := os.Getenv("CCTV_LOG_FILE") + if logFileName == "" { + logFileName = "app.log" // Default log file name if the environment variable is not set + } + + var logFilePath string + if runtime.GOOS == "windows" { + logFilePath = filepath.Join("C:\\ProgramData\\CCTV", logFileName) + } else { + logFilePath = filepath.Join("/var/log", logFileName) + } + + // Ensure directory exists + err = os.MkdirAll(filepath.Dir(logFilePath), 0755) + if err != nil { + fmt.Println("Error creating log directory:", err) + os.Exit(1) + } + + logFile, err = os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Println("Error opening log file:", err) + os.Exit(1) + } +} + +func LogFailedAttempt(ip string) { + logEntry := fmt.Sprintf("[%s] Failed auth attempt from IP: %s\n", time.Now().Format(time.RFC3339), ip) + logFile.WriteString(logEntry) + logFile.Sync() + + fmt.Print(logEntry) +} + +func LogBan(ip string) { + logEntry := fmt.Sprintf("[%s] Banned IP: %s\n", time.Now().Format(time.RFC3339), ip) + logFile.WriteString(logEntry) + logFile.Sync() + + fmt.Print(logEntry) +} + +func LogSuccessUpload(ip string, fileName string) { + logEntry := fmt.Sprintf("[%s] Successfully uploaded file %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 new file mode 100644 index 0000000..b0424ea --- /dev/null +++ b/internal/server.go @@ -0,0 +1,131 @@ +package internal + +import ( + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +var ( + MaxFailedAttempts = 3 + BanDuration = 5 * time.Minute + UploadDir = "" + failedAttempts = make(map[string]int) + bannedIPs = make(map[string]time.Time) +) + +func SetShit() { + if maxAttempts, err := strconv.Atoi(os.Getenv("CCTV_MaxFailedAttempts")); err == nil { + MaxFailedAttempts = maxAttempts + } + if banDuration, err := time.ParseDuration(os.Getenv("CCTV_BanDuration")); err == nil { + BanDuration = banDuration * time.Minute + } + UploadDir = os.Getenv("CCTV_UploadDir") +} + +func UploadHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseMultipartForm(10 << 20) + if err != nil { + http.Error(w, "Error parsing form", http.StatusBadRequest) + return + } + + file, handler, err := r.FormFile("file") + if err != nil { + http.Error(w, "Error retrieving file", http.StatusBadRequest) + return + } + + defer file.Close() + + filePath := filepath.Join(UploadDir, handler.Filename) + + if _, err := os.Stat(filePath); err == nil { + http.Error(w, "File already exists", http.StatusBadRequest) + return + } + + out, err := os.Create(filePath) + if err != nil { + http.Error(w, "Error creating file", http.StatusInternalServerError) + return + } + + defer out.Close() + + _, err = io.Copy(out, file) + if err != nil { + http.Error(w, "Error saving file", http.StatusInternalServerError) + return + } + + LogSuccessUpload(getClientIP(r), handler.Filename) +} + +func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ip := getClientIP(r) + + mu.Lock() + + // Check if IP is banned + 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() + + authHeader := r.Header.Get("Authorization") + if !strings.HasPrefix(authHeader, "Bearer ") || strings.TrimPrefix(authHeader, "Bearer ") != os.Getenv("CCTV_BearerToken") { + 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) + return + } + + mu.Lock() + delete(failedAttempts, ip) + mu.Unlock() + + next(w, r) + } + +} + +func HealthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +// Get client IP address from request +func getClientIP(r *http.Request) string { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr // Fallback + } + return ip +}