Update reason field handling in auth.go and db.go to properly support NULL values using sql.NullString. When inserting expired temp entries and serializing logs to JSON, extract the string value to avoid JSON marshaling issues with nullable fields.
281 lines
7.4 KiB
Go
281 lines
7.4 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
|
|
|
|
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, `action = ?`)
|
|
args = append(args, filterAction)
|
|
}
|
|
if filterIP != "" {
|
|
conditions = append(conditions, `ip = ?`)
|
|
args = append(args, filterIP)
|
|
}
|
|
|
|
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 string
|
|
var ip, cidr, reason 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.String,
|
|
}
|
|
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"))
|
|
}
|