Conversion to big ol map

This commit is contained in:
Nixon 2024-08-14 13:11:08 -07:00
parent e399f75453
commit 9ccc4b16f3
No known key found for this signature in database
14 changed files with 10106 additions and 231 deletions

View file

@ -1,22 +0,0 @@
steps:
- name: build
when:
branch: main
image: docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker build -t codebreaker/caddy-keydb:latest .
- name: deploy
when:
branch: main
image: docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker pull codebreaker/caddy-keydb:latest
- docker stop caddy-extension || true
- docker rm caddy-extension || true
- docker run -d --name caddy-extension -p 80:80 codebreaker/caddy-keydb:latest
depends_on: build

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# Task 4 Caddy Cache Extension

53
data.go Normal file
View file

@ -0,0 +1,53 @@
package guardianextension
import (
"encoding/hex"
"encoding/json"
"os"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/twmb/murmur3"
)
// LoadData loads the map from a JSON file.
func (handler *GuardianCacheHandler) LoadData() error {
file, err := os.Open(handler.DataFile)
if err != nil {
return err
}
defer file.Close()
decoder := json.NewDecoder(file)
return decoder.Decode(&handler.data)
}
// parseCaddyfile creates a new GuardianCacheHandler instance and initializes it by parsing the Caddyfile configuration.
// This function is used by Caddy to load the GuardianCacheHandler module from the Caddyfile. It's called once during init.
func parseCaddyfile(helper httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
handler := new(GuardianCacheHandler)
err := handler.UnmarshalCaddyfile(helper.Dispenser)
return handler, err
}
// questionHash generates a 32-byte hash string from the provided question string using the MurmurHash3 algorithm.
// The hash is encoded as a hexadecimal string and returned.
//
// Benchmarks
// cpu: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz
// BenchmarkStringSum128WithPreGeneratedData-12 6135975 179.3 ns/op
// BenchmarkSprintfWithPreGeneratedData-12 6602058 179.9 ns/op
// BenchmarkStrconvWithPreGeneratedData-12 10127676 123.0 ns/op
// BenchmarkBytesBufferWithPreGeneratedData-12 7616581 156.3 ns/op
// BenchmarkEncodingHexWithPreGeneratedData-12 18349746 64.95 ns/op <----
func questionHash(question string) string {
h1, h2 := murmur3.StringSum128(question)
hash := hex.EncodeToString([]byte{
byte(h1 >> 56), byte(h1 >> 48), byte(h1 >> 40), byte(h1 >> 32),
byte(h1 >> 24), byte(h1 >> 16), byte(h1 >> 8), byte(h1),
byte(h2 >> 56), byte(h2 >> 48), byte(h2 >> 40), byte(h2 >> 32),
byte(h2 >> 24), byte(h2 >> 16), byte(h2 >> 8), byte(h2),
})
// caddy.Log().Named("guardianextension").Sugar().Debugf("questionHash: %s -> %s", question, hash)
return hash
}

38
data_test.go Normal file
View file

@ -0,0 +1,38 @@
package guardianextension
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestQuestionHash(t *testing.T) {
tests := []struct {
question string
expected string
}{
{
question: "What is your name?",
expected: "3c42bcf12462b2875ba67f912cb680a2",
},
{
question: "How old are you?",
expected: "349383e43828e53221c3af2efb8715d4",
},
{
question: "",
expected: "00000000000000000000000000000000",
},
{
question: "The quick brown fox jumps over the lazy dog",
expected: "e34bbc7bbc071b6c7a433ca9c49a9347",
},
}
for _, tt := range tests {
t.Run(tt.question, func(t *testing.T) {
hash := questionHash(tt.question)
assert.Equal(t, tt.expected, hash)
})
}
}

View file

