Conversion to big ol map
This commit is contained in:
parent
e399f75453
commit
9ccc4b16f3
14 changed files with 10106 additions and 231 deletions
|
@ -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
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Task 4 Caddy Cache Extension
|
||||
|
53
data.go
Normal file
53
data.go
Normal 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
38
data_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
74
errors.go
74
errors.go
|
@ -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
6
go.mod
|
@ -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
14
go.sum
|
@ -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
147
guardian_extension.go
Normal 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)
|
||||
)
|
55
guardian_extension_test.go
Normal file
55
guardian_extension_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
3242
questions.json
Normal file
File diff suppressed because it is too large
Load diff
3242
testdata.json
Normal file
3242
testdata.json
Normal file
File diff suppressed because it is too large
Load diff
3242
testdata.json.original
Normal file
3242
testdata.json.original
Normal file
File diff suppressed because it is too large
Load diff
35
testdata.py
Normal file
35
testdata.py
Normal 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)
|
Loading…
Reference in a new issue