package guardianextension
import (
"fmt"
"html"
"net/http"
"sync"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
const authTokenHeader = "X-GuardianInternal-Token"
// init registers the GuardianCacheHandler module with Caddy and registers the "guardiancache" directive
// for use in the Caddyfile configuration.
func init() {
caddy.RegisterModule((*GuardianCacheHandler)(nil))
httpcaddyfile.RegisterHandlerDirective("guardiancache", parseCaddyfile)
}
// GuardianCacheHandler is a Caddy module that provides a cache handler for the Guardian application.
// It manages a cache of data stored in a JSON file, and validates access tokens.
// The mutex is necessary because maps are not thread-safe for concurrent read (and/or write) operations in Go.
type GuardianCacheHandler struct {
DataFile string `json:"data_file"`
validTokens map[string]bool
data map[string]string
mutex sync.RWMutex
}
// CaddyModule returns module information for use by Caddy.
func (handler *GuardianCacheHandler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.guardiancache",
New: func() caddy.Module { return new(GuardianCacheHandler) },
}
}
// Provision initializes the GuardianCacheHandler by loading data from a JSON file and setting up valid tokens.
func (handler *GuardianCacheHandler) Provision(ctx caddy.Context) error {
handler.validTokens = map[string]bool{
"token1": true,
"token2": true,
"token3": true,
}
errorResponsesOnce.Do(initErrorResponses)
// Load data from file
if err := handler.LoadData(); err != nil {
return fmt.Errorf("failed to load data: %v", err)
}
return nil
}
// validateAuthToken checks if the auth token is valid. If the token is valid, it returns the escaped token.
// If the token is invalid, it sends an error response and returns an empty string which returns nil in the calling ServeHTTP method.
func (handler *GuardianCacheHandler) validateAuthToken(authToken string) (string, error) {
if authToken == "" {
return "", ErrNoAuthToken
}
if len(authToken) > 256 {
return "", ErrUnauthorized
}
escapedAuthToken := html.EscapeString(authToken)
// TODO: Temporary - this isn't how validity will be checked
if !handler.validTokens[escapedAuthToken] {
return "", ErrUnauthorized
}
return escapedAuthToken, nil
}
// ServeHTTP is the HTTP handler for the GuardianCacheHandler module. It retrieves a value from the loaded JSON data
// based on the "q" query parameter provided in the request. If the q parameter is missing, it returns
// a 400 Bad Request error. If the value is not found in the loaded JSON data, it returns a 404 Not Found error.
// If there is an error interacting with the loaded JSON data, it returns a 500 Internal Server Error.
func (handler *GuardianCacheHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request, next caddyhttp.Handler) error {
// Step 1: Get authentication token from the request header and validate it
_, err := handler.validateAuthToken(request.Header.Get(authTokenHeader))
if err != nil {
sendJSONError(writer, err)
return nil
}
question := request.URL.Query().Get("q")
if question == "" {
sendJSONError(writer, ErrIncompleteReq)
return nil
}
if len(question) > 256 {
sendJSONError(writer, ErrConnFailed)
return nil
}
hashedQuestion := questionHash(question)
handler.mutex.RLock()
val, exists := handler.data[hashedQuestion]
handler.mutex.RUnlock()
if !exists {
sendJSONError(writer, ErrConnFailed)
return nil
}
fmt.Fprintln(writer, val)
return nil
}
// UnmarshalCaddyfile parses the Caddyfile configuration for the GuardianCacheHandler module.
// It sets the path to the JSON data file for the GuardianCacheHandler.
// The parameter "data_file" specifies the location of the JSON file to be loaded.
func (handler *GuardianCacheHandler) UnmarshalCaddyfile(dispenser *caddyfile.Dispenser) error {
for dispenser.Next() {
for dispenser.NextBlock(0) {
switch dispenser.Val() {
case "filepath":
if !dispenser.Args(&handler.DataFile) {
return dispenser.ArgErr()
}
default:
return dispenser.Errf("unrecognized parameter: %s", dispenser.Val())
}
}
}
return nil
}
// The GuardianCacheHandler type implements several interfaces that allow it to be used as a Caddy module:
// - caddy.Provisioner: Allows the module to be provisioned and configured.
// - caddyhttp.MiddlewareHandler: Allows the module to be used as HTTP middleware.
// - caddyfile.Unmarshaler: Allows the module to be configured from a Caddyfile.
var (
_ caddy.Provisioner = (*GuardianCacheHandler)(nil)
_ caddyhttp.MiddlewareHandler = (*GuardianCacheHandler)(nil)
_ caddyfile.Unmarshaler = (*GuardianCacheHandler)(nil)
)