mirror of
https://github.com/cubixle/gridlock.git
synced 2026-04-30 17:28:44 +01:00
added stats handler to read stats files
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -20,24 +23,16 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
stats = map[string]int{}
|
stats = map[string]int{}
|
||||||
ticker = time.NewTicker(10 * time.Second)
|
ticker = time.NewTicker(10 * time.Minute)
|
||||||
)
|
)
|
||||||
|
|
||||||
func getEnv(key, fallback string) string {
|
|
||||||
value := os.Getenv(key)
|
|
||||||
if value == "" {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
Level: slog.LevelInfo,
|
Level: slog.LevelDebug,
|
||||||
}))
|
}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
domain := getEnv("DOMAIN", "0.0.0.0:8070")
|
domain := cmp.Or(os.Getenv("DOMAIN"), "0.0.0.0:8070")
|
||||||
|
|
||||||
srv := http.NewServeMux()
|
srv := http.NewServeMux()
|
||||||
srv.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
srv.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -51,6 +46,8 @@ func main() {
|
|||||||
_, _ = w.Write([]byte(``))
|
_, _ = w.Write([]byte(``))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
srv.HandleFunc("/stats", fileHandler)
|
||||||
|
|
||||||
srv.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
srv.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// generate links1 to 7 from the names list and populate the template
|
// generate links1 to 7 from the names list and populate the template
|
||||||
// with the links
|
// with the links
|
||||||
@@ -61,7 +58,7 @@ func main() {
|
|||||||
"x_forwarded_for", r.Header.Get("X-Forwarded-For"),
|
"x_forwarded_for", r.Header.Get("X-Forwarded-For"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if !agents.IsCrawler(r.UserAgent()) {
|
if agents.IsCrawler(r.UserAgent()) {
|
||||||
slog.Info("crawler detected", "user_agent", r.UserAgent())
|
slog.Info("crawler detected", "user_agent", r.UserAgent())
|
||||||
// remove all commas from simpler CSV handling.
|
// remove all commas from simpler CSV handling.
|
||||||
ua := strings.ReplaceAll(r.UserAgent(), ",", "")
|
ua := strings.ReplaceAll(r.UserAgent(), ",", "")
|
||||||
@@ -73,7 +70,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentName := "Ziggy"
|
currentName := "Ziggy"
|
||||||
// get current name from the subdomai#e8c4c2n
|
// get current name from the subdomain
|
||||||
if strings.Contains(r.Host, ".") {
|
if strings.Contains(r.Host, ".") {
|
||||||
currentName = strings.Split(r.Host, ".")[0]
|
currentName = strings.Split(r.Host, ".")[0]
|
||||||
currentName = strings.ReplaceAll(currentName, "-", " ")
|
currentName = strings.ReplaceAll(currentName, "-", " ")
|
||||||
@@ -83,7 +80,7 @@ func main() {
|
|||||||
|
|
||||||
baseLink := "http://%s." + domain + "/"
|
baseLink := "http://%s." + domain + "/"
|
||||||
|
|
||||||
content := template
|
content := indexTemplate
|
||||||
content = strings.ReplaceAll(content, "{{img}}", img)
|
content = strings.ReplaceAll(content, "{{img}}", img)
|
||||||
content = strings.ReplaceAll(content, "{{current_name}}", currentName)
|
content = strings.ReplaceAll(content, "{{current_name}}", currentName)
|
||||||
|
|
||||||
@@ -127,12 +124,91 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeStatsToFile() error {
|
func safeJoin(baseDir, targetDir string) (string, error) {
|
||||||
fileDir := getEnv("LOG_FILE_DIR", "./logs/helprob")
|
// Clean and absolute paths
|
||||||
if fileDir == "" {
|
basePath, err := filepath.Abs(filepath.Clean(baseDir))
|
||||||
return nil
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetPath, err := filepath.Abs(filepath.Clean(filepath.Join(basePath, targetDir)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent directory traversal
|
||||||
|
if !strings.HasPrefix(targetPath, basePath) {
|
||||||
|
return "", fmt.Errorf("invalid directory traversal attempt")
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileDir := cmp.Or(os.Getenv("LOG_FILE_DIR"), "logs/helprob")
|
||||||
|
|
||||||
|
requestedDir, err := url.QueryUnescape(r.URL.Query().Get("dir"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid path.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := safeJoin(fileDir, requestedDir)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid path.", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("reading path", "path", path)
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "File not found.", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("reading path", "path", path, "info", fileInfo)
|
||||||
|
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Could not read directory.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]string, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
entries = append(entries, url.QueryEscape(file.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileTemplate.Execute(w, struct {
|
||||||
|
BaseDir string
|
||||||
|
Path string
|
||||||
|
Entries []string
|
||||||
|
}{
|
||||||
|
BaseDir: fileDir,
|
||||||
|
Path: requestedDir,
|
||||||
|
Entries: entries,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Handle CSV file viewing
|
||||||
|
if strings.HasSuffix(path, ".csv") {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Could not read file.", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Write(data)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Unsupported file type.", http.StatusUnsupportedMediaType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStatsToFile() error {
|
||||||
|
fileDir := cmp.Or(os.Getenv("LOG_FILE_DIR"), "./logs/helprob")
|
||||||
|
|
||||||
slog.Info("Configured to write stats file", "destination", fileDir)
|
slog.Info("Configured to write stats file", "destination", fileDir)
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
@@ -206,7 +282,21 @@ func writeStatsToFile() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var template = `
|
var fileTemplate = template.Must(template.New("files").Parse(`
|
||||||
|
<html>
|
||||||
|
<head><title>Stats</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Stats</h1>
|
||||||
|
<ul>
|
||||||
|
{{- range .Entries}}
|
||||||
|
<li><a href="/stats?dir={{$.Path}}/{{.}}">{{.}}</a></li>
|
||||||
|
{{- end}}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`))
|
||||||
|
|
||||||
|
var indexTemplate = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
Reference in New Issue
Block a user