The logsHandler now accepts query parameters to filter logs by action and IP address, with support for pagination via limit and offset parameters. Query parameters are safely bound to prepared statements to prevent SQL injection, and pagination values are validated to ensure they fall within acceptable ranges.
284 lines
7.5 KiB
Go
284 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func CheckAuth(db *sql.DB, r *http.Request) bool {
|
|
ip := r.Header.Get("X-Real-IP")
|
|
if ip == "" {
|
|
return false
|
|
}
|
|
ok, err := IsPermanentWhitelisted(db, ip)
|
|
if err == nil && ok {
|
|
return true
|
|
}
|
|
ok, err = IsTempWhitelisted(db, ip)
|
|
if err == nil && ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func authHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !CheckAuth(db, r) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func whitelistTempHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
type request struct {
|
|
IP string `json:"ip"`
|
|
TTLSeconds int `json:"ttl_seconds"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
var req request
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.IP == "" || req.TTLSeconds <= 0 {
|
|
http.Error(w, "ip and ttl_seconds required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
clientIP := getClientIP(r)
|
|
if err := AddTempEntry(db, req.IP, req.TTLSeconds, req.Reason, clientIP); err != nil {
|
|
slog.Error("failed to add temp entry", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
slog.Info("temp whitelist added", "ip", req.IP, "ttl", req.TTLSeconds)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func whitelistListHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
entries, err := ListTempEntries(db)
|
|
if err != nil {
|
|
http.Error(w, "database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(entries)
|
|
}
|
|
}
|
|
|
|
func permWhitelistListHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
entries, err := ListPermEntries(db)
|
|
if err != nil {
|
|
http.Error(w, "database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(entries)
|
|
}
|
|
}
|
|
|
|
func whitelistDeleteHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
ip := r.PathValue("ip")
|
|
if ip == "" {
|
|
http.Error(w, "ip required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
clientIP := getClientIP(r)
|
|
if err := DeleteTempEntry(db, ip, clientIP); err != nil {
|
|
slog.Error("failed to delete temp entry", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
slog.Info("temp whitelist deleted", "ip", ip)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func logsHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
query := `SELECT timestamp, action, ip, cidr, ttl_seconds, reason, api_client_ip FROM audit_log`
|
|
var args []any
|
|
argIndex := 1
|
|
|
|
filterAction := r.URL.Query().Get("action")
|
|
filterIP := r.URL.Query().Get("ip")
|
|
limitStr := r.URL.Query().Get("limit")
|
|
offsetStr := r.URL.Query().Get("offset")
|
|
|
|
var conditions []string
|
|
if filterAction != "" {
|
|
conditions = append(conditions, fmt.Sprintf(`action = $%d`, argIndex))
|
|
args = append(args, filterAction)
|
|
argIndex++
|
|
}
|
|
if filterIP != "" {
|
|
conditions = append(conditions, fmt.Sprintf(`ip = $%d`, argIndex))
|
|
args = append(args, filterIP)
|
|
argIndex++
|
|
}
|
|
|
|
if len(conditions) > 0 {
|
|
query += " WHERE " + strings.Join(conditions, " AND ")
|
|
}
|
|
query += ` ORDER BY timestamp DESC`
|
|
|
|
limit := 200
|
|
offset := 0
|
|
if limitStr != "" {
|
|
if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 1000 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
if offsetStr != "" {
|
|
if parsed, err := strconv.Atoi(offsetStr); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
query += fmt.Sprintf(` LIMIT %d OFFSET %d`, limit, offset)
|
|
|
|
rows, err := db.Query(query, args...)
|
|
if err != nil {
|
|
slog.Error("failed to query logs", "error", err)
|
|
http.Error(w, "database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
logs := make([]map[string]interface{}, 0)
|
|
for rows.Next() {
|
|
var ts time.Time
|
|
var action, apiClientIP, reason string
|
|
var ip, cidr sql.NullString
|
|
var ttlSeconds sql.NullInt64
|
|
if err := rows.Scan(&ts, &action, &ip, &cidr, &ttlSeconds, &reason, &apiClientIP); err != nil {
|
|
continue
|
|
}
|
|
entry := map[string]interface{}{
|
|
"timestamp": ts,
|
|
"action": action,
|
|
"api_client_ip": apiClientIP,
|
|
"reason": reason,
|
|
}
|
|
if ip.Valid {
|
|
entry["ip"] = ip.String
|
|
}
|
|
if cidr.Valid {
|
|
entry["cidr"] = cidr.String
|
|
}
|
|
if ttlSeconds.Valid {
|
|
entry["ttl_seconds"] = ttlSeconds.Int64
|
|
}
|
|
logs = append(logs, entry)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(logs)
|
|
}
|
|
}
|
|
|
|
func permWhitelistAddHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
type request struct {
|
|
Entry string `json:"entry"`
|
|
}
|
|
var req request
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Entry == "" {
|
|
http.Error(w, "entry required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
ok, err := AddPermanentEntry(db, req.Entry, getClientIP(r))
|
|
if err != nil {
|
|
slog.Error("failed to add perm entry", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if !ok {
|
|
http.Error(w, "entry already exists", http.StatusConflict)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func permWhitelistDeleteHandler(cfg Config, db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !verifyAPIKey(r, cfg.APIToken) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
entry := r.PathValue("entry")
|
|
if entry == "" {
|
|
http.Error(w, "entry required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := DeletePermanentEntry(db, entry, getClientIP(r)); err != nil {
|
|
slog.Error("failed to delete perm entry", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func getClientIP(r *http.Request) string {
|
|
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
|
return ip
|
|
}
|
|
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
return ip
|
|
}
|
|
|
|
func verifyAPIKey(r *http.Request, expectedToken string) bool {
|
|
return r.Header.Get("Authorization") == "Bearer "+expectedToken
|
|
}
|
|
|
|
func statusHandler(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Write([]byte("ok\n"))
|
|
}
|