package main
import (
"bufio"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"net/smtp"
"encoding/json"
"crypto/sha256"
"encoding/hex"
"sort"
"net/url"
"net/mail"
// _ "expvar"
// _ "net/http/pprof"
)
// private recaptcha v3
// public web key have to put on the html website
const pivcap string = "change-me"
// score recaptcha v3
const scorecap float64 = 0.6
//Max Log Size
const maxLogSize int64 = 500 * 1024 * 1024 // 500 MB (in Bytes)
// Configuration structure
type Config struct {
LogLevel string
LogFile string
Proxy string
}
// recaptcha v3 structure
type jDaten struct {
Success bool
ChallengeTS string
Hostname string
Score float64
Action string
}
var (
configFile = "./server.json"
config Config
filterFile = "./filter.cfg"
logFile = "./requests.log"
blockFile = "./blocklist.cfg"
)
func init() {
// Load configuration
loadConfig()
}
func main() {
// 1. Log-Datei auf Übergröße prüfen und rotieren (leeren), falls nötig.
// Dies muss VOR dem Öffnen und Einrichten des Loggers erfolgen.
checkAndRotateLog(config.LogFile)
logFile, err := os.OpenFile(config.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
defer logFile.Close()
// Den Output des globalen Log-Packages auf die Log-Datei umleiten.
log.SetOutput(logFile)
// Setzt die Flags für den globalen Logger, um MIKROSEKUNDEN ZU ENTFERNEN.
log.SetFlags(log.Ldate | log.Ltime)
// Der custom Logger für RequestLogger (schreibt ebenfalls in die Datei).
// Auch hier MIKROSEKUNDEN ENTFERNT, um Konsistenz zu gewährleisten.
logger := log.New(logFile, "", log.Ldate|log.Ltime)
mux := http.NewServeMux()
mux.HandleFunc("/", handleRequest)
mux.HandleFunc("/subscribe", handleSubscriptionForm)
// Der RequestLogger wird als Top-Level-Handler auf den gesamten MUX angewendet,
// um sicherzustellen, dass alle Anfragen protokolliert werden.
loggedMux := RequestLogger(mux, logger, config.LogLevel)
port := 8080
fmt.Printf("Server listening on :%d...\n", port)
// Der Server verwendet nun den loggedMux als seinen Haupt-Handler.
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), loggedMux))
}
func RequestLogger(targetMux http.Handler, logger *log.Logger, logLevel string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
secheader(w)
if _, err := os.Stat(blockFile); err == nil {
if isBlocked(getClientIP(r)) {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
}
buffer := &responseBuffer{ResponseWriter: w}
switch logLevel {
case "info":
logger.Printf("%s\t\t%s\t\t%s\t\t%s\n", getClientIP(r), r.Method, r.Referer(), r.RequestURI)
case "debug":
logger.Printf(
"%s\t\t%s\t\t%s\t\t%s\t\t%s\t\t%v\n",
getClientIP(r),
r.Method,
r.Referer(),
r.RequestURI,
r.Form.Encode(),
r.Header,
)
default:
logger.Printf("Unknown log level: %s", logLevel)
}
targetMux.ServeHTTP(buffer, r)
})
}
func checkAndRotateLog(filePath string) {
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
// Datei existiert nicht, keine Rotation erforderlich.
return
}
// Fehler wird an die Standardfehlerausgabe des Servers gesendet.
fmt.Fprintf(os.Stderr, "Error stating log file %s: %v\n", filePath, err)
return
}
// Wenn die Dateigröße das Limit überschreitet (500 MB)
if fileInfo.Size() > maxLogSize {
oldSize := fileInfo.Size()
// Leert die Datei durch Kürzung auf 0 Bytes.
err = os.Truncate(filePath, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: Failed to truncate log file %s: %v\n", filePath, err)
} else {
// Erfolgsmeldung an die Standardausgabe senden.
fmt.Printf("NOTICE: Log file %s rotated (emptied). Size exceeded %d bytes. Old size: %d bytes.\n", filePath, maxLogSize, oldSize)
}
}
}
// Custom response buffer to capture the status code
type responseBuffer struct {
http.ResponseWriter
statusCode int
}
func (b *responseBuffer) WriteHeader(code int) {
b.statusCode = code
b.ResponseWriter.WriteHeader(code)
}
func loadConfig() {
// Standardwerte
config = Config{
LogLevel: "info", // Standardwert für log_level
Proxy: "", // Standardwert für proxy
}
file, err := os.Open(configFile)
if err != nil {
log.Printf("Config file not found, using default settings.")
return
}
defer file.Close()
// JSON einlesen
var jsonData map[string]string
decoder := json.NewDecoder(file)
if err := decoder.Decode(&jsonData); err != nil {
log.Fatalf("Error parsing config file: %v", err)
}
// Alte Switch-Struktur beibehalten
for key, value := range jsonData {
switch key {
case "log_level":
config.LogLevel = value
case "log_file":
config.LogFile = value
case "proxy":
config.Proxy = value
}
}
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Get the requested path from the URL
requestedPath := filepath.Clean(r.URL.Path)
// Create the absolute path to the requested resource in the ./www directory
absolutePath := filepath.Join(".", "www", requestedPath)
// Check if the requested path is a directory
fileInfo, err := os.Stat(absolutePath)
if err != nil {
if os.IsNotExist(err) {
log.Printf("%v\t\t\t\t404 Page not found: %s", getClientIP(r), requestedPath)
http.NotFound(w, r)
return
}
log.Printf("\t\t\t\tError checking if path is a directory: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if fileInfo.IsDir() {
var indexFileName string
/*
if proto := r.Header.Get("X-Forwarded-Proto"); proto == "https" {
indexFileName = "index.html"
} else {
indexFileName = "index_http.html"
}
*/
indexFileName = "index.html"
// If the requested path is a directory, check for index.html
indexFilePath := filepath.Join(absolutePath, indexFileName)
_, err := os.Stat(indexFilePath)
if err == nil {
// If index.html exists, serve its content
serveFile(w, r, indexFilePath)
} else {
// If index.html doesn't exist, list the directory
listDirectory(w, r, absolutePath, requestedPath, indexFilePath)
}
} else {
// If the requested path is a file, serve its contents
serveFile(w, r, absolutePath)
}
}
func handleSubscriptionForm(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
err := r.ParseForm()
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
email := r.FormValue("email")
if email == "" {
http.Error(w, "Bad Request: Email is required", http.StatusBadRequest)
return
}
token := r.FormValue("recaptcha_token")
if token == "" {
http.Error(w, "Bad Token", http.StatusBadRequest)
return
}
// Get the real IP from X-Real-IP header, fallback to RemoteAddr
requesterIP := getClientIP(r)
if (OnPage(email, token, requesterIP , "subscript") == "1") {
// Send the subscription email to root@localhost
//log.Printf("%v\t\t\t\tSubscription: - %s", requesterIP, email)
// Send email
err = sendSubscriptionEmail(email)
if err != nil {
log.Printf("\t\t\t\tError sending subscription email: %v", err)
http.Error(w, fmt.Sprintf("Internal Server Error: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, "")
fmt.Fprintln(w, "Subscription email sent successfully")
} else {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, "")
fmt.Fprintln(w, "Google Token failed")
}
}
func sendSubscriptionEmail(email string) error {
// 1. Strikte Formatvalidierung
if _, err := mail.ParseAddress(email); err != nil {
// Wir returnen nicht den genauen Fehler an den Client, um keine Infos preiszugeben
return fmt.Errorf("Internal Server Error. Please try again later.")
}
// Entferne CR/LF, um Header Injection zu verhindern (dies ist immer noch notwendig,
// falls ParseAddress manipulierbare Zeichen wie 'Name ' durchlässt).
cleanEmail := strings.ReplaceAll(email, "\r", "")
cleanEmail = strings.ReplaceAll(cleanEmail, "\n", "")
// Feste, vom Server kontrollierte Adressen
from := "noreply-subscription@localhost"
to := "root@localhost"
subject := "New Subscription"
// Die bereinigte E-Mail-Adresse wird NUR im Body verwendet.
body := fmt.Sprintf("Subscriber Email: %s has subscribed.", cleanEmail)
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", from, to, subject, body)
// Unverschlüsselte lokale Verbindung über smtp.Client
c, err := smtp.Dial("127.0.0.1:25")
if err != nil {
log.Printf("Error connecting to SMTP: %v", err)
return err
}
defer c.Close()
// 4. SMTP MAIL FROM fixieren
// Verwende die feste Absenderadresse.
if err = c.Mail(from); err != nil {
log.Printf("Error setting MAIL FROM: %v", err)
return err
}
if err = c.Rcpt(to); err != nil {
log.Printf("Error setting RCPT TO: %v", err)
return err
}
w, err := c.Data()
if err != nil {
log.Printf("Error getting DATA writer: %v", err)
return err
}
_, err = w.Write([]byte(msg))
if err != nil {
log.Printf("Error writing message: %v", err)
return err
}
if err = w.Close(); err != nil {
log.Printf("Error closing DATA writer: %v", err)
return err
}
return c.Quit()
}
func getVisitorFingerprint(r *http.Request) (string, error) {
// ip := strings.Split(r.RemoteAddr, ":")[0]
ip := getClientIP(r)
userAgent := r.UserAgent()
// Concatenate IP address and user agent
fingerprint := ip + userAgent
// Hash the fingerprint using SHA-256 to create a fixed-size hash
hash := sha256.New()
hash.Write([]byte(fingerprint))
hashBytes := hash.Sum(nil)
return hex.EncodeToString(hashBytes), nil
}
func serveFile(w http.ResponseWriter, r *http.Request, path string) {
// Open the file with proper error handling
file, err := os.Open(path)
if err != nil {
log.Printf("\t\t\t\tError opening file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer file.Close()
// Get the visitor's fingerprint
fingerprint, err := getVisitorFingerprint(r)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Extract the filename and extension
filename := filepath.Base(path)
// Check if the fingerprint is already in the file
fingerprintPath := filepath.Join(filepath.Dir(path), "."+filename)
if fingerprintExists(fingerprintPath, fingerprint) {
// Fingerprint is already present, serve the file content and return
fileInfo, err := file.Stat()
if err != nil {
log.Printf("\t\t\t\tError getting file info: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
http.ServeContent(w, r, path, fileInfo.ModTime(), file)
return
}
// Append the fingerprint as a new line to the file
fingerprintFile, err := os.OpenFile(fingerprintPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
log.Printf("\t\t\t\tError opening or creating fingerprint file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer fingerprintFile.Close()
// Write the fingerprint as a new line to the file
_, err = fmt.Fprintln(fingerprintFile, fingerprint)
if err != nil {
log.Printf("\t\t\t\tError writing fingerprint to file: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Use http.ServeContent for efficient file serving
fileInfo, err := file.Stat()
if err != nil {
log.Printf("\t\t\t\tError getting file info: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
http.ServeContent(w, r, path, fileInfo.ModTime(), file)
}
// Function to check if a fingerprint already exists in the file
func fingerprintExists(fingerprintPath, fingerprint string) bool {
existingFingerprints, err := ioutil.ReadFile(fingerprintPath)
if err != nil {
return false // Assume the file does not exist
}
fingerprints := strings.Split(string(existingFingerprints), "\n")
for _, fp := range fingerprints {
if fp == fingerprint {
return true
}
}
return false
}
func listDirectory(w http.ResponseWriter, r *http.Request, basePath, relativePath , indexFilePath string) {
// Open the directory with proper error handling
dir, err := os.Open(basePath)
if err != nil {
log.Printf("\t\t\t\tError opening directory: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
defer dir.Close()
// Read the directory entries
files, err := dir.Readdir(-1)
if err != nil {
log.Printf("\t\t\t\tError reading directory: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Filter out files/directories starting with a dot
var visibleFiles []os.FileInfo
for _, file := range files {
if !strings.HasPrefix(file.Name(), ".") {
visibleFiles = append(visibleFiles, file)
}
}
// Sortiere alphabetisch
sort.Slice(visibleFiles, func(i, j int) bool {
return visibleFiles[i].Name() < visibleFiles[j].Name()
})
// Create a styled HTML page to list the directory contents
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, `
`)
fmt.Fprintf(w, " Index of %s
", relativePath)
fmt.Fprintln(w, "
Name | ") fmt.Fprintln(w, "Last Modified | ") fmt.Fprintln(w, "Size | ") fmt.Fprintln(w, "Visitors | ") fmt.Fprintln(w, "||
---|---|---|---|---|---|
Parent Directory | |||||
%s | ", linkPath, file.Name()) if fileInfo.IsDir() { // If it's a directory, set file size to an empty string fmt.Fprintf(w, "--- | ") } else { // If it's a file, display file date fmt.Fprintf(w, "%s | ", file.ModTime().Format("2006-01-02 15:04:05")) } if fileInfo.IsDir() { // If it's a directory, set file size to an empty string fmt.Fprintf(w, "--- | ") } else { // If it's a file, display file size fmt.Fprintf(w, "%d bytes | ", file.Size()) } fmt.Fprintf(w, "%v | ", visitorCount) fmt.Fprintln(w, "