feat: API-first approach for REST interface (#1129)

* feat: embed OpenAPI definition file

* feat: use OpenAPI generated server and client

* feat: provide OpenAPI interface documentation

* chore(test): add additional tests
This commit is contained in:
Dimitri Herzog 2023-09-09 19:30:55 +02:00 committed by GitHub
parent 245bb613df
commit 72d747c16f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 6185 additions and 9313 deletions

View File

@ -1,7 +1,6 @@
bin
dist
site
docs
node_modules
.git
.idea

2
.gitignore vendored
View File

@ -5,8 +5,6 @@
/*.pem
bin/
dist/
docs/swagger.json
docs/swagger.yaml
docs/docs.go
site/
config.yml

View File

@ -1,4 +1,4 @@
.PHONY: all clean generate build swagger test e2e-test lint run fmt docker-build help
.PHONY: all clean generate build test e2e-test lint run fmt docker-build help
.DEFAULT_GOAL:=help
VERSION?=$(shell git describe --always --tags)
@ -31,12 +31,6 @@ all: build test lint ## Build binary (with tests)
clean: ## cleans output directory
rm -rf $(BIN_OUT_DIR)/*
swagger: ## creates swagger documentation as html file
npm install bootprint bootprint-openapi html-inline
go run github.com/swaggo/swag/cmd/swag init -g api/api.go
$(shell) node_modules/bootprint/bin/bootprint.js openapi docs/swagger.json /tmp/swagger/
$(shell) node_modules/html-inline/bin/cmd.js /tmp/swagger/index.html > docs/swagger.html
serve_docs: ## serves online docs
pip install mkdocs-material
mkdocs serve

View File

@ -1,63 +0,0 @@
// @title blocky API
// @description blocky API
// @contact.name blocky@github
// @contact.url https://github.com/0xERR0R/blocky
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @BasePath /api/
// Package api provides basic API structs for the REST services
package api
const (
// PathBlockingStatusPath defines the REST endpoint for blocking status
PathBlockingStatusPath = "/api/blocking/status"
// PathBlockingEnablePath defines the REST endpoint for blocking enable
PathBlockingEnablePath = "/api/blocking/enable"
// PathBlockingDisablePath defines the REST endpoint for blocking disable
PathBlockingDisablePath = "/api/blocking/disable"
// PathListsRefresh defines the REST endpoint for blocking refresh
PathListsRefresh = "/api/lists/refresh"
// PathQueryPath defines the REST endpoint for query
PathQueryPath = "/api/query"
// PathDohQuery DoH Url
PathDohQuery = "/dns-query"
)
// QueryRequest is a data structure for a DNS request
type QueryRequest struct {
// query for DNS request
Query string `json:"query"`
// request type (A, AAAA, ...)
Type string `json:"type"`
}
// QueryResult is a data structure for the DNS result
type QueryResult struct {
// blocky reason for resolution
Reason string `json:"reason"`
// response type (CACHED, BLOCKED, ...)
ResponseType string `json:"responseType"`
// actual DNS response
Response string `json:"response"`
// DNS return code (NOERROR, NXDOMAIN, ...)
ReturnCode string `json:"returnCode"`
}
// BlockingStatus represents the current blocking status
type BlockingStatus struct {
// True if blocking is enabled
Enabled bool `json:"enabled"`
// Disabled group names
DisabledGroups []string `json:"disabledGroups"`
// If blocking is temporary disabled: amount of seconds until blocking will be enabled
AutoEnableInSec uint `json:"autoEnableInSec"`
}

687
api/api_client.gen.go Normal file
View File

@ -0,0 +1,687 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.14.0 DO NOT EDIT.
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/oapi-codegen/runtime"
)
// RequestEditorFn is the function signature for the RequestEditor callback function
type RequestEditorFn func(ctx context.Context, req *http.Request) error
// Doer performs HTTP requests.
//
// The standard http.Client implements this interface.
type HttpRequestDoer interface {
Do(req *http.Request) (*http.Response, error)
}
// Client which conforms to the OpenAPI3 specification for this service.
type Client struct {
// The endpoint of the server conforming to this interface, with scheme,
// https://api.deepmap.com for example. This can contain a path relative
// to the server, such as https://api.deepmap.com/dev-test, and all the
// paths in the swagger spec will be appended to the server.
Server string
// Doer for performing requests, typically a *http.Client with any
// customized settings, such as certificate chains.
Client HttpRequestDoer
// A list of callbacks for modifying requests which are generated before sending over
// the network.
RequestEditors []RequestEditorFn
}
// ClientOption allows setting custom parameters during construction
type ClientOption func(*Client) error
// Creates a new Client, with reasonable defaults
func NewClient(server string, opts ...ClientOption) (*Client, error) {
// create a client with sane default values
client := Client{
Server: server,
}
// mutate client and add all optional params
for _, o := range opts {
if err := o(&client); err != nil {
return nil, err
}
}
// ensure the server URL always has a trailing slash
if !strings.HasSuffix(client.Server, "/") {
client.Server += "/"
}
// create httpClient, if not already present
if client.Client == nil {
client.Client = &http.Client{}
}
return &client, nil
}
// WithHTTPClient allows overriding the default Doer, which is
// automatically created using http.Client. This is useful for tests.
func WithHTTPClient(doer HttpRequestDoer) ClientOption {
return func(c *Client) error {
c.Client = doer
return nil
}
}
// WithRequestEditorFn allows setting up a callback function, which will be
// called right before sending the request. This can be used to mutate the request.
func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
return func(c *Client) error {
c.RequestEditors = append(c.RequestEditors, fn)
return nil
}
}
// The interface specification for the client above.
type ClientInterface interface {
// DisableBlocking request
DisableBlocking(ctx context.Context, params *DisableBlockingParams, reqEditors ...RequestEditorFn) (*http.Response, error)
// EnableBlocking request
EnableBlocking(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// BlockingStatus request
BlockingStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// ListRefresh request
ListRefresh(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// QueryWithBody request with any body
QueryWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
Query(ctx context.Context, body QueryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
}
func (c *Client) DisableBlocking(ctx context.Context, params *DisableBlockingParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewDisableBlockingRequest(c.Server, params)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) EnableBlocking(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewEnableBlockingRequest(c.Server)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) BlockingStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewBlockingStatusRequest(c.Server)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) ListRefresh(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewListRefreshRequest(c.Server)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) QueryWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewQueryRequestWithBody(c.Server, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) Query(ctx context.Context, body QueryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewQueryRequest(c.Server, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
// NewDisableBlockingRequest generates requests for DisableBlocking
func NewDisableBlockingRequest(server string, params *DisableBlockingParams) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/blocking/disable")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
if params != nil {
queryValues := queryURL.Query()
if params.Duration != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "duration", runtime.ParamLocationQuery, *params.Duration); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
}
if params.Groups != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "groups", runtime.ParamLocationQuery, *params.Groups); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
}
queryURL.RawQuery = queryValues.Encode()
}
req, err := http.NewRequest("GET", queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewEnableBlockingRequest generates requests for EnableBlocking
func NewEnableBlockingRequest(server string) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/blocking/enable")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewBlockingStatusRequest generates requests for BlockingStatus
func NewBlockingStatusRequest(server string) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/blocking/status")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewListRefreshRequest generates requests for ListRefresh
func NewListRefreshRequest(server string) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/lists/refresh")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), nil)
if err != nil {
return nil, err
}
return req, nil
}
// NewQueryRequest calls the generic Query builder with application/json body
func NewQueryRequest(server string, body QueryJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(buf)
return NewQueryRequestWithBody(server, "application/json", bodyReader)
}
// NewQueryRequestWithBody generates requests for Query with any type of body
func NewQueryRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/query")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
for _, r := range c.RequestEditors {
if err := r(ctx, req); err != nil {
return err
}
}
for _, r := range additionalEditors {
if err := r(ctx, req); err != nil {
return err
}
}
return nil
}
// ClientWithResponses builds on ClientInterface to offer response payloads
type ClientWithResponses struct {
ClientInterface
}
// NewClientWithResponses creates a new ClientWithResponses, which wraps
// Client with return type handling
func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) {
client, err := NewClient(server, opts...)
if err != nil {
return nil, err
}
return &ClientWithResponses{client}, nil
}
// WithBaseURL overrides the baseURL.
func WithBaseURL(baseURL string) ClientOption {
return func(c *Client) error {
newBaseURL, err := url.Parse(baseURL)
if err != nil {
return err
}
c.Server = newBaseURL.String()
return nil
}
}
// ClientWithResponsesInterface is the interface specification for the client with responses above.
type ClientWithResponsesInterface interface {
// DisableBlockingWithResponse request
DisableBlockingWithResponse(ctx context.Context, params *DisableBlockingParams, reqEditors ...RequestEditorFn) (*DisableBlockingResponse, error)
// EnableBlockingWithResponse request
EnableBlockingWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*EnableBlockingResponse, error)
// BlockingStatusWithResponse request
BlockingStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*BlockingStatusResponse, error)
// ListRefreshWithResponse request
ListRefreshWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListRefreshResponse, error)
// QueryWithBodyWithResponse request with any body
QueryWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*QueryResponse, error)
QueryWithResponse(ctx context.Context, body QueryJSONRequestBody, reqEditors ...RequestEditorFn) (*QueryResponse, error)
}
type DisableBlockingResponse struct {
Body []byte
HTTPResponse *http.Response
}
// Status returns HTTPResponse.Status
func (r DisableBlockingResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r DisableBlockingResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type EnableBlockingResponse struct {
Body []byte
HTTPResponse *http.Response
}
// Status returns HTTPResponse.Status
func (r EnableBlockingResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r EnableBlockingResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type BlockingStatusResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *ApiBlockingStatus
}
// Status returns HTTPResponse.Status
func (r BlockingStatusResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r BlockingStatusResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type ListRefreshResponse struct {
Body []byte
HTTPResponse *http.Response
}
// Status returns HTTPResponse.Status
func (r ListRefreshResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r ListRefreshResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type QueryResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *ApiQueryResult
}
// Status returns HTTPResponse.Status
func (r QueryResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r QueryResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
// DisableBlockingWithResponse request returning *DisableBlockingResponse
func (c *ClientWithResponses) DisableBlockingWithResponse(ctx context.Context, params *DisableBlockingParams, reqEditors ...RequestEditorFn) (*DisableBlockingResponse, error) {
rsp, err := c.DisableBlocking(ctx, params, reqEditors...)
if err != nil {
return nil, err
}
return ParseDisableBlockingResponse(rsp)
}
// EnableBlockingWithResponse request returning *EnableBlockingResponse
func (c *ClientWithResponses) EnableBlockingWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*EnableBlockingResponse, error) {
rsp, err := c.EnableBlocking(ctx, reqEditors...)
if err != nil {
return nil, err
}
return ParseEnableBlockingResponse(rsp)
}
// BlockingStatusWithResponse request returning *BlockingStatusResponse
func (c *ClientWithResponses) BlockingStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*BlockingStatusResponse, error) {
rsp, err := c.BlockingStatus(ctx, reqEditors...)
if err != nil {
return nil, err
}
return ParseBlockingStatusResponse(rsp)
}
// ListRefreshWithResponse request returning *ListRefreshResponse
func (c *ClientWithResponses) ListRefreshWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListRefreshResponse, error) {
rsp, err := c.ListRefresh(ctx, reqEditors...)
if err != nil {
return nil, err
}
return ParseListRefreshResponse(rsp)
}
// QueryWithBodyWithResponse request with arbitrary body returning *QueryResponse
func (c *ClientWithResponses) QueryWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*QueryResponse, error) {
rsp, err := c.QueryWithBody(ctx, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseQueryResponse(rsp)
}
func (c *ClientWithResponses) QueryWithResponse(ctx context.Context, body QueryJSONRequestBody, reqEditors ...RequestEditorFn) (*QueryResponse, error) {
rsp, err := c.Query(ctx, body, reqEditors...)
if err != nil {
return nil, err
}
return ParseQueryResponse(rsp)
}
// ParseDisableBlockingResponse parses an HTTP response from a DisableBlockingWithResponse call
func ParseDisableBlockingResponse(rsp *http.Response) (*DisableBlockingResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &DisableBlockingResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
return response, nil
}
// ParseEnableBlockingResponse parses an HTTP response from a EnableBlockingWithResponse call
func ParseEnableBlockingResponse(rsp *http.Response) (*EnableBlockingResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &EnableBlockingResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
return response, nil
}
// ParseBlockingStatusResponse parses an HTTP response from a BlockingStatusWithResponse call
func ParseBlockingStatusResponse(rsp *http.Response) (*BlockingStatusResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &BlockingStatusResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest ApiBlockingStatus
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
}
return response, nil
}
// ParseListRefreshResponse parses an HTTP response from a ListRefreshWithResponse call
func ParseListRefreshResponse(rsp *http.Response) (*ListRefreshResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &ListRefreshResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
return response, nil
}
// ParseQueryResponse parses an HTTP response from a QueryWithResponse call
func ParseQueryResponse(rsp *http.Response) (*QueryResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &QueryResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest ApiQueryResult
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
}
return response, nil
}

View File

@ -1,152 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/util"
"github.com/go-chi/chi/v5"
)
const (
contentTypeHeader = "content-type"
jsonContentType = "application/json"
)
// BlockingControl interface to control the blocking status
type BlockingControl interface {
EnableBlocking()
DisableBlocking(duration time.Duration, disableGroups []string) error
BlockingStatus() BlockingStatus
}
// ListRefresher interface to control the list refresh
type ListRefresher interface {
RefreshLists()
}
// BlockingEndpoint endpoint for the blocking status control
type BlockingEndpoint struct {
control BlockingControl
}
// ListRefreshEndpoint endpoint for list refresh
type ListRefreshEndpoint struct {
refresher ListRefresher
}
// RegisterEndpoint registers an implementation as HTTP endpoint
func RegisterEndpoint(router chi.Router, t interface{}) {
if a, ok := t.(BlockingControl); ok {
registerBlockingEndpoints(router, a)
}
if a, ok := t.(ListRefresher); ok {
registerListRefreshEndpoints(router, a)
}
}
func registerListRefreshEndpoints(router chi.Router, refresher ListRefresher) {
l := &ListRefreshEndpoint{refresher}
router.Post(PathListsRefresh, l.apiListRefresh)
}
// apiListRefresh is the http endpoint to trigger the refresh of all lists
// @Summary List refresh
// @Description Refresh all lists
// @Tags lists
// @Success 200 "Lists were reloaded"
// @Router /lists/refresh [post]
func (l *ListRefreshEndpoint) apiListRefresh(rw http.ResponseWriter, _ *http.Request) {
rw.Header().Set(contentTypeHeader, jsonContentType)
l.refresher.RefreshLists()
}
func registerBlockingEndpoints(router chi.Router, control BlockingControl) {
s := &BlockingEndpoint{control}
// register API endpoints
router.Get(PathBlockingEnablePath, s.apiBlockingEnable)
router.Get(PathBlockingDisablePath, s.apiBlockingDisable)
router.Get(PathBlockingStatusPath, s.apiBlockingStatus)
}
// apiBlockingEnable is the http endpoint to enable the blocking status
// @Summary Enable blocking
// @Description enable the blocking status
// @Tags blocking
// @Success 200 "Blocking is enabled"
// @Router /blocking/enable [get]
func (s *BlockingEndpoint) apiBlockingEnable(rw http.ResponseWriter, _ *http.Request) {
log.Log().Info("enabling blocking...")
s.control.EnableBlocking()
rw.Header().Set(contentTypeHeader, jsonContentType)
}
// apiBlockingDisable is the http endpoint to disable the blocking status
// @Summary Disable blocking
// @Description disable the blocking status
// @Tags blocking
// @Param duration query string false "duration of blocking (Example: 300s, 5m, 1h, 5m30s)" Format(duration)
// @Param groups query string false "groups to disable (comma separated). If empty, disable all groups" Format(string)
// @Success 200 "Blocking is disabled"
// @Failure 400 "Wrong duration format"
// @Failure 400 "Unknown group"
// @Router /blocking/disable [get]
func (s *BlockingEndpoint) apiBlockingDisable(rw http.ResponseWriter, req *http.Request) {
var (
duration time.Duration
groups []string
err error
)
rw.Header().Set(contentTypeHeader, jsonContentType)
// parse duration from query parameter
durationParam := req.URL.Query().Get("duration")
if len(durationParam) > 0 {
duration, err = time.ParseDuration(durationParam)
if err != nil {
log.Log().Errorf("wrong duration format '%s'", log.EscapeInput(durationParam))
rw.WriteHeader(http.StatusBadRequest)
return
}
}
groupsParam := req.URL.Query().Get("groups")
if len(groupsParam) > 0 {
groups = strings.Split(groupsParam, ",")
}
err = s.control.DisableBlocking(duration, groups)
if err != nil {
log.Log().Error("can't disable the blocking: ", log.EscapeInput(err.Error()))
rw.WriteHeader(http.StatusBadRequest)
}
}
// apiBlockingStatus is the http endpoint to get current blocking status
// @Summary Blocking status
// @Description get current blocking status
// @Tags blocking
// @Produce json
// @Success 200 {object} api.BlockingStatus "Returns current blocking status"
// @Router /blocking/status [get]
func (s *BlockingEndpoint) apiBlockingStatus(rw http.ResponseWriter, _ *http.Request) {
status := s.control.BlockingStatus()
rw.Header().Set(contentTypeHeader, jsonContentType)
response, err := json.Marshal(status)
util.LogOnError("unable to marshal response ", err)
_, err = rw.Write(response)
util.LogOnError("unable to write response ", err)
}

View File

@ -1,147 +0,0 @@
package api
import (
"encoding/json"
"net/http"
"time"
. "github.com/0xERR0R/blocky/helpertest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/go-chi/chi/v5"
)
type BlockingControlMock struct {
enabled bool
}
type ListRefreshMock struct {
refreshTriggered bool
}
func (l *ListRefreshMock) RefreshLists() {
l.refreshTriggered = true
}
func (b *BlockingControlMock) EnableBlocking() {
b.enabled = true
}
func (b *BlockingControlMock) DisableBlocking(time.Duration, []string) error {
b.enabled = false
return nil
}
func (b *BlockingControlMock) BlockingStatus() BlockingStatus {
return BlockingStatus{Enabled: b.enabled}
}
var _ = Describe("API tests", func() {
Describe("Register router", func() {
RegisterEndpoint(chi.NewRouter(), &BlockingControlMock{})
RegisterEndpoint(chi.NewRouter(), &ListRefreshMock{})
})
Describe("Lists API", func() {
When("List refresh is called", func() {
r := &ListRefreshMock{}
sut := &ListRefreshEndpoint{refresher: r}
It("should trigger the list refresh", func() {
resp, _ := DoGetRequest("/api/lists/refresh", sut.apiListRefresh)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
})
})
Describe("Control blocking status via API", func() {
var (
bc *BlockingControlMock
sut *BlockingEndpoint
)
BeforeEach(func() {
bc = &BlockingControlMock{enabled: true}
sut = &BlockingEndpoint{control: bc}
})
When("Disable blocking is called", func() {
It("should disable blocking resolver", func() {
By("Calling Rest API to deactivate", func() {
resp, _ := DoGetRequest("/api/blocking/disable", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
})
})
When("Disable blocking is called with a wrong parameter", func() {
It("Should return http bad request as return code", func() {
resp, _ := DoGetRequest("/api/blocking/disable?duration=xyz", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusBadRequest))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
})
When("Disable blocking is called with a duration parameter", func() {
It("Should disable blocking only for the passed amount of time", func() {
By("ensure that the blocking status is active", func() {
Expect(bc.enabled).Should(BeTrue())
})
By("Calling Rest API to deactivate blocking for 0.5 sec", func() {
resp, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
By("ensure that the blocking is disabled", func() {
// now is blocking disabled
Expect(bc.enabled).Should(BeFalse())
})
})
})
When("Blocking status is called", func() {
It("should return correct status", func() {
By("enable blocking via API", func() {
resp, _ := DoGetRequest("/api/blocking/enable", sut.apiBlockingEnable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
By("Query blocking status via API should return 'enabled'", func() {
resp, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
var result BlockingStatus
err := json.NewDecoder(body).Decode(&result)
Expect(err).Should(Succeed())
Expect(result.Enabled).Should(BeTrue())
})
By("disable blocking via API", func() {
resp, _ := DoGetRequest("/api/blocking/disable?duration=500ms", sut.apiBlockingDisable)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
})
By("Query blocking status via API again should return 'disabled'", func() {
resp, body := DoGetRequest("/api/blocking/status", sut.apiBlockingStatus)
Expect(resp).Should(HaveHTTPStatus(http.StatusOK))
Expect(resp).Should(HaveHTTPHeaderWithValue("Content-type", "application/json"))
var result BlockingStatus
err := json.NewDecoder(body).Decode(&result)
Expect(err).Should(Succeed())
Expect(result.Enabled).Should(BeFalse())
})
})
})
})
})

147
api/api_interface_impl.go Normal file
View File

@ -0,0 +1,147 @@
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=types.cfg.yaml ../docs/api/openapi.yaml
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=server.cfg.yaml ../docs/api/openapi.yaml
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen --config=client.cfg.yaml ../docs/api/openapi.yaml
package api
import (
"context"
"fmt"
"strings"
"time"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/go-chi/chi/v5"
"github.com/miekg/dns"
)
// BlockingStatus represents the current blocking status
type BlockingStatus struct {
// True if blocking is enabled
Enabled bool
// Disabled group names
DisabledGroups []string
// If blocking is temporary disabled: amount of seconds until blocking will be enabled
AutoEnableInSec int
}
// BlockingControl interface to control the blocking status
type BlockingControl interface {
EnableBlocking()
DisableBlocking(duration time.Duration, disableGroups []string) error
BlockingStatus() BlockingStatus
}
// ListRefresher interface to control the list refresh
type ListRefresher interface {
RefreshLists() error
}
type Querier interface {
Query(question string, qType dns.Type) (*model.Response, error)
}
func RegisterOpenAPIEndpoints(router chi.Router, impl StrictServerInterface) {
HandlerFromMuxWithBaseURL(NewStrictHandler(impl, nil), router, "/api")
}
type OpenAPIInterfaceImpl struct {
control BlockingControl
querier Querier
refresher ListRefresher
}
func NewOpenAPIInterfaceImpl(control BlockingControl, querier Querier, refresher ListRefresher) *OpenAPIInterfaceImpl {
return &OpenAPIInterfaceImpl{
control: control,
querier: querier,
refresher: refresher,
}
}
func (i *OpenAPIInterfaceImpl) DisableBlocking(_ context.Context,
request DisableBlockingRequestObject,
) (DisableBlockingResponseObject, error) {
var (
duration time.Duration
groups []string
err error
)
if request.Params.Duration != nil {
duration, err = time.ParseDuration(*request.Params.Duration)
if err != nil {
return DisableBlocking400TextResponse(log.EscapeInput(err.Error())), nil
}
}
if request.Params.Groups != nil {
groups = strings.Split(*request.Params.Groups, ",")
}
err = i.control.DisableBlocking(duration, groups)
if err != nil {
return DisableBlocking400TextResponse(log.EscapeInput(err.Error())), nil
}
return DisableBlocking200Response{}, nil
}
func (i *OpenAPIInterfaceImpl) EnableBlocking(_ context.Context, _ EnableBlockingRequestObject,
) (EnableBlockingResponseObject, error) {
i.control.EnableBlocking()
return EnableBlocking200Response{}, nil
}
func (i *OpenAPIInterfaceImpl) BlockingStatus(_ context.Context, _ BlockingStatusRequestObject,
) (BlockingStatusResponseObject, error) {
blStatus := i.control.BlockingStatus()
result := ApiBlockingStatus{
Enabled: blStatus.Enabled,
}
if blStatus.AutoEnableInSec > 0 {
result.AutoEnableInSec = &blStatus.AutoEnableInSec
}
if len(blStatus.DisabledGroups) > 0 {
result.DisabledGroups = &blStatus.DisabledGroups
}
return BlockingStatus200JSONResponse(result), nil
}
func (i *OpenAPIInterfaceImpl) ListRefresh(_ context.Context,
_ ListRefreshRequestObject,
) (ListRefreshResponseObject, error) {
err := i.refresher.RefreshLists()
if err != nil {
return ListRefresh500TextResponse(log.EscapeInput(err.Error())), nil
}
return ListRefresh200Response{}, nil
}
func (i *OpenAPIInterfaceImpl) Query(_ context.Context, request QueryRequestObject) (QueryResponseObject, error) {
qType := dns.Type(dns.StringToType[request.Body.Type])
if qType == dns.Type(dns.TypeNone) {
return Query400TextResponse(fmt.Sprintf("unknown query type '%s'", request.Body.Type)), nil
}
resp, err := i.querier.Query(dns.Fqdn(request.Body.Query), qType)
if err != nil {
return nil, err
}
return Query200JSONResponse(ApiQueryResult{
Reason: resp.Reason,
ResponseType: resp.RType.String(),
Response: util.AnswerToString(resp.Res.Answer),
ReturnCode: dns.RcodeToString[resp.Res.Rcode],
}), nil
}

View File

@ -0,0 +1,216 @@
package api
import (
"context"
"errors"
"time"
// . "github.com/0xERR0R/blocky/helpertest"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
"github.com/miekg/dns"
"github.com/stretchr/testify/mock"
. "github.com/0xERR0R/blocky/helpertest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type BlockingControlMock struct {
mock.Mock
}
type ListRefreshMock struct {
mock.Mock
}
type QuerierMock struct {
mock.Mock
}
func (m *ListRefreshMock) RefreshLists() error {
args := m.Called()
return args.Error(0)
}
func (m *BlockingControlMock) EnableBlocking() {
_ = m.Called()
}
func (m *BlockingControlMock) DisableBlocking(t time.Duration, g []string) error {
args := m.Called(t, g)
return args.Error(0)
}
func (m *BlockingControlMock) BlockingStatus() BlockingStatus {
args := m.Called()
return args.Get(0).(BlockingStatus)
}
func (m *QuerierMock) Query(question string, qType dns.Type) (*model.Response, error) {
args := m.Called(question, qType)
return args.Get(0).(*model.Response), args.Error(1)
}
var _ = Describe("API implementation tests", func() {
var (
blockingControlMock *BlockingControlMock
querierMock *QuerierMock
listRefreshMock *ListRefreshMock
sut *OpenAPIInterfaceImpl
)
BeforeEach(func() {
blockingControlMock = &BlockingControlMock{}
querierMock = &QuerierMock{}
listRefreshMock = &ListRefreshMock{}
sut = NewOpenAPIInterfaceImpl(blockingControlMock, querierMock, listRefreshMock)
})
AfterEach(func() {
blockingControlMock.AssertExpectations(GinkgoT())
querierMock.AssertExpectations(GinkgoT())
listRefreshMock.AssertExpectations(GinkgoT())
})
Describe("Query API", func() {
When("Query is called", func() {
It("should return 200 on success", func() {
queryResponse, err := util.NewMsgWithAnswer(
"domain.", 123, A, "0.0.0.0",
)
Expect(err).Should(Succeed())
querierMock.On("Query", "google.com.", A).Return(&model.Response{
Res: queryResponse,
Reason: "reason",
}, nil)
resp, err := sut.Query(context.Background(), QueryRequestObject{
Body: &ApiQueryRequest{
Query: "google.com", Type: "A",
},
})
Expect(err).Should(Succeed())
var resp200 Query200JSONResponse
Expect(resp).Should(BeAssignableToTypeOf(resp200))
resp200 = resp.(Query200JSONResponse)
Expect(resp200.Reason).Should(Equal("reason"))
Expect(resp200.Response).Should(Equal("A (0.0.0.0)"))
Expect(resp200.ResponseType).Should(Equal("RESOLVED"))
Expect(resp200.ReturnCode).Should(Equal("NOERROR"))
})
It("should return 400 on wrong parameter", func() {
resp, err := sut.Query(context.Background(), QueryRequestObject{
Body: &ApiQueryRequest{
Query: "google.com",
Type: "WRONGTYPE",
},
})
Expect(err).Should(Succeed())
var resp400 Query400TextResponse
Expect(resp).Should(BeAssignableToTypeOf(resp400))
Expect(resp).Should(Equal(Query400TextResponse("unknown query type 'WRONGTYPE'")))
})
})
})
Describe("Lists API", func() {
When("List refresh is called", func() {
It("should return 200 on success", func() {
listRefreshMock.On("RefreshLists").Return(nil)
resp, err := sut.ListRefresh(context.Background(), ListRefreshRequestObject{})
Expect(err).Should(Succeed())
var resp200 ListRefresh200Response
Expect(resp).Should(BeAssignableToTypeOf(resp200))
})
It("should return 500 on failure", func() {
listRefreshMock.On("RefreshLists").Return(errors.New("failed"))
resp, err := sut.ListRefresh(context.Background(), ListRefreshRequestObject{})
Expect(err).Should(Succeed())
var resp500 ListRefresh500TextResponse
Expect(resp).Should(BeAssignableToTypeOf(resp500))
Expect(resp).Should(Equal(ListRefresh500TextResponse("failed")))
})
})
})
Describe("Control blocking status via API", func() {
When("Disable blocking is called", func() {
It("should return 200 on success", func() {
blockingControlMock.On("DisableBlocking", 3*time.Second, []string{"gr1", "gr2"}).Return(nil)
duration := "3s"
grroups := "gr1,gr2"
resp, err := sut.DisableBlocking(context.Background(), DisableBlockingRequestObject{
Params: DisableBlockingParams{
Duration: &duration,
Groups: &grroups,
},
})
Expect(err).Should(Succeed())
var resp200 DisableBlocking200Response
Expect(resp).Should(BeAssignableToTypeOf(resp200))
})
It("should return 400 on failure", func() {
blockingControlMock.On("DisableBlocking", mock.Anything, mock.Anything).Return(errors.New("failed"))
resp, err := sut.DisableBlocking(context.Background(), DisableBlockingRequestObject{})
Expect(err).Should(Succeed())
var resp400 DisableBlocking400TextResponse
Expect(resp).Should(BeAssignableToTypeOf(resp400))
Expect(resp).Should(Equal(DisableBlocking400TextResponse("failed")))
})
It("should return 400 on wrong duration parameter", func() {
wrongDuration := "4sds"
resp, err := sut.DisableBlocking(context.Background(), DisableBlockingRequestObject{
Params: DisableBlockingParams{
Duration: &wrongDuration,
},
})
Expect(err).Should(Succeed())
var resp400 DisableBlocking400TextResponse
Expect(resp).Should(BeAssignableToTypeOf(resp400))
Expect(resp).Should(Equal(DisableBlocking400TextResponse("time: unknown unit \"sds\" in duration \"4sds\"")))
})
})
When("Enable blocking is called", func() {
It("should return 200 on success", func() {
blockingControlMock.On("EnableBlocking").Return()
resp, err := sut.EnableBlocking(context.Background(), EnableBlockingRequestObject{})
Expect(err).Should(Succeed())
var resp200 EnableBlocking200Response
Expect(resp).Should(BeAssignableToTypeOf(resp200))
})
})
When("Blocking status is called", func() {
It("should return 200 and correct status", func() {
blockingControlMock.On("BlockingStatus").Return(BlockingStatus{
Enabled: false,
DisabledGroups: []string{"gr1", "gr2"},
AutoEnableInSec: 47,
})
resp, err := sut.BlockingStatus(context.Background(), BlockingStatusRequestObject{})
Expect(err).Should(Succeed())
var resp200 BlockingStatus200JSONResponse
Expect(resp).Should(BeAssignableToTypeOf(resp200))
resp200 = resp.(BlockingStatus200JSONResponse)
Expect(resp200.Enabled).Should(Equal(false))
Expect(resp200.DisabledGroups).Should(HaveValue(Equal([]string{"gr1", "gr2"})))
Expect(resp200.AutoEnableInSec).Should(HaveValue(BeNumerically("==", 47)))
})
})
})
})

591
api/api_server.gen.go Normal file
View File

@ -0,0 +1,591 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.14.0 DO NOT EDIT.
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/oapi-codegen/runtime"
strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp"
)
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Disable blocking
// (GET /blocking/disable)
DisableBlocking(w http.ResponseWriter, r *http.Request, params DisableBlockingParams)
// Enable blocking
// (GET /blocking/enable)
EnableBlocking(w http.ResponseWriter, r *http.Request)
// Blocking status
// (GET /blocking/status)
BlockingStatus(w http.ResponseWriter, r *http.Request)
// List refresh
// (POST /lists/refresh)
ListRefresh(w http.ResponseWriter, r *http.Request)
// Performs DNS query
// (POST /query)
Query(w http.ResponseWriter, r *http.Request)
}
// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint.
type Unimplemented struct{}
// Disable blocking
// (GET /blocking/disable)
func (_ Unimplemented) DisableBlocking(w http.ResponseWriter, r *http.Request, params DisableBlockingParams) {
w.WriteHeader(http.StatusNotImplemented)
}
// Enable blocking
// (GET /blocking/enable)
func (_ Unimplemented) EnableBlocking(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Blocking status
// (GET /blocking/status)
func (_ Unimplemented) BlockingStatus(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// List refresh
// (POST /lists/refresh)
func (_ Unimplemented) ListRefresh(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// Performs DNS query
// (POST /query)
func (_ Unimplemented) Query(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
type MiddlewareFunc func(http.Handler) http.Handler
// DisableBlocking operation middleware
func (siw *ServerInterfaceWrapper) DisableBlocking(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
// Parameter object where we will unmarshal all parameters from the context
var params DisableBlockingParams
// ------------- Optional query parameter "duration" -------------
err = runtime.BindQueryParameter("form", true, false, "duration", r.URL.Query(), &params.Duration)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "duration", Err: err})
return
}
// ------------- Optional query parameter "groups" -------------
err = runtime.BindQueryParameter("form", true, false, "groups", r.URL.Query(), &params.Groups)
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "groups", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.DisableBlocking(w, r, params)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// EnableBlocking operation middleware
func (siw *ServerInterfaceWrapper) EnableBlocking(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.EnableBlocking(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// BlockingStatus operation middleware
func (siw *ServerInterfaceWrapper) BlockingStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.BlockingStatus(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// ListRefresh operation middleware
func (siw *ServerInterfaceWrapper) ListRefresh(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ListRefresh(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
// Query operation middleware
func (siw *ServerInterfaceWrapper) Query(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.Query(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r.WithContext(ctx))
}
type UnescapedCookieParamError struct {
ParamName string
Err error
}
func (e *UnescapedCookieParamError) Error() string {
return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName)
}
func (e *UnescapedCookieParamError) Unwrap() error {
return e.Err
}
type UnmarshalingParamError struct {
ParamName string
Err error
}
func (e *UnmarshalingParamError) Error() string {
return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error())
}
func (e *UnmarshalingParamError) Unwrap() error {
return e.Err
}
type RequiredParamError struct {
ParamName string
}
func (e *RequiredParamError) Error() string {
return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName)
}
type RequiredHeaderError struct {
ParamName string
Err error
}
func (e *RequiredHeaderError) Error() string {
return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName)
}
func (e *RequiredHeaderError) Unwrap() error {
return e.Err
}
type InvalidParamFormatError struct {
ParamName string
Err error
}
func (e *InvalidParamFormatError) Error() string {
return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error())
}
func (e *InvalidParamFormatError) Unwrap() error {
return e.Err
}
type TooManyValuesForParamError struct {
ParamName string
Count int
}
func (e *TooManyValuesForParamError) Error() string {
return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count)
}
// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{})
}
type ChiServerOptions struct {
BaseURL string
BaseRouter chi.Router
Middlewares []MiddlewareFunc
ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseRouter: r,
})
}
func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler {
return HandlerWithOptions(si, ChiServerOptions{
BaseURL: baseURL,
BaseRouter: r,
})
}
// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
r := options.BaseRouter
if r == nil {
r = chi.NewRouter()
}
if options.ErrorHandlerFunc == nil {
options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
wrapper := ServerInterfaceWrapper{
Handler: si,
HandlerMiddlewares: options.Middlewares,
ErrorHandlerFunc: options.ErrorHandlerFunc,
}
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/blocking/disable", wrapper.DisableBlocking)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/blocking/enable", wrapper.EnableBlocking)
})
r.Group(func(r chi.Router) {
r.Get(options.BaseURL+"/blocking/status", wrapper.BlockingStatus)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/lists/refresh", wrapper.ListRefresh)
})
r.Group(func(r chi.Router) {
r.Post(options.BaseURL+"/query", wrapper.Query)
})
return r
}
type DisableBlockingRequestObject struct {
Params DisableBlockingParams
}
type DisableBlockingResponseObject interface {
VisitDisableBlockingResponse(w http.ResponseWriter) error
}
type DisableBlocking200Response struct {
}
func (response DisableBlocking200Response) VisitDisableBlockingResponse(w http.ResponseWriter) error {
w.WriteHeader(200)
return nil
}
type DisableBlocking400TextResponse string
func (response DisableBlocking400TextResponse) VisitDisableBlockingResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(400)
_, err := w.Write([]byte(response))
return err
}
type EnableBlockingRequestObject struct {
}
type EnableBlockingResponseObject interface {
VisitEnableBlockingResponse(w http.ResponseWriter) error
}
type EnableBlocking200Response struct {
}
func (response EnableBlocking200Response) VisitEnableBlockingResponse(w http.ResponseWriter) error {
w.WriteHeader(200)
return nil
}
type BlockingStatusRequestObject struct {
}
type BlockingStatusResponseObject interface {
VisitBlockingStatusResponse(w http.ResponseWriter) error
}
type BlockingStatus200JSONResponse ApiBlockingStatus
func (response BlockingStatus200JSONResponse) VisitBlockingStatusResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type ListRefreshRequestObject struct {
}
type ListRefreshResponseObject interface {
VisitListRefreshResponse(w http.ResponseWriter) error
}
type ListRefresh200Response struct {
}
func (response ListRefresh200Response) VisitListRefreshResponse(w http.ResponseWriter) error {
w.WriteHeader(200)
return nil
}
type ListRefresh500TextResponse string
func (response ListRefresh500TextResponse) VisitListRefreshResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(500)
_, err := w.Write([]byte(response))
return err
}
type QueryRequestObject struct {
Body *QueryJSONRequestBody
}
type QueryResponseObject interface {
VisitQueryResponse(w http.ResponseWriter) error
}
type Query200JSONResponse ApiQueryResult
func (response Query200JSONResponse) VisitQueryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
return json.NewEncoder(w).Encode(response)
}
type Query400TextResponse string
func (response Query400TextResponse) VisitQueryResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(400)
_, err := w.Write([]byte(response))
return err
}
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// Disable blocking
// (GET /blocking/disable)
DisableBlocking(ctx context.Context, request DisableBlockingRequestObject) (DisableBlockingResponseObject, error)
// Enable blocking
// (GET /blocking/enable)
EnableBlocking(ctx context.Context, request EnableBlockingRequestObject) (EnableBlockingResponseObject, error)
// Blocking status
// (GET /blocking/status)
BlockingStatus(ctx context.Context, request BlockingStatusRequestObject) (BlockingStatusResponseObject, error)
// List refresh
// (POST /lists/refresh)
ListRefresh(ctx context.Context, request ListRefreshRequestObject) (ListRefreshResponseObject, error)
// Performs DNS query
// (POST /query)
Query(ctx context.Context, request QueryRequestObject) (QueryResponseObject, error)
}
type StrictHandlerFunc = strictnethttp.StrictHttpHandlerFunc
type StrictMiddlewareFunc = strictnethttp.StrictHttpMiddlewareFunc
type StrictHTTPServerOptions struct {
RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{
RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusBadRequest)
},
ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
},
}}
}
func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares, options: options}
}
type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
options StrictHTTPServerOptions
}
// DisableBlocking operation middleware
func (sh *strictHandler) DisableBlocking(w http.ResponseWriter, r *http.Request, params DisableBlockingParams) {
var request DisableBlockingRequestObject
request.Params = params
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.DisableBlocking(ctx, request.(DisableBlockingRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "DisableBlocking")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(DisableBlockingResponseObject); ok {
if err := validResponse.VisitDisableBlockingResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// EnableBlocking operation middleware
func (sh *strictHandler) EnableBlocking(w http.ResponseWriter, r *http.Request) {
var request EnableBlockingRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.EnableBlocking(ctx, request.(EnableBlockingRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "EnableBlocking")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(EnableBlockingResponseObject); ok {
if err := validResponse.VisitEnableBlockingResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// BlockingStatus operation middleware
func (sh *strictHandler) BlockingStatus(w http.ResponseWriter, r *http.Request) {
var request BlockingStatusRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.BlockingStatus(ctx, request.(BlockingStatusRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "BlockingStatus")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(BlockingStatusResponseObject); ok {
if err := validResponse.VisitBlockingStatusResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// ListRefresh operation middleware
func (sh *strictHandler) ListRefresh(w http.ResponseWriter, r *http.Request) {
var request ListRefreshRequestObject
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.ListRefresh(ctx, request.(ListRefreshRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "ListRefresh")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(ListRefreshResponseObject); ok {
if err := validResponse.VisitListRefreshResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}
// Query operation middleware
func (sh *strictHandler) Query(w http.ResponseWriter, r *http.Request) {
var request QueryRequestObject
var body QueryJSONRequestBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
return
}
request.Body = &body
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
return sh.ssi.Query(ctx, request.(QueryRequestObject))
}
for _, middleware := range sh.middlewares {
handler = middleware(handler, "Query")
}
response, err := handler(r.Context(), w, r, request)
if err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
} else if validResponse, ok := response.(QueryResponseObject); ok {
if err := validResponse.VisitQueryResponse(w); err != nil {
sh.options.ResponseErrorHandlerFunc(w, r, err)
}
} else if response != nil {
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
}
}

52
api/api_types.gen.go Normal file
View File

@ -0,0 +1,52 @@
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.14.0 DO NOT EDIT.
package api
// ApiBlockingStatus defines model for api.BlockingStatus.
type ApiBlockingStatus struct {
// AutoEnableInSec If blocking is temporary disabled: amount of seconds until blocking will be enabled
AutoEnableInSec *int `json:"autoEnableInSec,omitempty"`
// DisabledGroups Disabled group names
DisabledGroups *[]string `json:"disabledGroups,omitempty"`
// Enabled True if blocking is enabled
Enabled bool `json:"enabled"`
}
// ApiQueryRequest defines model for api.QueryRequest.
type ApiQueryRequest struct {
// Query query for DNS request
Query string `json:"query"`
// Type request type (A, AAAA, ...)
Type string `json:"type"`
}
// ApiQueryResult defines model for api.QueryResult.
type ApiQueryResult struct {
// Reason blocky reason for resolution
Reason string `json:"reason"`
// Response actual DNS response
Response string `json:"response"`
// ResponseType response type (CACHED, BLOCKED, ...)
ResponseType string `json:"responseType"`
// ReturnCode DNS return code (NOERROR, NXDOMAIN, ...)
ReturnCode string `json:"returnCode"`
}
// DisableBlockingParams defines parameters for DisableBlocking.
type DisableBlockingParams struct {
// Duration duration of blocking (Example: 300s, 5m, 1h, 5m30s)
Duration *string `form:"duration,omitempty" json:"duration,omitempty"`
// Groups groups to disable (comma separated). If empty, disable all groups
Groups *string `form:"groups,omitempty" json:"groups,omitempty"`
}
// QueryJSONRequestBody defines body for Query for application/json ContentType.
type QueryJSONRequestBody = ApiQueryRequest

4
api/client.cfg.yaml Normal file
View File

@ -0,0 +1,4 @@
package: api
generate:
client: true
output: api_client.gen.go

6
api/server.cfg.yaml Normal file
View File

@ -0,0 +1,6 @@
package: api
generate:
chi-server: true
strict-server: true
embedded-spec: false
output: api_server.gen.go

4
api/types.cfg.yaml Normal file
View File

@ -0,0 +1,4 @@
package: api
generate:
models: true
output: api_types.gen.go

View File

@ -1,7 +1,7 @@
package cmd
import (
"encoding/json"
"context"
"fmt"
"net/http"
"strings"
@ -47,16 +47,20 @@ func newBlockingCommand() *cobra.Command {
}
func enableBlocking(_ *cobra.Command, _ []string) error {
resp, err := http.Get(apiURL(api.PathBlockingEnablePath))
client, err := api.NewClientWithResponses(apiURL())
if err != nil {
return fmt.Errorf("can't create client: %w", err)
}
resp, err := client.EnableBlockingWithResponse(context.Background())
if err != nil {
return fmt.Errorf("can't execute %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
if resp.StatusCode() == http.StatusOK {
log.Log().Info("OK")
} else {
return fmt.Errorf("response NOK, Status: %s", resp.Status)
return fmt.Errorf("response NOK, Status: %s", resp.Status())
}
return nil
@ -66,48 +70,62 @@ func disableBlocking(cmd *cobra.Command, _ []string) error {
duration, _ := cmd.Flags().GetDuration("duration")
groups, _ := cmd.Flags().GetStringArray("groups")
resp, err := http.Get(fmt.Sprintf("%s?duration=%s&groups=%s",
apiURL(api.PathBlockingDisablePath), duration, strings.Join(groups, ",")))
durationString := duration.String()
groupsString := strings.Join(groups, ",")
client, err := api.NewClientWithResponses(apiURL())
if err != nil {
return fmt.Errorf("can't create client: %w", err)
}
resp, err := client.DisableBlockingWithResponse(context.Background(), &api.DisableBlockingParams{
Duration: &durationString,
Groups: &groupsString,
})
if err != nil {
return fmt.Errorf("can't execute %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
if resp.StatusCode() == http.StatusOK {
log.Log().Info("OK")
} else {
return fmt.Errorf("response NOK, Status: %s", resp.Status)
return fmt.Errorf("response NOK, Status: %s", resp.Status())
}
return nil
}
func statusBlocking(_ *cobra.Command, _ []string) error {
resp, err := http.Get(apiURL(api.PathBlockingStatusPath))
client, err := api.NewClientWithResponses(apiURL())
if err != nil {
return fmt.Errorf("can't create client: %w", err)
}
resp, err := client.BlockingStatusWithResponse(context.Background())
if err != nil {
return fmt.Errorf("can't execute %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("response NOK, Status: %s", resp.Status)
if resp.StatusCode() != http.StatusOK {
return fmt.Errorf("response NOK, Status: %s", resp.Status())
}
var result api.BlockingStatus
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return fmt.Errorf("can't parse response %w", err)
}
if result.Enabled {
if resp.JSON200.Enabled {
log.Log().Info("blocking enabled")
} else {
if result.AutoEnableInSec == 0 {
log.Log().Infof("blocking disabled for groups: %s", strings.Join(result.DisabledGroups, "; "))
var groupNames string
if resp.JSON200.DisabledGroups != nil {
groupNames = strings.Join(*resp.JSON200.DisabledGroups, "; ")
}
if resp.JSON200.AutoEnableInSec == nil || *resp.JSON200.AutoEnableInSec == 0 {
log.Log().Infof("blocking disabled for groups: %s", groupNames)
} else {
log.Log().Infof("blocking disabled for groups: %s, for %d seconds",
strings.Join(result.DisabledGroups, "; "), result.AutoEnableInSec)
log.Log().Infof("blocking disabled for groups: '%s', for %d seconds",
groupNames, *resp.JSON200.AutoEnableInSec)
}
}

View File

@ -96,9 +96,11 @@ var _ = Describe("Blocking command", func() {
When("status blocking is called via REST and blocking is enabled", func() {
BeforeEach(func() {
mockFn = func(w http.ResponseWriter, _ *http.Request) {
response, err := json.Marshal(api.BlockingStatus{
w.Header().Add("Content-Type", "application/json")
i := 5
response, err := json.Marshal(api.ApiBlockingStatus{
Enabled: true,
AutoEnableInSec: uint(5),
AutoEnableInSec: &i,
})
Expect(err).Should(Succeed())
@ -112,13 +114,15 @@ var _ = Describe("Blocking command", func() {
})
})
When("status blocking is called via REST and blocking is disabled", func() {
var autoEnable uint
var autoEnable int
diabledGroups := []string{"abc"}
BeforeEach(func() {
mockFn = func(w http.ResponseWriter, _ *http.Request) {
response, err := json.Marshal(api.BlockingStatus{
w.Header().Add("Content-Type", "application/json")
response, err := json.Marshal(api.ApiBlockingStatus{
Enabled: false,
AutoEnableInSec: autoEnable,
DisabledGroups: []string{"abc"},
AutoEnableInSec: &autoEnable,
DisabledGroups: &diabledGroups,
})
Expect(err).Should(Succeed())
@ -129,7 +133,7 @@ var _ = Describe("Blocking command", func() {
It("should show the blocking status with time", func() {
autoEnable = 5
Expect(statusBlocking(newBlockingCommand(), []string{})).Should(Succeed())
Expect(loggerHook.LastEntry().Message).Should(Equal("blocking disabled for groups: abc, for 5 seconds"))
Expect(loggerHook.LastEntry().Message).Should(Equal("blocking disabled for groups: 'abc', for 5 seconds"))
})
It("should show the blocking status", func() {
autoEnable = 0

View File

@ -1,8 +1,8 @@
package cmd
import (
"context"
"fmt"
"io"
"net/http"
"github.com/0xERR0R/blocky/api"
@ -32,16 +32,18 @@ func newRefreshCommand() *cobra.Command {
}
func refreshList(_ *cobra.Command, _ []string) error {
resp, err := http.Post(apiURL(api.PathListsRefresh), "application/json", nil)
client, err := api.NewClientWithResponses(apiURL())
if err != nil {
return fmt.Errorf("can't create client: %w", err)
}
resp, err := client.ListRefreshWithResponse(context.Background())
if err != nil {
return fmt.Errorf("can't execute %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("response NOK, %s %s", resp.Status, string(body))
if resp.StatusCode() != http.StatusOK {
return fmt.Errorf("response NOK, %s %s", resp.Status(), string(resp.Body))
}
log.Log().Info("OK")

View File

@ -1,10 +1,8 @@
package cmd
import (
"bytes"
"encoding/json"
"context"
"fmt"
"io"
"net/http"
"github.com/0xERR0R/blocky/api"
@ -35,40 +33,30 @@ func query(cmd *cobra.Command, args []string) error {
return fmt.Errorf("unknown query type '%s'", typeFlag)
}
apiRequest := api.QueryRequest{
client, err := api.NewClientWithResponses(apiURL())
if err != nil {
return fmt.Errorf("can't create client: %w", err)
}
req := api.ApiQueryRequest{
Query: args[0],
Type: typeFlag,
}
jsonValue, err := json.Marshal(apiRequest)
resp, err := client.QueryWithResponse(context.Background(), req)
if err != nil {
return fmt.Errorf("can't marshal request: %w", err)
return fmt.Errorf("can't execute %w", err)
}
resp, err := http.Post(apiURL(api.PathQueryPath), "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
return fmt.Errorf("can't execute: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("response NOK, %s %s", resp.Status, string(body))
if resp.StatusCode() != http.StatusOK {
return fmt.Errorf("response NOK, %s %s", resp.Status(), string(resp.Body))
}
var result api.QueryResult
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return fmt.Errorf("can't read response: %w", err)
}
log.Log().Infof("Query result for '%s' (%s):", apiRequest.Query, apiRequest.Type)
log.Log().Infof("\treason: %20s", result.Reason)
log.Log().Infof("\tresponse type: %20s", result.ResponseType)
log.Log().Infof("\tresponse: %20s", result.Response)
log.Log().Infof("\treturn code: %20s", result.ReturnCode)
log.Log().Infof("Query result for '%s' (%s):", req.Query, req.Type)
log.Log().Infof("\treason: %20s", resp.JSON200.Reason)
log.Log().Infof("\tresponse type: %20s", resp.JSON200.ResponseType)
log.Log().Infof("\tresponse: %20s", resp.JSON200.Response)
log.Log().Infof("\treturn code: %20s", resp.JSON200.ReturnCode)
return nil
}

View File

@ -37,7 +37,7 @@ var _ = Describe("Blocking command", func() {
Describe("Call query command", func() {
BeforeEach(func() {
mockFn = func(w http.ResponseWriter, _ *http.Request) {
response, err := json.Marshal(api.QueryResult{
response, err := json.Marshal(api.ApiQueryResult{
Reason: "Reason",
ResponseType: "Type",
Response: "Response",
@ -52,7 +52,8 @@ var _ = Describe("Blocking command", func() {
When("query command is called via REST", func() {
BeforeEach(func() {
mockFn = func(w http.ResponseWriter, _ *http.Request) {
response, err := json.Marshal(api.QueryResult{
w.Header().Add("Content-Type", "application/json")
response, err := json.Marshal(api.ApiQueryResult{
Reason: "Reason",
ResponseType: "Type",
Response: "Response",

View File

@ -59,8 +59,8 @@ Complete documentation is available at https://github.com/0xERR0R/blocky`,
return c
}
func apiURL(path string) string {
return fmt.Sprintf("http://%s%s", net.JoinHostPort(apiHost, strconv.Itoa(int(apiPort))), path)
func apiURL() string {
return fmt.Sprintf("http://%s%s", net.JoinHostPort(apiHost, strconv.Itoa(int(apiPort))), "/api")
}
//nolint:gochecknoinits

View File

@ -14,4 +14,5 @@ coverage:
ignore:
- "**/mock_*"
- "**/*_enum.go"
- "**/*.gen.go"
- "e2e/*.go"

235
docs/api/openapi.yaml Normal file
View File

@ -0,0 +1,235 @@
openapi: 3.1.1
info:
title: blocky API
description: >-
# Blocky
Blocky is a DNS proxy and ad-blocker for the local network written in Go with following features:
## Features
- **Blocking** - Blocking of DNS queries with external lists (Ad-block, malware) and whitelisting
- Definition of black and white lists per client group (Kids, Smart home devices, etc.)
- Periodical reload of external black and white lists
- Regex support
- Blocking of request domain, response CNAME (deep CNAME inspection) and response IP addresses (against IP lists)
- **Advanced DNS configuration** - not just an ad-blocker
- Custom DNS resolution for certain domain names
- Conditional forwarding to external DNS server
- Upstream resolvers can be defined per client group
- **Performance** - Improves speed and performance in your network
- Customizable caching of DNS answers for queries -> improves DNS resolution speed and reduces amount of external DNS
queries
- Prefetching and caching of often used queries
- Using multiple external resolver simultaneously
- Low memory footprint
- **Various Protocols** - Supports modern DNS protocols
- DNS over UDP and TCP
- DNS over HTTPS (aka DoH)
- DNS over TLS (aka DoT)
- **Security and Privacy** - Secure communication
- Supports modern DNS extensions: DNSSEC, eDNS, ...
- Free configurable blocking lists - no hidden filtering etc.
- Provides DoH Endpoint
- Uses random upstream resolvers from the configuration - increases your privacy through the distribution of your DNS
traffic over multiple provider
- Blocky does **NOT** collect any user data, telemetry, statistics etc.
- **Integration** - various integration
- [Prometheus](https://prometheus.io/) metrics
- Prepared [Grafana](https://grafana.com/) dashboards (Prometheus and database)
- Logging of DNS queries per day / per client in CSV format or MySQL/MariaDB/PostgreSQL database - easy to analyze
- Various REST API endpoints
- CLI tool
- **Simple configuration** - single or multiple configuration files in YAML format
- Simple to maintain
- Simple to backup
- **Simple installation/configuration** - blocky was designed for simple installation
- Stateless (no database, no temporary files)
- Docker image with Multi-arch support
- Single binary
- Supports x86-64 and ARM architectures -> runs fine on Raspberry PI
- Community supported Helm chart for k8s deployment
## Quick start
You can jump to [Installation](https://0xerr0r.github.io/blocky/installation/) chapter in the documentation.
## Full documentation
You can find full documentation and configuration examples
at: [https://0xERR0R.github.io/blocky/](https://0xERR0R.github.io/blocky/)
contact:
name: blocky@github
url: https://github.com/0xERR0R/blocky
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: '1.0'
servers:
- url: /api
paths:
/blocking/disable:
get:
operationId: disableBlocking
tags:
- blocking
summary: Disable blocking
description: disable the blocking status
parameters:
- name: duration
in: query
description: 'duration of blocking (Example: 300s, 5m, 1h, 5m30s)'
schema:
type: string
- name: groups
in: query
description: groups to disable (comma separated). If empty, disable all groups
schema:
type: string
responses:
'200':
description: Blocking is disabled
'400':
description: Bad request (e.g. unknown group)
content:
text/plain:
schema:
type: string
example: Bad request
/blocking/enable:
get:
operationId: enableBlocking
tags:
- blocking
summary: Enable blocking
description: enable the blocking status
responses:
'200':
description: Blocking is enabled
/blocking/status:
get:
operationId: blockingStatus
tags:
- blocking
summary: Blocking status
description: get current blocking status
responses:
'200':
description: Returns current blocking status
content:
application/json:
schema:
$ref: '#/components/schemas/api.BlockingStatus'
/lists/refresh:
post:
operationId: listRefresh
tags:
- lists
summary: List refresh
description: Refresh all lists
responses:
'200':
description: Lists were reloaded
'500':
description: List refresh error
content:
text/plain:
schema:
type: string
example: Error text
/query:
post:
operationId: query
tags:
- query
summary: Performs DNS query
description: Performs DNS query
requestBody:
description: query data
content:
application/json:
schema:
$ref: '#/components/schemas/api.QueryRequest'
required: true
responses:
'200':
description: query was executed
content:
application/json:
schema:
$ref: '#/components/schemas/api.QueryResult'
'400':
description: Wrong request format
content:
text/plain:
schema:
type: string
example: Bad request
components:
schemas:
api.BlockingStatus:
type: object
properties:
autoEnableInSec:
type: integer
minimum: 0
description: >-
If blocking is temporary disabled: amount of seconds until blocking
will be enabled
disabledGroups:
type: array
description: Disabled group names
items:
type: string
enabled:
type: boolean
description: True if blocking is enabled
required:
- enabled
api.QueryRequest:
type: object
properties:
query:
type: string
description: query for DNS request
type:
type: string
description: request type (A, AAAA, ...)
required:
- query
- type
api.QueryResult:
type: object
properties:
reason:
type: string
description: blocky reason for resolution
response:
type: string
description: actual DNS response
responseType:
type: string
description: response type (CACHED, BLOCKED, ...)
returnCode:
type: string
description: DNS return code (NOERROR, NXDOMAIN, ...)
required:
- reason
- response
- responseType
- returnCode

6
docs/embed.go Normal file
View File

@ -0,0 +1,6 @@
package docs
import _ "embed"
//go:embed api/openapi.yaml
var OpenAPI string

View File

@ -2,8 +2,16 @@
## REST API
If http listener is enabled, blocky provides REST API. You can browse the API documentation (Swagger) documentation
under [https://0xERR0R.github.io/blocky/swagger.html](https://0xERR0R.github.io/blocky/swagger.html).
??? abstract "OpenAPI specification"
```yaml
--8<-- "docs/api/openapi.yaml"
```
If http listener is enabled, blocky provides REST API. You can download the [OpenAPI YAML](api/openapi.yaml) interface specification.
You can also browse the interactive API documentation (RapiDoc) documentation [online](rapidoc.html).
## CLI

23
docs/rapidoc.html Normal file
View File

@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="api/openapi.yaml"
theme = "light"
allow-authentication = "false"
show-header = "false"
bg-color = "#fdf8ed"
nav-bg-color = "#3f4d67"
nav-text-color = "#a9b7d0"
nav-hover-bg-color = "#333f54"
nav-hover-text-color = "#fff"
nav-accent-color = "#f87070"
primary-color = "#5c7096"
allow-try = "false"
> </rapi-doc>
</body>
</html>

File diff suppressed because it is too large Load Diff

18
go.mod
View File

@ -25,7 +25,6 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
github.com/swaggo/swag v1.16.2
github.com/x-cray/logrus-prefixed-formatter v0.5.2
golang.org/x/net v0.14.0
gopkg.in/yaml.v2 v2.4.0
@ -38,8 +37,10 @@ require (
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/ThinkChaos/parcour v0.0.0-20230710171753-fbf917c9eaef
github.com/deepmap/oapi-codegen v1.14.0
github.com/docker/go-connections v0.4.0
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198
github.com/oapi-codegen/runtime v1.0.0
github.com/testcontainers/testcontainers-go v0.23.0
mvdan.cc/gofumpt v0.5.0
)
@ -50,36 +51,39 @@ require (
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.3 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.5+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/getkin/kin-openapi v0.118.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/patternmatcher v0.5.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/perimeterx/marshmallow v1.1.4 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/tools/cmd/cover v0.1.0-deprecated // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
google.golang.org/grpc v1.57.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -88,8 +92,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@ -107,7 +109,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mattn/goveralls v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@ -132,6 +134,6 @@ require (
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.12.0
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

63
go.sum
View File

@ -7,8 +7,6 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
@ -19,6 +17,7 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.10.0-rc.8 h1:YSZVvlIIDD1UxQpJp0h+dnpLUw+TrY0cx8obKsp3bek=
github.com/Microsoft/hcsshim v0.10.0-rc.8/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/ThinkChaos/parcour v0.0.0-20230710171753-fbf917c9eaef h1:lg6zRor4+PZN1Pxqtieo/NMhd61ZdV1Z/+bFURWIVfU=
github.com/ThinkChaos/parcour v0.0.0-20230710171753-fbf917c9eaef/go.mod h1:hkcYs23P9zbezt09v8168B4lt69PGuoxRPQ6IJHKpHo=
github.com/abice/go-enum v0.5.7 h1:vOrobjpce5D/x5hYNqrWRkFUXFk7A6BlsJyVy4BS1jM=
@ -27,12 +26,15 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZp
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.30.5 h1:3r6kTHdKnuP4fkS8k2IrvSfxpxUTcW1SOL0wN7b7Dt0=
github.com/alicebob/miniredis/v2 v2.30.5/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
@ -53,7 +55,6 @@ github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=
@ -63,6 +64,8 @@ github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deepmap/oapi-codegen v1.14.0 h1:b51/kQwH69rjN5pu+8j/Q5fUGD/rUclLAcGLQWQwa3E=
github.com/deepmap/oapi-codegen v1.14.0/go.mod h1:QcEpzjVDwJEH3Fq6I7XYkI0M/JwvoL82ToYveaeVMAw=
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/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
@ -81,21 +84,17 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM=
github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
@ -104,6 +103,8 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@ -124,6 +125,7 @@ github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHa
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -140,6 +142,8 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@ -152,10 +156,11 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -170,15 +175,15 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/goveralls v0.0.12 h1:PEEeF0k1SsTjOBQ8FOmrOAoCu4ytuMaWCnWe94zxbCg=
@ -202,14 +207,17 @@ github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
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.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
@ -224,6 +232,8 @@ github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw=
github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -256,6 +266,7 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
@ -267,13 +278,17 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/testcontainers/testcontainers-go v0.23.0 h1:ERYTSikX01QczBLPZpqsETTBO7lInqEP349phDOVJVs=
github.com/testcontainers/testcontainers-go v0.23.0/go.mod h1:3gzuZfb7T9qfcH2pHpV4RLlWrPjeWNQah6XlYQ32c4I=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
@ -300,6 +315,8 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -355,6 +372,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -373,8 +391,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -398,11 +416,10 @@ google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@ -412,8 +429,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.0 h1:6hSAT5QcyIaty0jfnff0z0CLDjyRgZ8mlMHLqSt7uXM=
@ -429,5 +446,3 @@ gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@ -92,8 +92,8 @@ func (b *ListCache) Match(domain string, groupsToCheck []string) (groups []strin
}
// Refresh triggers the refresh of a list
func (b *ListCache) Refresh() {
_ = b.refresh(context.Background())
func (b *ListCache) Refresh() error {
return b.refresh(context.Background())
}
func (b *ListCache) refresh(ctx context.Context) error {

View File

@ -24,6 +24,6 @@ func BenchmarkRefresh(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
cache.Refresh()
_ = cache.Refresh()
}
}

View File

@ -181,7 +181,7 @@ var _ = Describe("ListCache", func() {
Expect(group).Should(ContainElement("gr1"))
})
sut.Refresh()
_ = sut.Refresh()
By("List couldn't be loaded due to timeout", func() {
group := sut.Match("blocked1.com", []string{"gr1"})

View File

@ -172,9 +172,13 @@ func setupRedisEnabledSubscriber(c *BlockingResolver) {
}
// RefreshLists triggers the refresh of all black and white lists in the cache
func (r *BlockingResolver) RefreshLists() {
r.blacklistMatcher.Refresh()
r.whitelistMatcher.Refresh()
func (r *BlockingResolver) RefreshLists() error {
var err *multierror.Error
err = multierror.Append(err, r.blacklistMatcher.Refresh())
err = multierror.Append(err, r.whitelistMatcher.Refresh())
return err.ErrorOrNil()
}
//nolint:prealloc
@ -283,7 +287,7 @@ func (r *BlockingResolver) BlockingStatus() api.BlockingStatus {
return api.BlockingStatus{
Enabled: r.status.enabled,
DisabledGroups: r.status.disabledGroups,
AutoEnableInSec: uint(autoEnableDuration.Seconds()),
AutoEnableInSec: int(autoEnableDuration.Seconds()),
}
}

View File

@ -1,6 +1,7 @@
package resolver
import (
"fmt"
"net"
"time"
@ -109,7 +110,7 @@ type NamedResolver interface {
}
// Chain creates a chain of resolvers
func Chain(resolvers ...Resolver) Resolver {
func Chain(resolvers ...Resolver) ChainedResolver {
for i, res := range resolvers {
if i+1 < len(resolvers) {
if cr, ok := res.(ChainedResolver); ok {
@ -118,7 +119,23 @@ func Chain(resolvers ...Resolver) Resolver {
}
}
return resolvers[0]
return resolvers[0].(ChainedResolver)
}
func GetFromChainWithType[T any](resolver ChainedResolver) (result T, err error) {
for resolver != nil {
if result, found := resolver.(T); found {
return result, nil
}
if cr, ok := resolver.GetNext().(ChainedResolver); ok {
resolver = cr
} else {
break
}
}
return result, fmt.Errorf("type was not found in the chain")
}
// Name returns a user-friendly name of a resolver

View File

@ -48,6 +48,22 @@ var _ = Describe("Resolver", func() {
})
})
Describe("GetFromChainWithType", func() {
It("should return resolver with type", func() {
ch := Chain(&CustomDNSResolver{}, &BlockingResolver{})
res, err := GetFromChainWithType[*BlockingResolver](ch)
var expectedResolver *BlockingResolver
Expect(err).Should(Succeed())
Expect(res).Should(BeAssignableToTypeOf(expectedResolver))
})
It("should fail if chain does not contain the desired type", func() {
ch := Chain(&CustomDNSResolver{}, &BlockingResolver{})
_, err := GetFromChainWithType[*FilteringResolver](ch)
Expect(err).Should(Not(Succeed()))
})
})
Describe("ForEach", func() {
It("should iterate on all resolvers in the chain", func() {
ch := Chain(r1, r2, r3, r4)

View File

@ -19,7 +19,6 @@ import (
"strings"
"time"
"github.com/0xERR0R/blocky/api"
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/metrics"
@ -45,7 +44,7 @@ type Server struct {
dnsServers []*dns.Server
httpListeners []net.Listener
httpsListeners []net.Listener
queryResolver resolver.Resolver
queryResolver resolver.ChainedResolver
cfg *config.Config
httpMux *chi.Mux
httpsMux *chi.Mux
@ -131,7 +130,7 @@ func NewServer(cfg *config.Config) (server *Server, err error) {
return nil, fmt.Errorf("server creation failed: %w", err)
}
httpRouter := createRouter(cfg)
httpRouter := createHTTPRouter(cfg)
httpsRouter := createHTTPSRouter(cfg)
httpListeners, httpsListeners, err := createHTTPListeners(cfg)
@ -175,11 +174,17 @@ func NewServer(cfg *config.Config) (server *Server, err error) {
server.printConfiguration()
server.registerDNSHandlers()
server.registerAPIEndpoints(httpRouter)
server.registerAPIEndpoints(httpsRouter)
err = server.registerAPIEndpoints(httpRouter)
registerResolverAPIEndpoints(httpRouter, queryResolver)
registerResolverAPIEndpoints(httpsRouter, queryResolver)
if err != nil {
return nil, err
}
err = server.registerAPIEndpoints(httpsRouter)
if err != nil {
return nil, err
}
return server, err
}
@ -241,18 +246,6 @@ func newListeners(proto string, addresses config.ListenConfig) ([]net.Listener,
return listeners, nil
}
func registerResolverAPIEndpoints(router chi.Router, res resolver.Resolver) {
for res != nil {
api.RegisterEndpoint(router, res)
if cr, ok := res.(resolver.ChainedResolver); ok {
res = cr.GetNext()
} else {
return
}
}
}
func createTLSServer(address string, cert tls.Certificate) (*dns.Server, error) {
return &dns.Server{
Addr: address,
@ -395,7 +388,7 @@ func createQueryResolver(
cfg *config.Config,
bootstrap *resolver.Bootstrap,
redisClient *redis.Client,
) (r resolver.Resolver, err error) {
) (r resolver.ChainedResolver, err error) {
upstreamBranches, uErr := createUpstreamBranches(cfg, bootstrap)
if uErr != nil {
return nil, fmt.Errorf("creation of upstream branches failed: %w", uErr)

View File

@ -2,7 +2,6 @@ package server
import (
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
@ -11,8 +10,11 @@ import (
"strings"
"time"
"github.com/0xERR0R/blocky/resolver"
"github.com/0xERR0R/blocky/api"
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/docs"
"github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
"github.com/0xERR0R/blocky/util"
@ -30,6 +32,7 @@ const (
dnsContentType = "application/dns-message"
jsonContentType = "application/json"
htmlContentType = "text/html; charset=UTF-8"
yamlContentType = "text/yaml"
corsMaxAge = 5 * time.Minute
)
@ -43,15 +46,38 @@ func secureHeader(next http.Handler) http.Handler {
})
}
func (s *Server) registerAPIEndpoints(router *chi.Mux) {
router.Post(api.PathQueryPath, s.apiQuery)
func (s *Server) createOpenAPIInterfaceImpl() (impl api.StrictServerInterface, err error) {
bControl, err := resolver.GetFromChainWithType[api.BlockingControl](s.queryResolver)
if err != nil {
return nil, fmt.Errorf("no blocking API implementation found %w", err)
}
router.Get(api.PathDohQuery, s.dohGetRequestHandler)
router.Get(api.PathDohQuery+"/", s.dohGetRequestHandler)
router.Get(api.PathDohQuery+"/{clientID}", s.dohGetRequestHandler)
router.Post(api.PathDohQuery, s.dohPostRequestHandler)
router.Post(api.PathDohQuery+"/", s.dohPostRequestHandler)
router.Post(api.PathDohQuery+"/{clientID}", s.dohPostRequestHandler)
refresher, err := resolver.GetFromChainWithType[api.ListRefresher](s.queryResolver)
if err != nil {
return nil, fmt.Errorf("no refresh API implementation found %w", err)
}
return api.NewOpenAPIInterfaceImpl(bControl, s, refresher), nil
}
func (s *Server) registerAPIEndpoints(router *chi.Mux) error {
const pathDohQuery = "/dns-query"
openAPIImpl, err := s.createOpenAPIInterfaceImpl()
if err != nil {
return err
}
api.RegisterOpenAPIEndpoints(router, openAPIImpl)
router.Get(pathDohQuery, s.dohGetRequestHandler)
router.Get(pathDohQuery+"/", s.dohGetRequestHandler)
router.Get(pathDohQuery+"/{clientID}", s.dohGetRequestHandler)
router.Post(pathDohQuery, s.dohPostRequestHandler)
router.Post(pathDohQuery+"/", s.dohPostRequestHandler)
router.Post(pathDohQuery+"/{clientID}", s.dohPostRequestHandler)
return nil
}
func (s *Server) dohGetRequestHandler(rw http.ResponseWriter, req *http.Request) {
@ -162,69 +188,11 @@ func extractIP(r *http.Request) string {
return hostPort
}
// apiQuery is the http endpoint to perform a DNS query
// @Summary Performs DNS query
// @Description Performs DNS query
// @Tags query
// @Accept json
// @Produce json
// @Param query body api.QueryRequest true "query data"
// @Success 200 {object} api.QueryResult "query was executed"
// @Failure 400 "Wrong request format"
// @Router /query [post]
func (s *Server) apiQuery(rw http.ResponseWriter, req *http.Request) {
var queryRequest api.QueryRequest
func (s *Server) Query(question string, qType dns.Type) (*model.Response, error) {
dnsRequest := util.NewMsgWithQuestion(question, qType)
r := createResolverRequest(nil, dnsRequest)
rw.Header().Set(contentTypeHeader, jsonContentType)
err := json.NewDecoder(req.Body).Decode(&queryRequest)
if err != nil {
logAndResponseWithError(err, "can't read request: ", rw)
return
}
// validate query type
qType := dns.Type(dns.StringToType[queryRequest.Type])
if qType == dns.Type(dns.TypeNone) {
err = fmt.Errorf("unknown query type '%s'", queryRequest.Type)
logAndResponseWithError(err, "unknown query type: ", rw)
return
}
query := queryRequest.Query
// append dot
if !strings.HasSuffix(query, ".") {
query += "."
}
dnsRequest := util.NewMsgWithQuestion(query, qType)
r := newRequest(net.ParseIP(extractIP(req)), model.RequestProtocolTCP, "", dnsRequest)
response, err := s.queryResolver.Resolve(r)
if err != nil {
logAndResponseWithError(err, "unable to process query: ", rw)
return
}
jsonResponse, err := json.Marshal(api.QueryResult{
Reason: response.Reason,
ResponseType: response.RType.String(),
Response: util.AnswerToString(response.Res.Answer),
ReturnCode: dns.RcodeToString[response.Res.Rcode],
})
if err != nil {
logAndResponseWithError(err, "unable to marshal response: ", rw)
return
}
_, err = rw.Write(jsonResponse)
logAndResponseWithError(err, "unable to write response: ", rw)
return s.queryResolver.Resolve(r)
}
func createHTTPSRouter(cfg *config.Config) *chi.Mux {
@ -232,25 +200,45 @@ func createHTTPSRouter(cfg *config.Config) *chi.Mux {
configureSecureHeaderHandler(router)
configureCorsHandler(router)
configureDebugHandler(router)
configureRootHandler(cfg, router)
registerHandlers(cfg, router)
return router
}
func createRouter(cfg *config.Config) *chi.Mux {
func createHTTPRouter(cfg *config.Config) *chi.Mux {
router := chi.NewRouter()
registerHandlers(cfg, router)
return router
}
func registerHandlers(cfg *config.Config, router *chi.Mux) {
configureCorsHandler(router)
configureDebugHandler(router)
configureRootHandler(cfg, router)
configureDocsHandler(router)
return router
configureStaticAssetsHandler(router)
configureRootHandler(cfg, router)
}
func configureDocsHandler(router *chi.Mux) {
router.Get("/docs/openapi.yaml", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set(contentTypeHeader, yamlContentType)
_, err := writer.Write([]byte(docs.OpenAPI))
logAndResponseWithError(err, "can't write OpenAPI definition file: ", writer)
})
}
func configureStaticAssetsHandler(router *chi.Mux) {
assets, err := web.Assets()
util.FatalOnError("unable to load static asset files", err)
fs := http.FileServer(http.FS(assets))
router.Handle("/static/*", http.StripPrefix("/static/", fs))
}
func configureRootHandler(cfg *config.Config, router *chi.Mux) {
@ -264,11 +252,6 @@ func configureRootHandler(cfg *config.Config, router *chi.Mux) {
Title string
}
swaggerVersion := "main"
if util.Version != "undefined" {
swaggerVersion = util.Version
}
type PageData struct {
Links []HandlerLink
Version string
@ -281,11 +264,12 @@ func configureRootHandler(cfg *config.Config, router *chi.Mux) {
}
pd.Links = []HandlerLink{
{
URL: fmt.Sprintf(
"https://htmlpreview.github.io/?https://github.com/0xERR0R/blocky/blob/%s/docs/swagger.html",
swaggerVersion,
),
Title: "Swagger Rest API Documentation (Online @GitHub)",
URL: "/docs/openapi.yaml",
Title: "Rest API Documentation (OpenAPI)",
},
{
URL: "/static/rapidoc.html",
Title: "Interactive Rest API Documentation (RapiDoc)",
},
{
URL: "/debug/",

View File

@ -3,7 +3,6 @@ package server
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"net"
"net/http"
@ -11,8 +10,8 @@ import (
"sync/atomic"
"time"
"github.com/0xERR0R/blocky/api"
"github.com/0xERR0R/blocky/config"
"github.com/0xERR0R/blocky/docs"
. "github.com/0xERR0R/blocky/helpertest"
. "github.com/0xERR0R/blocky/log"
"github.com/0xERR0R/blocky/model"
@ -182,19 +181,10 @@ var _ = Describe("Running DNS server", func() {
BeforeEach(func() {
mockClientName.Store("")
// reset client cache
res := sut.queryResolver
for res != nil {
if t, ok := res.(*resolver.ClientNamesResolver); ok {
t.FlushCache()
clientNamesResolver, err := resolver.GetFromChainWithType[*resolver.ClientNamesResolver](sut.queryResolver)
Expect(err).Should(Succeed())
break
}
if c, ok := res.(resolver.ChainedResolver); ok {
res = c.GetNext()
} else {
break
}
}
clientNamesResolver.FlushCache()
})
Context("DNS query is resolvable via external DNS", func() {
@ -383,76 +373,17 @@ var _ = Describe("Running DNS server", func() {
})
})
})
Describe("Query Rest API", func() {
When("Query API is called", func() {
It("Should process the query", func() {
req := api.QueryRequest{
Query: "google.de",
Type: "A",
}
jsonValue, err := json.Marshal(req)
Describe("Docs endpoints", func() {
When("OpenApi URL is called", func() {
It("should return openAPI definition file", func() {
resp, err := http.Get("http://localhost:4000/docs/openapi.yaml")
Expect(err).Should(Succeed())
resp, err := http.Post("http://localhost:4000/api/query", "application/json", bytes.NewBuffer(jsonValue))
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp).Should(
SatisfyAll(
HaveHTTPStatus(http.StatusOK),
HaveHTTPHeaderWithValue("Content-type", "application/json"),
HaveHTTPHeaderWithValue("Content-type", "text/yaml"),
HaveHTTPBody(docs.OpenAPI),
))
var result api.QueryResult
err = json.NewDecoder(resp.Body).Decode(&result)
Expect(err).Should(Succeed())
Expect(result.Response).Should(Equal("A (123.124.122.122)"))
})
})
When("Wrong request type is used", func() {
It("Should return internal error", func() {
req := api.QueryRequest{
Query: "google.de",
Type: "WrongType",
}
jsonValue, err := json.Marshal(req)
Expect(err).Should(Succeed())
resp, err := http.Post("http://localhost:4000/api/query", "application/json", bytes.NewBuffer(jsonValue))
Expect(err).Should(Succeed())
DeferCleanup(resp.Body.Close)
Expect(resp.StatusCode).Should(Equal(http.StatusInternalServerError))
})
})
When("Internal error occurs", func() {
It("Should return internal error", func() {
req := api.QueryRequest{
Query: "error.",
Type: "A",
}
jsonValue, err := json.Marshal(req)
Expect(err).Should(Succeed())
resp, err := http.Post("http://localhost:4000/api/query", "application/json", bytes.NewBuffer(jsonValue))
Expect(err).Should(Succeed())
DeferCleanup(resp.Body.Close)
Expect(resp.StatusCode).Should(Equal(http.StatusInternalServerError))
})
})
When("Request is malformed", func() {
It("Should return internal error", func() {
jsonValue := []byte("")
resp, err := http.Post("http://localhost:4000/api/query", "application/json", bytes.NewBuffer(jsonValue))
Expect(err).Should(Succeed())
DeferCleanup(resp.Body.Close)
Expect(resp.StatusCode).Should(Equal(http.StatusInternalServerError))
})
})
})

View File

@ -7,10 +7,10 @@ package tools
import (
_ "github.com/abice/go-enum"
_ "github.com/deepmap/oapi-codegen/cmd/oapi-codegen"
_ "github.com/dosgo/zigtool/zigcc"
_ "github.com/dosgo/zigtool/zigcpp"
_ "github.com/onsi/ginkgo/v2/ginkgo"
_ "github.com/swaggo/swag/cmd/swag"
_ "golang.org/x/tools/cmd/goimports"
_ "mvdan.cc/gofumpt"
)

View File

@ -1,8 +1,18 @@
package web
import _ "embed"
import (
"embed"
"io/fs"
)
// IndexTmpl html template for the start page
//
//go:embed index.html
var IndexTmpl string
//go:embed all:static
var static embed.FS
func Assets() (fs.FS, error) {
return fs.Sub(static, "static")
}

3895
web/static/rapidoc-min.js vendored Normal file

File diff suppressed because one or more lines are too long

22
web/static/rapidoc.html Normal file
View File

@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="/static/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/docs/openapi.yaml"
theme = "light"
allow-authentication = "false"
show-header = "false"
bg-color = "#fdf8ed"
nav-bg-color = "#3f4d67"
nav-text-color = "#a9b7d0"
nav-hover-bg-color = "#333f54"
nav-hover-text-color = "#fff"
nav-accent-color = "#f87070"
primary-color = "#5c7096"
> </rapi-doc>
</body>
</html>