@ -1,4 +1,4 @@
package keydbextension
package guardianextension
import (
"encoding/json"
@ -7,51 +7,65 @@ import (
"sync"
)
const (
ErrNoAuthToken = "ErrNoAuthToken"
ErrUnauthorized = "ErrUnauthorized"
ErrIncompleteReq = "ErrIncompleteReq"
ErrConnFailed = "ErrConnFailed"
ErrReqNotProcessed = "ErrReqNotProcessed"
ErrUnknown = "ErrUnknown"
// GuardianError is a custom error type that includes an HTTP status code and a message.
type GuardianError struct {
Code int
Message string
}
func (e *GuardianError) Error() string {
return e.Message
}
var (
ErrNoAuthToken = &GuardianError{401, "No auth token provided"}
ErrUnauthorized = &GuardianError{403, "Unauthorized access"}
ErrIncompleteReq = &GuardianError{400, "Request was incomplete"}
ErrConnFailed = &GuardianError{523, "Connection to GuardianPT failed"}
ErrReqNotProcessed = &GuardianError{522, "Request could not be processed"}
ErrUnknown = &GuardianError{520, "An unknown error occurred"}
ErrAuthTokenTooLong = &GuardianError{494, "Invalid auth token length"}
)
var (
errorResponses map[string][]byte
errorStatusCodes map[string]int
errorResponsesOnce sync.Once
)
// initErrorResponses initializes the error response and status code maps.
// It defines a set of standard error responses and their associated HTTP status codes.
func initErrorResponses() {
errorResponses = make(map[string][]byte)
errorStatusCodes = make(map[string]int)
errors := []struct {
key string
code int
message string
}{
{ErrNoAuthToken, http.StatusUnauthorized, "No auth token provided"},
{ErrUnauthorized, http.StatusForbidden, "Unauthorized access"},
{ErrIncompleteReq, http.StatusBadRequest, "Request was incomplete"},
{ErrConnFailed, 523, "Connection to GuardianPT failed"},
{ErrReqNotProcessed, 522, "Request could not be processed"},
{ErrUnknown, 520, "An unknown error occurred"},
errors := []*GuardianError{
ErrNoAuthToken,
ErrUnauthorized,
ErrIncompleteReq,
ErrConnFailed,
ErrReqNotProcessed,
ErrUnknown,
ErrAuthTokenTooLong,
}
for _, err := range errors {
error_code := fmt.Sprintf("0x%08X", 0xC0043293+err.code)
error_code := fmt.Sprintf("0x%08X", 0xC0043293+err.Code)
response, _ := json.Marshal(map[string]interface{}{
"error": true,
"code": error_code,
"message": err.message,
"message": err.Message,
})
errorResponses[err.key] = response
errorStatusCodes[err.key] = err.code
// Overwrite error message as string for less space allocation
err.Message = string(response)
}
}
func getHTTPStatusCode(key string) int {
// sendJSONError writes a JSON-formatted error response to the provided http.ResponseWriter.
func sendJSONError(w http.ResponseWriter, err error) {
errorResponsesOnce.Do(initErrorResponses)
return errorStatusCodes[key]
customErr, ok := err.(*GuardianError)
if !ok {
customErr = ErrUnknown
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(customErr.Code)
w.Write([]byte(customErr.Message))
}

6
go.mod
View file

@ -4,7 +4,8 @@ go 1.21.4
require (
github.com/caddyserver/caddy/v2 v2.8.4
github.com/go-redis/redis/v8 v8.11.5
github.com/stretchr/testify v1.9.0
github.com/twmb/murmur3 v1.1.8
)
require (
@ -23,11 +24,11 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-kit/kit v0.13.0 // indirect
@ -66,6 +67,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect

14
go.sum
View file

@ -110,16 +110,12 @@ github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -136,8 +132,6 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -295,10 +289,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
@ -401,6 +391,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU=
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
@ -589,8 +581,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

147
guardian_extension.go Normal file
View file

@ -0,0 +1,147 @@
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)
)

View file

@ -0,0 +1,55 @@
package guardianextension
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateAuthToken(t *testing.T) {
handler := &GuardianCacheHandler{
validTokens: map[string]bool{
"validToken": true,
},
}
tests := []struct {
name string
authToken string
expected string
err error
}{
{
name: "Empty auth token",
authToken: "",
expected: "",
err: ErrNoAuthToken,
},
{
name: "Auth token too long",
authToken: string(make([]byte, 257)),
expected: "",
err: ErrUnauthorized,
},
{
name: "Invalid auth token",
authToken: "invalidToken",
expected: "",
err: ErrUnauthorized,
},
{
name: "Valid auth token",
authToken: "validToken",
expected: "validToken",
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := handler.validateAuthToken(tt.authToken)
assert.Equal(t, tt.err, err)
assert.Equal(t, tt.expected, result)
})
}
}

View file

@ -1,165 +0,0 @@
package keydbextension
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"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"
"github.com/go-redis/redis/v8"
)
const authTokenHeader = "X-GuardianInternal-Token"
// init registers the KeyDBHandler module with Caddy and registers the "keydb" directive
// for use in the Caddyfile configuration.
func init() {
caddy.RegisterModule(KeyDBHandler{})
httpcaddyfile.RegisterHandlerDirective("keydb", parseCaddyfile)
}
// KeyDBHandler is a Caddy module that provides a HTTP handler for interacting with a KeyDB server.
// It allows retrieving values from the KeyDB server based on a provided hash parameter.
type KeyDBHandler struct {
Address string `json:"address"`
Password string `json:"password"`
DB int `json:"db"`
client *redis.Client
validTokens map[string]bool
}
// CaddyModule returns module information for use by Caddy.
func (KeyDBHandler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.keydb",
New: func() caddy.Module { return new(KeyDBHandler) },
}
}
// Provision initializes the KeyDBHandler by creating a new Redis client with the configured address, password, and database index.
// The Redis client is the most robust and is backwards compatible with KeyDB.
func (handler *KeyDBHandler) Provision(ctx caddy.Context) error {
// The database index must be between 0 and 15 (inclusive).
if handler.DB < 0 || handler.DB > 15 {
return fmt.Errorf("invalid db value: %d", handler.DB)
}
handler.client = redis.NewClient(&redis.Options{
Addr: handler.Address,
Password: handler.Password,
DB: handler.DB,
})
handler.validTokens = map[string]bool{
"token1": true,
"token2": true,
"token3": true,
}
return nil
}
// ServeHTTP is the HTTP handler for the KeyDBHandler module. It retrieves a value from the KeyDB server
// 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 KeyDB server, it returns a 404 Not Found error.
// If there is an error interacting with the KeyDB server, it returns a 500 Internal Server Error.
func (handler KeyDBHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request, next caddyhttp.Handler) error {
authToken := request.Header.Get(authTokenHeader)
if authToken == "" {
sendJSONError(writer, ErrNoAuthToken)
return nil
}
if !handler.validTokens[authToken] {
sendJSONError(writer, ErrUnauthorized)
return nil
}
question := request.URL.Query().Get("q")
if question == "" {
sendJSONError(writer, ErrIncompleteReq)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
val, err := handler.client.Get(ctx, question).Result()
if err == redis.Nil {
sendJSONError(writer, ErrConnFailed)
return nil
} else if err != nil {
sendJSONError(writer, ErrReqNotProcessed)
return err
}
// Append the auth token to the response content
responseContent := fmt.Sprintf("%s\nAuth Token: %s", val, authToken)
// Write the returned value with the appended auth token to the response writer
fmt.Fprintln(writer, responseContent)
return nil
}
// UnmarshalCaddyfile parses the Caddyfile configuration for the KeyDBHandler module.
// It sets the address, password, and database index for the KeyDB connection.
// The database index must be between 0 and 15 (inclusive) and is converted to an integer here for the Redis client.
func (handler *KeyDBHandler) UnmarshalCaddyfile(dispenser *caddyfile.Dispenser) error {
for dispenser.Next() {
for dispenser.NextBlock(0) {
switch dispenser.Val() {
case "address":
if !dispenser.Args(&handler.Address) {
return dispenser.ArgErr()
}
case "password":
if !dispenser.Args(&handler.Password) {
return dispenser.ArgErr()
}
case "db":
var dbString string
if !dispenser.Args(&dbString) {
return dispenser.ArgErr()
}
db, err := strconv.Atoi(dbString)
if err != nil {
return err
}
handler.DB = db
}
}
}
return nil
}
// parseCaddyfile creates a new KeyDBHandler instance and initializes it by parsing the Caddyfile configuration.
// This function is used by Caddy to load the KeyDBHandler module from the Caddyfile. It's called once during init.
func parseCaddyfile(helper httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var handler KeyDBHandler
err := handler.UnmarshalCaddyfile(helper.Dispenser)
return handler, err
}
// sendJSONError writes a JSON-formatted error response to the provided http.ResponseWriter.
func sendJSONError(w http.ResponseWriter, errorKey string) {
errorResponse, exists := errorResponses[errorKey]
if !exists {
errorResponse = errorResponses["ErrUnknown"]
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(getHTTPStatusCode(errorKey))
w.Write(errorResponse)
}
// The KeyDBHandler 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 = (*KeyDBHandler)(nil)
_ caddyhttp.MiddlewareHandler = (*KeyDBHandler)(nil)
_ caddyfile.Unmarshaler = (*KeyDBHandler)(nil)
)

3242
questions.json Normal file

File diff suppressed because it is too large Load diff

3242
testdata.json Normal file

File diff suppressed because it is too large Load diff

3242
testdata.json.original Normal file

File diff suppressed because it is too large Load diff

35
testdata.py Normal file
View file

@ -0,0 +1,35 @@
import json
import pymmh3
import binascii
def question_hash(question):
# MurmurHash3 128-bit hash
hash_value = pymmh3.hash128(question, x64arch=True)
# Split the 128-bit integer into two 64-bit integers (high and low)
h1 = (hash_value >> 64) & 0xFFFFFFFFFFFFFFFF
h2 = hash_value & 0xFFFFFFFFFFFFFFFF
# Convert each part to a byte array in big endian order and concatenate
hash_bytes = h2.to_bytes(8, byteorder='big') + h1.to_bytes(8, byteorder='big')
# Convert the byte array to a hexadecimal string
return binascii.hexlify(hash_bytes).decode('utf-8')
# Read the testdata.json file
with open('testdata.json.original', 'r') as f:
data = json.load(f)
# Create the new dictionaries
questions = {}
hashed_data = {}
for question, answer in data.items():
hashed_question = question_hash(question)
questions[hashed_question] = question
hashed_data[hashed_question] = answer
# Write the questions.json file
with open('questions.json', 'w') as f:
json.dump(questions, f, indent=4)
# # Overwrite the testdata.json file
with open('testdata.json', 'w') as f:
json.dump(hashed_data, f, indent=4)