diff --git a/.dockerignore b/.dockerignore index 3bb60f99..f9653b66 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ bin dist site -docs node_modules .git .idea diff --git a/.gitignore b/.gitignore index e012930d..0d27db6d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ /*.pem bin/ dist/ -docs/swagger.json -docs/swagger.yaml docs/docs.go site/ config.yml diff --git a/Makefile b/Makefile index 90fc9106..f053f00c 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/api/api.go b/api/api.go deleted file mode 100644 index afc1fcff..00000000 --- a/api/api.go +++ /dev/null @@ -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"` -} diff --git a/api/api_client.gen.go b/api/api_client.gen.go new file mode 100644 index 00000000..e8124e7d --- /dev/null +++ b/api/api_client.gen.go @@ -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 +} diff --git a/api/api_endpoints.go b/api/api_endpoints.go deleted file mode 100644 index 9ad78c30..00000000 --- a/api/api_endpoints.go +++ /dev/null @@ -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) -} diff --git a/api/api_endpoints_test.go b/api/api_endpoints_test.go deleted file mode 100644 index a6445b95..00000000 --- a/api/api_endpoints_test.go +++ /dev/null @@ -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()) - }) - }) - }) - }) -}) diff --git a/api/api_interface_impl.go b/api/api_interface_impl.go new file mode 100644 index 00000000..c16ab87c --- /dev/null +++ b/api/api_interface_impl.go @@ -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 +} diff --git a/api/api_interface_impl_test.go b/api/api_interface_impl_test.go new file mode 100644 index 00000000..02afdb6a --- /dev/null +++ b/api/api_interface_impl_test.go @@ -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))) + }) + }) + }) +}) diff --git a/api/api_server.gen.go b/api/api_server.gen.go new file mode 100644 index 00000000..f79db3d9 --- /dev/null +++ b/api/api_server.gen.go @@ -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(), ¶ms.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(), ¶ms.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)) + } +} diff --git a/api/api_types.gen.go b/api/api_types.gen.go new file mode 100644 index 00000000..5a32aeb4 --- /dev/null +++ b/api/api_types.gen.go @@ -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 diff --git a/api/client.cfg.yaml b/api/client.cfg.yaml new file mode 100644 index 00000000..afb91a48 --- /dev/null +++ b/api/client.cfg.yaml @@ -0,0 +1,4 @@ +package: api +generate: + client: true +output: api_client.gen.go \ No newline at end of file diff --git a/api/server.cfg.yaml b/api/server.cfg.yaml new file mode 100644 index 00000000..2146abb4 --- /dev/null +++ b/api/server.cfg.yaml @@ -0,0 +1,6 @@ +package: api +generate: + chi-server: true + strict-server: true + embedded-spec: false +output: api_server.gen.go \ No newline at end of file diff --git a/api/types.cfg.yaml b/api/types.cfg.yaml new file mode 100644 index 00000000..6cf50ee3 --- /dev/null +++ b/api/types.cfg.yaml @@ -0,0 +1,4 @@ +package: api +generate: + models: true +output: api_types.gen.go \ No newline at end of file diff --git a/cmd/blocking.go b/cmd/blocking.go index b6122ef7..00fbf921 100644 --- a/cmd/blocking.go +++ b/cmd/blocking.go @@ -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) } } diff --git a/cmd/blocking_test.go b/cmd/blocking_test.go index 4ffa44ee..18eebc7a 100644 --- a/cmd/blocking_test.go +++ b/cmd/blocking_test.go @@ -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 diff --git a/cmd/lists.go b/cmd/lists.go index 004f2083..eca6de75 100644 --- a/cmd/lists.go +++ b/cmd/lists.go @@ -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") diff --git a/cmd/query.go b/cmd/query.go index 1cdec875..609372cc 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -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 } diff --git a/cmd/query_test.go b/cmd/query_test.go index dee1edf4..50aa19da 100644 --- a/cmd/query_test.go +++ b/cmd/query_test.go @@ -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", diff --git a/cmd/root.go b/cmd/root.go index 1b52a6c5..4124a80a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 diff --git a/codecov.yml b/codecov.yml index 04142e0d..76a09d9b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -14,4 +14,5 @@ coverage: ignore: - "**/mock_*" - "**/*_enum.go" + - "**/*.gen.go" - "e2e/*.go" diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml new file mode 100644 index 00000000..2d6299a3 --- /dev/null +++ b/docs/api/openapi.yaml @@ -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 diff --git a/docs/embed.go b/docs/embed.go new file mode 100644 index 00000000..9d27601c --- /dev/null +++ b/docs/embed.go @@ -0,0 +1,6 @@ +package docs + +import _ "embed" + +//go:embed api/openapi.yaml +var OpenAPI string diff --git a/docs/interfaces.md b/docs/interfaces.md index 9916819f..813af09d 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -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 diff --git a/docs/rapidoc.html b/docs/rapidoc.html new file mode 100644 index 00000000..c9739167 --- /dev/null +++ b/docs/rapidoc.html @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/swagger.html b/docs/swagger.html deleted file mode 100644 index 268bcc8c..00000000 --- a/docs/swagger.html +++ /dev/null @@ -1,8639 +0,0 @@ - - - - - - blocky API - - -
-

blocky API

-

- Base URL: /api/, - Version: -

-

-

blocky API

-

- - -
- Schemes: - -
- - -

Summary

-

Tag: blocking

- -

-

- - - - - - - - - - - - - - - - - - - - - -
OperationDescription
GET /blocking/disable

Disable blocking

-
GET /blocking/enable

Enable blocking

-
GET /blocking/status

Blocking status

-
-

Tag: lists

- -

-

- - - - - - - - - - - - - -
OperationDescription
POST /lists/refresh

List refresh

-
-

Tag: query

- -

-

- - - - - - - - - - - - - -
OperationDescription
POST /query

Performs DNS query

-
- - -

Paths

- - - -
-
-
Disable blocking
-

GET /blocking/disable

- Tags: - blocking -
-
-
-

disable the blocking status

- -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- duration -

duration of blocking (Example: 300s, 5m, 1h, 5m30s)

-
query - string (duration) - - - -
- groups -

groups to disable (comma separated). If empty, disable all groups

-
query - string (string) - - - -
-
- -
- -
-
- 200 OK - -
-
-
-
-

Blocking is disabled

- -
-
-
- -
-
- -
-
-
- 400 Bad Request - -
-
-
-
-

Unknown group

- -
-
-
- -
-
- -
-
-
-
-
-
- - -
-
-
Enable blocking
-

GET /blocking/enable

- Tags: - blocking -
-
-
-

enable the blocking status

- -
- - -
- -
-
- 200 OK - -
-
-
-
-

Blocking is enabled

- -
-
-
- -
-
- -
-
-
-
-
-
- - -
-
-
Blocking status
-

GET /blocking/status

- Tags: - blocking -
-
-
-

get current blocking status

- -
- - -
-

application/json -

- -
-
- 200 OK - -
-
-
-
-

Returns current blocking status

- -
-
-
- -
- -
- -
-
-
-
-
-
- - -
-
-
List refresh
-

POST /lists/refresh

- Tags: - lists -
-
-
-

Refresh all lists

- -
- - -
- -
-
- 200 OK - -
-
-
-
-

Lists were reloaded

- -
-
-
- -
-
- -
-
-
-
-
-
- - -
-
-
Performs DNS query
-

POST /query

- Tags: - query -
-
-
-

Performs DNS query

- -
- - -
- -

application/json -

-
-
-

-

query data

-

-
-
- -
-
-
- -
-

application/json -

- -
-
- 200 OK - -
-
-
-
-

query was executed

- -
-
-
- -
-
- -
-
- -
-
-
- 400 Bad Request - -
-
-
-
-

Wrong request format

- -
-
-
- -
-
- -
-
-
-
-
-
- - -

Schema definitions

- - -
-
-

api.BlockingStatus: - - object - - - -

-
-
- -
-
-
- autoEnableInSec: - - integer - - -
-
-

If blocking is temporary disabled: amount of seconds until blocking will be enabled

- -
- -
-
-
- disabledGroups: - - string[] - - -
-
-

Disabled group names

- -
- -
- - string - - -
- -
-
-
-
-
- enabled: - - boolean - - -
-
-

True if blocking is enabled

- -
- -
-
-
-
-
-
-
-
-

api.QueryRequest: - - object - - - -

-
-
- -
-
-
- query: - - string - - -
-
-

query for DNS request

- -
- -
-
-
- type: - - string - - -
-
-

request type (A, AAAA, ...)

- -
- -
-
-
-
-
-
-
-
-

api.QueryResult: - - object - - - -

-
-
- -
-
-
- reason: - - string - - -
-
-

blocky reason for resolution

- -
- -
-
-
- response: - - string - - -
-
-

actual DNS response

- -
- -
-
-
- responseType: - - string - - -
-
-

response type (CACHED, BLOCKED, ...)

- -
- -
-
-
- returnCode: - - string - - -
-
-

DNS return code (NOERROR, NXDOMAIN, ...)

- -
- -
-
-
-
-
-
-
- - - diff --git a/go.mod b/go.mod index 1204f72b..410308d0 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 8f569e2d..2feb7f85 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/lists/list_cache.go b/lists/list_cache.go index ba17c4ca..8d619ba5 100644 --- a/lists/list_cache.go +++ b/lists/list_cache.go @@ -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 { diff --git a/lists/list_cache_benchmark_test.go b/lists/list_cache_benchmark_test.go index fedf2fd8..8a82c9a1 100644 --- a/lists/list_cache_benchmark_test.go +++ b/lists/list_cache_benchmark_test.go @@ -24,6 +24,6 @@ func BenchmarkRefresh(b *testing.B) { b.ReportAllocs() for n := 0; n < b.N; n++ { - cache.Refresh() + _ = cache.Refresh() } } diff --git a/lists/list_cache_test.go b/lists/list_cache_test.go index 2d08cfb1..2832ab9a 100644 --- a/lists/list_cache_test.go +++ b/lists/list_cache_test.go @@ -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"}) diff --git a/resolver/blocking_resolver.go b/resolver/blocking_resolver.go index a501e0c1..468cbca3 100644 --- a/resolver/blocking_resolver.go +++ b/resolver/blocking_resolver.go @@ -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()), } } diff --git a/resolver/resolver.go b/resolver/resolver.go index 9b6e1c7e..59382834 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -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 diff --git a/resolver/resolver_test.go b/resolver/resolver_test.go index 003e18da..df188c4f 100644 --- a/resolver/resolver_test.go +++ b/resolver/resolver_test.go @@ -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) diff --git a/server/server.go b/server/server.go index 1b553995..9561a160 100644 --- a/server/server.go +++ b/server/server.go @@ -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) diff --git a/server/server_endpoints.go b/server/server_endpoints.go index 7346f9aa..7b9618b2 100644 --- a/server/server_endpoints.go +++ b/server/server_endpoints.go @@ -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/", diff --git a/server/server_test.go b/server/server_test.go index 65b30bc2..fa5275fd 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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)) }) }) }) diff --git a/tools.go b/tools.go index 87b0a1d8..8165640a 100644 --- a/tools.go +++ b/tools.go @@ -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" ) diff --git a/web/index.go b/web/index.go index afdf38c2..c2355e09 100644 --- a/web/index.go +++ b/web/index.go @@ -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") +} diff --git a/web/static/rapidoc-min.js b/web/static/rapidoc-min.js new file mode 100644 index 00000000..0104e904 --- /dev/null +++ b/web/static/rapidoc-min.js @@ -0,0 +1,3895 @@ +/*! RapiDoc 9.3.4 | Author - Mrinmoy Majumdar | License information can be found in rapidoc-min.js.LICENSE.txt */ +(()=>{var e,t,r={656:(e,t,r)=>{"use strict";const n=window,o=n.ShadowRoot&&(void 0===n.ShadyCSS||n.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,a=Symbol(),i=new WeakMap;class s{constructor(e,t,r){if(this._$cssResult$=!0,r!==a)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(o&&void 0===e){const r=void 0!==t&&1===t.length;r&&(e=i.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&i.set(t,e))}return e}toString(){return this.cssText}}const l=e=>new s("string"==typeof e?e:e+"",void 0,a),c=(e,...t)=>{const r=1===e.length?e[0]:t.reduce(((t,r,n)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+e[n+1]),e[0]);return new s(r,e,a)},p=o?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const r of e.cssRules)t+=r.cssText;return l(t)})(e):e;var d;const u=window,h=u.trustedTypes,f=h?h.emptyScript:"",m=u.reactiveElementPolyfillSupport,y={toAttribute(e,t){switch(t){case Boolean:e=e?f:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let r=e;switch(t){case Boolean:r=null!==e;break;case Number:r=null===e?null:Number(e);break;case Object:case Array:try{r=JSON.parse(e)}catch(e){r=null}}return r}},g=(e,t)=>t!==e&&(t==t||e==e),v={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:g};class b extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u()}static addInitializer(e){var t;this.finalize(),(null!==(t=this.h)&&void 0!==t?t:this.h=[]).push(e)}static get observedAttributes(){this.finalize();const e=[];return this.elementProperties.forEach(((t,r)=>{const n=this._$Ep(r,t);void 0!==n&&(this._$Ev.set(n,r),e.push(n))})),e}static createProperty(e,t=v){if(t.state&&(t.attribute=!1),this.finalize(),this.elementProperties.set(e,t),!t.noAccessor&&!this.prototype.hasOwnProperty(e)){const r="symbol"==typeof e?Symbol():"__"+e,n=this.getPropertyDescriptor(e,r,t);void 0!==n&&Object.defineProperty(this.prototype,e,n)}}static getPropertyDescriptor(e,t,r){return{get(){return this[t]},set(n){const o=this[e];this[t]=n,this.requestUpdate(e,o,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)||v}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const e=Object.getPrototypeOf(this);if(e.finalize(),void 0!==e.h&&(this.h=[...e.h]),this.elementProperties=new Map(e.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const e=this.properties,t=[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)];for(const r of t)this.createProperty(r,e[r])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const r=new Set(e.flat(1/0).reverse());for(const e of r)t.unshift(p(e))}else void 0!==e&&t.push(p(e));return t}static _$Ep(e,t){const r=t.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof e?e.toLowerCase():void 0}u(){var e;this._$E_=new Promise((e=>this.enableUpdating=e)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(e=this.constructor.h)||void 0===e||e.forEach((e=>e(this)))}addController(e){var t,r;(null!==(t=this._$ES)&&void 0!==t?t:this._$ES=[]).push(e),void 0!==this.renderRoot&&this.isConnected&&(null===(r=e.hostConnected)||void 0===r||r.call(e))}removeController(e){var t;null===(t=this._$ES)||void 0===t||t.splice(this._$ES.indexOf(e)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach(((e,t)=>{this.hasOwnProperty(t)&&(this._$Ei.set(t,this[t]),delete this[t])}))}createRenderRoot(){var e;const t=null!==(e=this.shadowRoot)&&void 0!==e?e:this.attachShadow(this.constructor.shadowRootOptions);return((e,t)=>{o?e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):t.forEach((t=>{const r=document.createElement("style"),o=n.litNonce;void 0!==o&&r.setAttribute("nonce",o),r.textContent=t.cssText,e.appendChild(r)}))})(t,this.constructor.elementStyles),t}connectedCallback(){var e;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostConnected)||void 0===t?void 0:t.call(e)}))}enableUpdating(e){}disconnectedCallback(){var e;null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostDisconnected)||void 0===t?void 0:t.call(e)}))}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$EO(e,t,r=v){var n;const o=this.constructor._$Ep(e,r);if(void 0!==o&&!0===r.reflect){const a=(void 0!==(null===(n=r.converter)||void 0===n?void 0:n.toAttribute)?r.converter:y).toAttribute(t,r.type);this._$El=e,null==a?this.removeAttribute(o):this.setAttribute(o,a),this._$El=null}}_$AK(e,t){var r;const n=this.constructor,o=n._$Ev.get(e);if(void 0!==o&&this._$El!==o){const e=n.getPropertyOptions(o),a="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==(null===(r=e.converter)||void 0===r?void 0:r.fromAttribute)?e.converter:y;this._$El=o,this[o]=a.fromAttribute(t,e.type),this._$El=null}}requestUpdate(e,t,r){let n=!0;void 0!==e&&(((r=r||this.constructor.getPropertyOptions(e)).hasChanged||g)(this[e],t)?(this._$AL.has(e)||this._$AL.set(e,t),!0===r.reflect&&this._$El!==e&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(e,r))):n=!1),!this.isUpdatePending&&n&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var e;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((e,t)=>this[t]=e)),this._$Ei=void 0);let t=!1;const r=this._$AL;try{t=this.shouldUpdate(r),t?(this.willUpdate(r),null===(e=this._$ES)||void 0===e||e.forEach((e=>{var t;return null===(t=e.hostUpdate)||void 0===t?void 0:t.call(e)})),this.update(r)):this._$Ek()}catch(e){throw t=!1,this._$Ek(),e}t&&this._$AE(r)}willUpdate(e){}_$AE(e){var t;null===(t=this._$ES)||void 0===t||t.forEach((e=>{var t;return null===(t=e.hostUpdated)||void 0===t?void 0:t.call(e)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(e){return!0}update(e){void 0!==this._$EC&&(this._$EC.forEach(((e,t)=>this._$EO(t,this[t],e))),this._$EC=void 0),this._$Ek()}updated(e){}firstUpdated(e){}}var x;b.finalized=!0,b.elementProperties=new Map,b.elementStyles=[],b.shadowRootOptions={mode:"open"},null==m||m({ReactiveElement:b}),(null!==(d=u.reactiveElementVersions)&&void 0!==d?d:u.reactiveElementVersions=[]).push("1.6.1");const w=window,$=w.trustedTypes,k=$?$.createPolicy("lit-html",{createHTML:e=>e}):void 0,S=`lit$${(Math.random()+"").slice(9)}$`,A="?"+S,E=`<${A}>`,O=document,T=(e="")=>O.createComment(e),C=e=>null===e||"object"!=typeof e&&"function"!=typeof e,j=Array.isArray,I=e=>j(e)||"function"==typeof(null==e?void 0:e[Symbol.iterator]),_=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,P=/-->/g,R=/>/g,L=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),F=/'/g,D=/"/g,B=/^(?:script|style|textarea|title)$/i,N=e=>(t,...r)=>({_$litType$:e,strings:t,values:r}),q=N(1),U=(N(2),Symbol.for("lit-noChange")),z=Symbol.for("lit-nothing"),M=new WeakMap,H=O.createTreeWalker(O,129,null,!1),W=(e,t)=>{const r=e.length-1,n=[];let o,a=2===t?"":"",i=_;for(let t=0;t"===l[0]?(i=null!=o?o:_,c=-1):void 0===l[1]?c=-2:(c=i.lastIndex-l[2].length,s=l[1],i=void 0===l[3]?L:'"'===l[3]?D:F):i===D||i===F?i=L:i===P||i===R?i=_:(i=L,o=void 0);const d=i===L&&e[t+1].startsWith("/>")?" ":"";a+=i===_?r+E:c>=0?(n.push(s),r.slice(0,c)+"$lit$"+r.slice(c)+S+d):r+S+(-2===c?(n.push(void 0),t):d)}const s=a+(e[r]||"")+(2===t?"":"");if(!Array.isArray(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==k?k.createHTML(s):s,n]};class V{constructor({strings:e,_$litType$:t},r){let n;this.parts=[];let o=0,a=0;const i=e.length-1,s=this.parts,[l,c]=W(e,t);if(this.el=V.createElement(l,r),H.currentNode=this.el.content,2===t){const e=this.el.content,t=e.firstChild;t.remove(),e.append(...t.childNodes)}for(;null!==(n=H.nextNode())&&s.length0){n.textContent=$?$.emptyScript:"";for(let r=0;r2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=z}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(e,t=this,r,n){const o=this.strings;let a=!1;if(void 0===o)e=G(this,e,t,0),a=!C(e)||e!==this._$AH&&e!==U,a&&(this._$AH=e);else{const n=e;let i,s;for(e=o[0],i=0;i{var n,o;const a=null!==(n=null==r?void 0:r.renderBefore)&&void 0!==n?n:t;let i=a._$litPart$;if(void 0===i){const e=null!==(o=null==r?void 0:r.renderBefore)&&void 0!==o?o:null;a._$litPart$=i=new J(t.insertBefore(T(),e),e,void 0,null!=r?r:{})}return i._$AI(e),i})(t,this.renderRoot,this.renderOptions)}connectedCallback(){var e;super.connectedCallback(),null===(e=this._$Dt)||void 0===e||e.setConnected(!0)}disconnectedCallback(){var e;super.disconnectedCallback(),null===(e=this._$Dt)||void 0===e||e.setConnected(!1)}render(){return U}}ie.finalized=!0,ie._$litElement$=!0,null===(oe=globalThis.litElementHydrateSupport)||void 0===oe||oe.call(globalThis,{LitElement:ie});const se=globalThis.litElementPolyfillSupport;null==se||se({LitElement:ie});function le(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}(null!==(ae=globalThis.litElementVersions)&&void 0!==ae?ae:globalThis.litElementVersions=[]).push("3.2.0");let ce={async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const pe=/[&<>"']/,de=new RegExp(pe.source,"g"),ue=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,he=new RegExp(ue.source,"g"),fe={"&":"&","<":"<",">":">",'"':""","'":"'"},me=e=>fe[e];function ye(e,t){if(t){if(pe.test(e))return e.replace(de,me)}else if(ue.test(e))return e.replace(he,me);return e}const ge=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function ve(e){return e.replace(ge,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const be=/(^|[^\[])\^/g;function xe(e,t){e="string"==typeof e?e:e.source,t=t||"";const r={replace:(t,n)=>(n=(n=n.source||n).replace(be,"$1"),e=e.replace(t,n),r),getRegex:()=>new RegExp(e,t)};return r}const we=/[^\w:]/g,$e=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function ke(e,t,r){if(e){let e;try{e=decodeURIComponent(ve(r)).replace(we,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!$e.test(r)&&(r=function(e,t){Se[" "+e]||(Ae.test(e)?Se[" "+e]=e+"/":Se[" "+e]=Ie(e,"/",!0));const r=-1===(e=Se[" "+e]).indexOf(":");return"//"===t.substring(0,2)?r?t:e.replace(Ee,"$1")+t:"/"===t.charAt(0)?r?t:e.replace(Oe,"$1")+t:e+t}(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r}const Se={},Ae=/^[^:]+:\/*[^/]*$/,Ee=/^([^:]+:)[\s\S]*$/,Oe=/^([^:]+:\/*[^/]*)[\s\S]*$/;const Te={exec:function(){}};function Ce(e){let t,r,n=1;for(;n{let n=!1,o=t;for(;--o>=0&&"\\"===r[o];)n=!n;return n?"|":" |"})).split(/ \|/);let n=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(r+=e),t>>=1,e+=e;return r+e}function Re(e,t,r,n){const o=t.href,a=t.title?ye(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){n.state.inLink=!0;const e={type:"link",raw:r,href:o,title:a,text:i,tokens:n.inlineTokens(i)};return n.state.inLink=!1,e}return{type:"image",raw:r,href:o,title:a,text:ye(i)}}class Le{constructor(e){this.options=e||ce}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:Ie(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],r=function(e,t){const r=e.match(/^(\s+)(?:```)/);if(null===r)return t;const n=r[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[r]=t;return r.length>=n.length?e.slice(n.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline._escapes,"$1"):t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=Ie(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *>[ \t]?/gm,""),r=this.lexer.state.top;this.lexer.state.top=!0;const n=this.lexer.blockTokens(e);return this.lexer.state.top=r,{type:"blockquote",raw:t[0],tokens:n,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let r,n,o,a,i,s,l,c,p,d,u,h,f=t[1].trim();const m=f.length>1,y={type:"list",raw:"",ordered:m,start:m?+f.slice(0,-1):"",loose:!1,items:[]};f=m?`\\d{1,9}\\${f.slice(-1)}`:`\\${f}`,this.options.pedantic&&(f=m?f:"[*+-]");const g=new RegExp(`^( {0,3}${f})((?:[\t ][^\\n]*)?(?:\\n|$))`);for(;e&&(h=!1,t=g.exec(e))&&!this.rules.block.hr.test(e);){if(r=t[0],e=e.substring(r.length),c=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),p=e.split("\n",1)[0],this.options.pedantic?(a=2,u=c.trimLeft()):(a=t[2].search(/[^ ]/),a=a>4?1:a,u=c.slice(a),a+=t[1].length),s=!1,!c&&/^ *$/.test(p)&&(r+=p+"\n",e=e.substring(p.length+1),h=!0),!h){const t=new RegExp(`^ {0,${Math.min(3,a-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,a-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),o=new RegExp(`^ {0,${Math.min(3,a-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,a-1)}}#`);for(;e&&(d=e.split("\n",1)[0],p=d,this.options.pedantic&&(p=p.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!o.test(p))&&!i.test(p)&&!t.test(p)&&!n.test(e);){if(p.search(/[^ ]/)>=a||!p.trim())u+="\n"+p.slice(a);else{if(s)break;if(c.search(/[^ ]/)>=4)break;if(o.test(c))break;if(i.test(c))break;if(n.test(c))break;u+="\n"+p}s||p.trim()||(s=!0),r+=d+"\n",e=e.substring(d.length+1),c=p.slice(a)}}y.loose||(l?y.loose=!0:/\n *\n *$/.test(r)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(u),n&&(o="[ ] "!==n[0],u=u.replace(/^\[[ xX]\] +/,""))),y.items.push({type:"list_item",raw:r,task:!!n,checked:o,loose:!1,text:u}),y.raw+=r}y.items[y.items.length-1].raw=r.trimRight(),y.items[y.items.length-1].text=u.trimRight(),y.raw=y.raw.trimRight();const v=y.items.length;for(i=0;i"space"===e.type)),t=e.length>0&&e.some((e=>/\n.*\n/.test(e.raw)));y.loose=t}if(y.loose)for(i=0;i$/,"$1").replace(this.rules.inline._escapes,"$1"):"",n=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline._escapes,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:r,title:n}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:je(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let r,n,o,a,i=e.align.length;for(r=0;r({text:e})));for(i=e.header.length,n=0;n/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):ye(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=Ie(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const r=e.length;let n=0,o=0;for(;o-1){const r=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let r=t[2],n="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);e&&(r=e[1],n=e[3])}else n=t[3]?t[3].slice(1,-1):"";return r=r.trim(),/^$/.test(e)?r.slice(1):r.slice(1,-1)),Re(t,{href:r?r.replace(this.rules.inline._escapes,"$1"):r,title:n?n.replace(this.rules.inline._escapes,"$1"):n},t[0],this.lexer)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let e=(r[2]||r[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e){const e=r[0].charAt(0);return{type:"text",raw:e,text:e}}return Re(r,e,r[0],this.lexer)}}emStrong(e,t,r=""){let n=this.rules.inline.emStrong.lDelim.exec(e);if(!n)return;if(n[3]&&r.match(/[\p{L}\p{N}]/u))return;const o=n[1]||n[2]||"";if(!o||o&&(""===r||this.rules.inline.punctuation.exec(r))){const r=n[0].length-1;let o,a,i=r,s=0;const l="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(l.lastIndex=0,t=t.slice(-1*e.length+r);null!=(n=l.exec(t));){if(o=n[1]||n[2]||n[3]||n[4]||n[5]||n[6],!o)continue;if(a=o.length,n[3]||n[4]){i+=a;continue}if((n[5]||n[6])&&r%3&&!((r+a)%3)){s+=a;continue}if(i-=a,i>0)continue;a=Math.min(a,a+i+s);const t=e.slice(0,r+n.index+(n[0].length-o.length)+a);if(Math.min(r,a)%2){const e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}const l=t.slice(2,-2);return{type:"strong",raw:t,text:l,tokens:this.lexer.inlineTokens(l)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const r=/[^ ]/.test(e),n=/^ /.test(e)&&/ $/.test(e);return r&&n&&(e=e.substring(1,e.length-1)),e=ye(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e,t){const r=this.rules.inline.autolink.exec(e);if(r){let e,n;return"@"===r[2]?(e=ye(this.options.mangle?t(r[1]):r[1]),n="mailto:"+e):(e=ye(r[1]),n=e),{type:"link",raw:r[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let r;if(r=this.rules.inline.url.exec(e)){let e,n;if("@"===r[2])e=ye(this.options.mangle?t(r[0]):r[0]),n="mailto:"+e;else{let t;do{t=r[0],r[0]=this.rules.inline._backpedal.exec(r[0])[0]}while(t!==r[0]);e=ye(r[0]),n="www."===r[1]?"http://"+r[0]:r[0]}return{type:"link",raw:r[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const r=this.rules.inline.text.exec(e);if(r){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(r[0]):ye(r[0]):r[0]:ye(this.options.smartypants?t(r[0]):r[0]),{type:"text",raw:r[0],text:e}}}}const Fe={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:Te,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};Fe.def=xe(Fe.def).replace("label",Fe._label).replace("title",Fe._title).getRegex(),Fe.bullet=/(?:[*+-]|\d{1,9}[.)])/,Fe.listItemStart=xe(/^( *)(bull) */).replace("bull",Fe.bullet).getRegex(),Fe.list=xe(Fe.list).replace(/bull/g,Fe.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+Fe.def.source+")").getRegex(),Fe._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",Fe._comment=/|$)/,Fe.html=xe(Fe.html,"i").replace("comment",Fe._comment).replace("tag",Fe._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Fe.paragraph=xe(Fe._paragraph).replace("hr",Fe.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Fe._tag).getRegex(),Fe.blockquote=xe(Fe.blockquote).replace("paragraph",Fe.paragraph).getRegex(),Fe.normal=Ce({},Fe),Fe.gfm=Ce({},Fe.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),Fe.gfm.table=xe(Fe.gfm.table).replace("hr",Fe.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Fe._tag).getRegex(),Fe.gfm.paragraph=xe(Fe._paragraph).replace("hr",Fe.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",Fe.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Fe._tag).getRegex(),Fe.pedantic=Ce({},Fe.normal,{html:xe("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",Fe._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Te,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:xe(Fe.normal._paragraph).replace("hr",Fe.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",Fe.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});const De={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:Te,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:Te,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(r="x"+r.toString(16)),n+="&#"+r+";";return n}De._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",De.punctuation=xe(De.punctuation).replace(/punctuation/g,De._punctuation).getRegex(),De.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,De.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,De._comment=xe(Fe._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),De.emStrong.lDelim=xe(De.emStrong.lDelim).replace(/punct/g,De._punctuation).getRegex(),De.emStrong.rDelimAst=xe(De.emStrong.rDelimAst,"g").replace(/punct/g,De._punctuation).getRegex(),De.emStrong.rDelimUnd=xe(De.emStrong.rDelimUnd,"g").replace(/punct/g,De._punctuation).getRegex(),De._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,De._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,De._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,De.autolink=xe(De.autolink).replace("scheme",De._scheme).replace("email",De._email).getRegex(),De._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,De.tag=xe(De.tag).replace("comment",De._comment).replace("attribute",De._attribute).getRegex(),De._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,De._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,De._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,De.link=xe(De.link).replace("label",De._label).replace("href",De._href).replace("title",De._title).getRegex(),De.reflink=xe(De.reflink).replace("label",De._label).replace("ref",Fe._label).getRegex(),De.nolink=xe(De.nolink).replace("ref",Fe._label).getRegex(),De.reflinkSearch=xe(De.reflinkSearch,"g").replace("reflink",De.reflink).replace("nolink",De.nolink).getRegex(),De.normal=Ce({},De),De.pedantic=Ce({},De.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:xe(/^!?\[(label)\]\((.*?)\)/).replace("label",De._label).getRegex(),reflink:xe(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",De._label).getRegex()}),De.gfm=Ce({},De.normal,{escape:xe(De.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\t+" ".repeat(r.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((n=>!!(r=n.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.space(e))e=e.substring(r.raw.length),1===r.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),n=t[t.length-1],!n||"paragraph"!==n.type&&"text"!==n.type?t.push(r):(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=n.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),n=t[t.length-1],!n||"paragraph"!==n.type&&"text"!==n.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(n.raw+="\n"+r.raw,n.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=n.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else{if(o=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const r=e.slice(1);let n;this.options.extensions.startBlock.forEach((function(e){n=e.call({lexer:this},r),"number"==typeof n&&n>=0&&(t=Math.min(t,n))})),t<1/0&&t>=0&&(o=e.substring(0,t+1))}if(this.state.top&&(r=this.tokenizer.paragraph(o)))n=t[t.length-1],a&&"paragraph"===n.type?(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=n.text):t.push(r),a=o.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===n.type?(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=n.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let r,n,o,a,i,s,l=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(a=this.tokenizer.rules.inline.reflinkSearch.exec(l));)e.includes(a[0].slice(a[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,a.index)+"["+Pe("a",a[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(a=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,a.index)+"["+Pe("a",a[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(a=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,a.index+a[0].length-2)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex),this.tokenizer.rules.inline.escapedEmSt.lastIndex--;for(;e;)if(i||(s=""),i=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((n=>!!(r=n.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===r.type&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===r.type&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,s))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,Ne))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,Ne))){if(o=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const r=e.slice(1);let n;this.options.extensions.startInline.forEach((function(e){n=e.call({lexer:this},r),"number"==typeof n&&n>=0&&(t=Math.min(t,n))})),t<1/0&&t>=0&&(o=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(o,Be))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(s=r.raw.slice(-1)),i=!0,n=t[t.length-1],n&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}}class Ue{constructor(e){this.options=e||ce}code(e,t,r){const n=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,n);null!=t&&t!==e&&(r=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",n?'
'+(r?e:ye(e,!0))+"
\n":"
"+(r?e:ye(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e){return e}heading(e,t,r,n){if(this.options.headerIds){return`${e}\n`}return`${e}\n`}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,r){const n=t?"ol":"ul";return"<"+n+(t&&1!==r?' start="'+r+'"':"")+">\n"+e+"\n"}listitem(e){return`
  • ${e}
  • \n`}checkbox(e){return" "}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const r=t.header?"th":"td";return(t.align?`<${r} align="${t.align}">`:`<${r}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return this.options.xhtml?"
    ":"
    "}del(e){return`${e}`}link(e,t,r){if(null===(e=ke(this.options.sanitize,this.options.baseUrl,e)))return r;let n='",n}image(e,t,r){if(null===(e=ke(this.options.sanitize,this.options.baseUrl,e)))return r;let n=`${r}":">",n}text(e){return e}}class ze{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,r){return""+r}image(e,t,r){return""+r}br(){return""}}class Me{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let r=e,n=0;if(this.seen.hasOwnProperty(r)){n=this.seen[e];do{n++,r=e+"-"+n}while(this.seen.hasOwnProperty(r))}return t||(this.seen[e]=n,this.seen[r]=0),r}slug(e,t={}){const r=this.serialize(e);return this.getNextSafeSlug(r,t.dryrun)}}class He{constructor(e){this.options=e||ce,this.options.renderer=this.options.renderer||new Ue,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ze,this.slugger=new Me}static parse(e,t){return new He(t).parse(e)}static parseInline(e,t){return new He(t).parseInline(e)}parse(e,t=!0){let r,n,o,a,i,s,l,c,p,d,u,h,f,m,y,g,v,b,x,w="";const $=e.length;for(r=0;r<$;r++)if(d=e[r],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[d.type]&&(x=this.options.extensions.renderers[d.type].call({parser:this},d),!1!==x||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(d.type)))w+=x||"";else switch(d.type){case"space":continue;case"hr":w+=this.renderer.hr();continue;case"heading":w+=this.renderer.heading(this.parseInline(d.tokens),d.depth,ve(this.parseInline(d.tokens,this.textRenderer)),this.slugger);continue;case"code":w+=this.renderer.code(d.text,d.lang,d.escaped);continue;case"table":for(c="",l="",a=d.header.length,n=0;n0&&"paragraph"===y.tokens[0].type?(y.tokens[0].text=b+" "+y.tokens[0].text,y.tokens[0].tokens&&y.tokens[0].tokens.length>0&&"text"===y.tokens[0].tokens[0].type&&(y.tokens[0].tokens[0].text=b+" "+y.tokens[0].tokens[0].text)):y.tokens.unshift({type:"text",text:b}):m+=b),m+=this.parse(y.tokens,f),p+=this.renderer.listitem(m,v,g);w+=this.renderer.list(p,u,h);continue;case"html":w+=this.renderer.html(d.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(d.tokens));continue;case"text":for(p=d.tokens?this.parseInline(d.tokens):d.text;r+1<$&&"text"===e[r+1].type;)d=e[++r],p+="\n"+(d.tokens?this.parseInline(d.tokens):d.text);w+=t?this.renderer.paragraph(p):p;continue;default:{const e='Token with "'+d.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw new Error(e)}}return w}parseInline(e,t){t=t||this.renderer;let r,n,o,a="";const i=e.length;for(r=0;r{n(e.text,e.lang,(function(t,r){if(t)return a(t);null!=r&&r!==e.text&&(e.text=r,e.escaped=!0),i--,0===i&&a()}))}),0))})),void(0===i&&a())}function n(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

    An error occurred:

    "+ye(e.message+"",!0)+"
    ";throw e}try{const r=qe.lex(e,t);if(t.walkTokens){if(t.async)return Promise.all(We.walkTokens(r,t.walkTokens)).then((()=>He.parse(r,t))).catch(n);We.walkTokens(r,t.walkTokens)}return He.parse(r,t)}catch(e){n(e)}}We.options=We.setOptions=function(e){var t;return Ce(We.defaults,e),t=We.defaults,ce=t,We},We.getDefaults=le,We.defaults=ce,We.use=function(...e){const t=We.defaults.extensions||{renderers:{},childTokens:{}};e.forEach((e=>{const r=Ce({},e);if(r.async=We.defaults.async||r.async,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const r=t.renderers[e.name];t.renderers[e.name]=r?function(...t){let n=e.renderer.apply(this,t);return!1===n&&(n=r.apply(this,t)),n}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");t[e.level]?t[e.level].unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),r.extensions=t),e.renderer){const t=We.defaults.renderer||new Ue;for(const r in e.renderer){const n=t[r];t[r]=(...o)=>{let a=e.renderer[r].apply(t,o);return!1===a&&(a=n.apply(t,o)),a}}r.renderer=t}if(e.tokenizer){const t=We.defaults.tokenizer||new Le;for(const r in e.tokenizer){const n=t[r];t[r]=(...o)=>{let a=e.tokenizer[r].apply(t,o);return!1===a&&(a=n.apply(t,o)),a}}r.tokenizer=t}if(e.walkTokens){const t=We.defaults.walkTokens;r.walkTokens=function(r){let n=[];return n.push(e.walkTokens.call(this,r)),t&&(n=n.concat(t.call(this,r))),n}}We.setOptions(r)}))},We.walkTokens=function(e,t){let r=[];for(const n of e)switch(r=r.concat(t.call(We,n)),n.type){case"table":for(const e of n.header)r=r.concat(We.walkTokens(e.tokens,t));for(const e of n.rows)for(const n of e)r=r.concat(We.walkTokens(n.tokens,t));break;case"list":r=r.concat(We.walkTokens(n.items,t));break;default:We.defaults.extensions&&We.defaults.extensions.childTokens&&We.defaults.extensions.childTokens[n.type]?We.defaults.extensions.childTokens[n.type].forEach((function(e){r=r.concat(We.walkTokens(n[e],t))})):n.tokens&&(r=r.concat(We.walkTokens(n.tokens,t)))}return r},We.parseInline=function(e,t){if(null==e)throw new Error("marked.parseInline(): input parameter is undefined or null");if("string"!=typeof e)throw new Error("marked.parseInline(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");_e(t=Ce({},We.defaults,t||{}));try{const r=qe.lexInline(e,t);return t.walkTokens&&We.walkTokens(r,t.walkTokens),He.parseInline(r,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

    An error occurred:

    "+ye(e.message+"",!0)+"
    ";throw e}},We.Parser=He,We.parser=He.parse,We.Renderer=Ue,We.TextRenderer=ze,We.Lexer=qe,We.lexer=qe.lex,We.Tokenizer=Le,We.Slugger=Me,We.parse=We;We.options,We.setOptions,We.use,We.walkTokens,We.parseInline,He.parse,qe.lex;var Ve=r(660),Ge=r.n(Ve);r(251),r(358),r(46),r(503),r(277),r(874),r(366),r(57),r(16);const Ke=c` + .hover-bg:hover{ + background: var(--bg3); + } + ::selection { + background: var(--selection-bg); + color: var(--selection-fg); + } + .regular-font{ + font-family:var(--font-regular); + } + .mono-font { + font-family:var(--font-mono); + } + .title { + font-size: calc(var(--font-size-small) + 18px); + font-weight: normal + } + .sub-title{ font-size: 20px;} + .req-res-title { + font-family: var(--font-regular); + font-size: calc(var(--font-size-small) + 4px); + font-weight:bold; + margin-bottom:8px; + text-align:left; + } + .tiny-title { + font-size:calc(var(--font-size-small) + 1px); + font-weight:bold; + } + .regular-font-size { font-size: var(--font-size-regular); } + .small-font-size { font-size: var(--font-size-small); } + .upper { text-transform: uppercase; } + .primary-text{ color: var(--primary-color); } + .bold-text { font-weight:bold; } + .gray-text { color: var(--light-fg); } + .red-text {color: var(--red)} + .blue-text {color: var(--blue)} + .multiline { + overflow: scroll; + max-height: var(--resp-area-height, 400px); + color: var(--fg3); + } + .method-fg.put { color: var(--orange); } + .method-fg.post { color: var(--green); } + .method-fg.get { color: var(--blue); } + .method-fg.delete { color: var(--red); } + .method-fg.options, + .method-fg.head, + .method-fg.patch { + color: var(--yellow); + } + + h1{ font-family:var(--font-regular); font-size:28px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h2{ font-family:var(--font-regular); font-size:24px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h3{ font-family:var(--font-regular); font-size:18px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h4{ font-family:var(--font-regular); font-size:16px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h5{ font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h6{ font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + + h1,h2,h3,h4,h5,h5{ + margin-block-end: 0.2em; + } + p { margin-block-start: 0.5em; } + a { color: var(--blue); cursor:pointer; } + a.inactive-link { + color:var(--fg); + text-decoration: none; + cursor:text; + } + + code, + pre { + margin: 0px; + font-family: var(--font-mono); + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown, + .m-markdown-small { + display:block; + } + + .m-markdown p, + .m-markdown span { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 8px); + } + .m-markdown li { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 10px); + } + + .m-markdown-small p, + .m-markdown-small span, + .m-markdown-small li { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 6px); + } + .m-markdown-small li { + line-height: calc(var(--font-size-small) + 8px); + } + + .m-markdown p:not(:first-child) { + margin-block-start: 24px; + } + + .m-markdown-small p:not(:first-child) { + margin-block-start: 12px; + } + .m-markdown-small p:first-child { + margin-block-start: 0; + } + + .m-markdown p, + .m-markdown-small p { + margin-block-end: 0 + } + + .m-markdown code span { + font-size:var(--font-size-mono); + } + + .m-markdown-small code, + .m-markdown code { + padding: 1px 6px; + border-radius: 2px; + color: var(--inline-code-fg); + background-color: var(--bg3); + font-size: calc(var(--font-size-mono)); + line-height: 1.2; + } + + .m-markdown-small code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown-small pre, + .m-markdown pre { + white-space: pre-wrap; + overflow-x: auto; + line-height: normal; + border-radius: 2px; + border: 1px solid var(--code-border-color); + } + + .m-markdown pre { + padding: 12px; + background-color: var(--code-bg); + color:var(--code-fg); + } + + .m-markdown-small pre { + margin-top: 4px; + padding: 2px 4px; + background-color: var(--bg3); + color: var(--fg2); + } + + .m-markdown-small pre code, + .m-markdown pre code { + border:none; + padding:0; + } + + .m-markdown pre code { + color: var(--code-fg); + background-color: var(--code-bg); + background-color: transparent; + } + + .m-markdown-small pre code { + color: var(--fg2); + background-color: var(--bg3); + } + + .m-markdown ul, + .m-markdown ol { + padding-inline-start: 30px; + } + + .m-markdown-small ul, + .m-markdown-small ol { + padding-inline-start: 20px; + } + + .m-markdown-small a, + .m-markdown a { + color:var(--blue); + } + + .m-markdown-small img, + .m-markdown img { + max-width: 100%; + } + + /* Markdown table */ + + .m-markdown-small table, + .m-markdown table { + border-spacing: 0; + margin: 10px 0; + border-collapse: separate; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: calc(var(--font-size-small) + 1px); + line-height: calc(var(--font-size-small) + 4px); + max-width: 100%; + } + + .m-markdown-small table { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 2px); + margin: 8px 0; + } + + .m-markdown-small td, + .m-markdown-small th, + .m-markdown td, + .m-markdown th { + vertical-align: top; + border-top: 1px solid var(--border-color); + line-height: calc(var(--font-size-small) + 4px); + } + + .m-markdown-small tr:first-child th, + .m-markdown tr:first-child th { + border-top: 0 none; + } + + .m-markdown th, + .m-markdown td { + padding: 10px 12px; + } + + .m-markdown-small th, + .m-markdown-small td { + padding: 8px 8px; + } + + .m-markdown th, + .m-markdown-small th { + font-weight: 600; + background-color: var(--bg2); + vertical-align: middle; + } + + .m-markdown-small table code { + font-size: calc(var(--font-size-mono) - 2px); + } + + .m-markdown table code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown blockquote, + .m-markdown-small blockquote { + margin-inline-start: 0; + margin-inline-end: 0; + border-left: 3px solid var(--border-color); + padding: 6px 0 6px 6px; + } + .m-markdown hr{ + border: 1px solid var(--border-color); + } +`,Je=c` +/* Button */ +.m-btn { + border-radius: var(--border-radius); + font-weight: 600; + display: inline-block; + padding: 6px 16px; + font-size: var(--font-size-small); + outline: 0; + line-height: 1; + text-align: center; + white-space: nowrap; + border: 2px solid var(--primary-color); + background-color:transparent; + transition: background-color 0.2s; + user-select: none; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} +.m-btn.primary { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.thin-border { border-width: 1px; } +.m-btn.large { padding:8px 14px; } +.m-btn.small { padding:5px 12px; } +.m-btn.tiny { padding:5px 6px; } +.m-btn.circle { border-radius: 50%; } +.m-btn:hover { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.nav { border: 2px solid var(--nav-accent-color); } +.m-btn.nav:hover { + background-color: var(--nav-accent-color); +} +.m-btn:disabled{ + background-color: var(--bg3); + color: var(--fg3); + border-color: var(--fg3); + cursor: not-allowed; + opacity: 0.4; +} +.toolbar-btn{ + cursor: pointer; + padding: 4px; + margin:0 2px; + font-size: var(--font-size-small); + min-width: 50px; + color: var(--primary-color-invert); + border-radius: 2px; + border: none; + background-color: var(--primary-color); +} + +input, textarea, select, button, pre { + color:var(--fg); + outline: none; + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); +} +button { + font-family: var(--font-regular); +} + +/* Form Inputs */ +pre, +select, +textarea, +input[type="file"], +input[type="text"], +input[type="password"] { + font-family: var(--font-mono); + font-weight: 400; + font-size: var(--font-size-small); + transition: border .2s; + padding: 6px 5px; +} + +select { + font-family: var(--font-regular); + padding: 5px 30px 5px 5px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%3E%3Cpath%20d%3D%22M10.3%203.3L6%207.6%201.7%203.3A1%201%200%2000.3%204.7l5%205a1%201%200%20001.4%200l5-5a1%201%200%2010-1.4-1.4z%22%20fill%3D%22%23777777%22%2F%3E%3C%2Fsvg%3E"); + background-position: calc(100% - 5px) center; + background-repeat: no-repeat; + background-size: 10px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; +} + +select:hover { + border-color: var(--primary-color); +} + +textarea::placeholder, +input[type="text"]::placeholder, +input[type="password"]::placeholder { + color: var(--placeholder-color); + opacity:1; +} + + +input[type="file"]{ + font-family: var(--font-regular); + padding:2px; + cursor:pointer; + border: 1px solid var(--primary-color); + min-height: calc(var(--font-size-small) + 18px); +} + +input[type="file"]::-webkit-file-upload-button { + font-family: var(--font-regular); + font-size: var(--font-size-small); + outline: none; + cursor:pointer; + padding: 3px 8px; + border: 1px solid var(--primary-color); + background-color: var(--primary-color); + color: var(--primary-color-invert); + border-radius: var(--border-radius);; + -webkit-appearance: none; +} + +pre, +textarea { + scrollbar-width: thin; + scrollbar-color: var(--border-color) var(--input-bg); +} + +pre::-webkit-scrollbar, +textarea::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +pre::-webkit-scrollbar-track, +textarea::-webkit-scrollbar-track { + background:var(--input-bg); +} + +pre::-webkit-scrollbar-thumb, +textarea::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: var(--border-color); +} + +.link { + font-size:var(--font-size-small); + text-decoration: underline; + color:var(--blue); + font-family:var(--font-mono); + margin-bottom:2px; +} + +/* Toggle Body */ +input[type="checkbox"] { + appearance: none; + display: inline-block; + background-color: var(--light-bg); + border: 1px solid var(--light-bg); + border-radius: 9px; + cursor: pointer; + height: 18px; + position: relative; + transition: border .25s .15s, box-shadow .25s .3s, padding .25s; + min-width: 36px; + width: 36px; + vertical-align: top; +} +/* Toggle Thumb */ +input[type="checkbox"]:after { + position: absolute; + background-color: var(--bg); + border: 1px solid var(--light-bg); + border-radius: 8px; + content: ''; + top: 0px; + left: 0px; + right: 16px; + display: block; + height: 16px; + transition: border .25s .15s, left .25s .1s, right .15s .175s; +} + +/* Toggle Body - Checked */ +input[type="checkbox"]:checked { + background-color: var(--green); + border-color: var(--green); +} +/* Toggle Thumb - Checked*/ +input[type="checkbox"]:checked:after { + border: 1px solid var(--green); + left: 16px; + right: 1px; + transition: border .25s, left .15s .25s, right .25s .175s; +}`,Ye=c` +.row, .col{ + display:flex; +} +.row { + align-items:center; + flex-direction: row; +} +.col { + align-items:stretch; + flex-direction: column; +} +`,Ze=c` +.m-table { + border-spacing: 0; + border-collapse: separate; + border: 1px solid var(--light-border-color); + border-radius: var(--border-radius); + margin: 0; + max-width: 100%; + direction: ltr; +} +.m-table tr:first-child td, +.m-table tr:first-child th { + border-top: 0 none; +} +.m-table td, +.m-table th { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 4px); + padding: 4px 5px 4px; + vertical-align: top; +} + +.m-table.padded-12 td, +.m-table.padded-12 th { + padding: 12px; +} + +.m-table td:not([align]), +.m-table th:not([align]) { + text-align: left; +} + +.m-table th { + color: var(--fg2); + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 18px); + font-weight: 600; + letter-spacing: normal; + background-color: var(--bg2); + vertical-align: bottom; + border-bottom: 1px solid var(--light-border-color); +} + +.m-table > tbody > tr > td, +.m-table > tr > td { + border-top: 1px solid var(--light-border-color); + text-overflow: ellipsis; + overflow: hidden; +} +.table-title { + font-size:var(--font-size-small); + font-weight:bold; + vertical-align: middle; + margin: 12px 0 4px 0; +} +`,Qe=c` +.only-large-screen { display:none; } +.endpoint-head .path{ + display: flex; + font-family:var(--font-mono); + font-size: var(--font-size-small); + align-items: center; + overflow-wrap: break-word; + word-break: break-all; +} + +.endpoint-head .descr { + font-size: var(--font-size-small); + color:var(--light-fg); + font-weight:400; + align-items: center; + overflow-wrap: break-word; + word-break: break-all; + display:none; +} + +.m-endpoint.expanded{margin-bottom:16px; } +.m-endpoint > .endpoint-head{ + border-width:1px 1px 1px 5px; + border-style:solid; + border-color:transparent; + border-top-color:var(--light-border-color); + display:flex; + padding:6px 16px; + align-items: center; + cursor: pointer; +} +.m-endpoint > .endpoint-head.put:hover, +.m-endpoint > .endpoint-head.put.expanded{ + border-color:var(--orange); + background-color:var(--light-orange); +} +.m-endpoint > .endpoint-head.post:hover, +.m-endpoint > .endpoint-head.post.expanded { + border-color:var(--green); + background-color:var(--light-green); +} +.m-endpoint > .endpoint-head.get:hover, +.m-endpoint > .endpoint-head.get.expanded { + border-color:var(--blue); + background-color:var(--light-blue); +} +.m-endpoint > .endpoint-head.delete:hover, +.m-endpoint > .endpoint-head.delete.expanded { + border-color:var(--red); + background-color:var(--light-red); +} + +.m-endpoint > .endpoint-head.head:hover, +.m-endpoint > .endpoint-head.head.expanded, +.m-endpoint > .endpoint-head.patch:hover, +.m-endpoint > .endpoint-head.patch.expanded, +.m-endpoint > .endpoint-head.options:hover, +.m-endpoint > .endpoint-head.options.expanded { + border-color:var(--yellow); + background-color:var(--light-yellow); +} + +.m-endpoint > .endpoint-head.deprecated:hover, +.m-endpoint > .endpoint-head.deprecated.expanded { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.m-endpoint .endpoint-body { + flex-wrap:wrap; + padding:16px 0px 0 0px; + border-width:0px 1px 1px 5px; + border-style:solid; + box-shadow: 0px 4px 3px -3px rgba(0, 0, 0, 0.15); +} +.m-endpoint .endpoint-body.delete{ border-color:var(--red); } +.m-endpoint .endpoint-body.put{ border-color:var(--orange); } +.m-endpoint .endpoint-body.post{border-color:var(--green);} +.m-endpoint .endpoint-body.get{ border-color:var(--blue); } +.m-endpoint .endpoint-body.head, +.m-endpoint .endpoint-body.patch, +.m-endpoint .endpoint-body.options { + border-color:var(--yellow); +} + +.m-endpoint .endpoint-body.deprecated{ + border-color:var(--border-color); + filter:opacity(0.6); +} + +.endpoint-head .deprecated{ + color: var(--light-fg); + filter:opacity(0.6); +} + +.summary{ + padding:8px 8px; +} +.summary .title{ + font-size:calc(var(--font-size-regular) + 2px); + margin-bottom: 6px; + word-break: break-all; +} + +.endpoint-head .method{ + padding:2px 5px; + vertical-align: middle; + font-size:var(--font-size-small); + height: calc(var(--font-size-small) + 16px); + line-height: calc(var(--font-size-small) + 8px); + width: 60px; + border-radius: 2px; + display:inline-block; + text-align: center; + font-weight: bold; + text-transform:uppercase; + margin-right:5px; +} +.endpoint-head .method.delete{ border: 2px solid var(--red);} +.endpoint-head .method.put{ border: 2px solid var(--orange); } +.endpoint-head .method.post{ border: 2px solid var(--green); } +.endpoint-head .method.get{ border: 2px solid var(--blue); } +.endpoint-head .method.get.deprecated{ border: 2px solid var(--border-color); } +.endpoint-head .method.head, +.endpoint-head .method.patch, +.endpoint-head .method.options { + border: 2px solid var(--yellow); +} + +.req-resp-container { + display: flex; + margin-top:16px; + align-items: stretch; + flex-wrap: wrap; + flex-direction: column; + border-top:1px solid var(--light-border-color); +} + +.view-mode-request, +api-response.view-mode { + flex:1; + min-height:100px; + padding:16px 8px; + overflow:hidden; +} +.view-mode-request { + border-width:0 0 1px 0; + border-style:dashed; +} + +.head .view-mode-request, +.patch .view-mode-request, +.options .view-mode-request { + border-color:var(--yellow); +} +.put .view-mode-request { + border-color:var(--orange); +} +.post .view-mode-request { + border-color:var(--green); +} +.get .view-mode-request { + border-color:var(--blue); +} +.delete .view-mode-request { + border-color:var(--red); +} + +@media only screen and (min-width: 1024px) { + .only-large-screen { display:block; } + .endpoint-head .path{ + font-size: var(--font-size-regular); + } + .endpoint-head .descr{ + display: flex; + } + .endpoint-head .m-markdown-small, + .descr .m-markdown-small{ + display:block; + } + .req-resp-container{ + flex-direction: var(--layout, row); + flex-wrap: nowrap; + } + api-response.view-mode { + padding:16px; + } + .view-mode-request.row-layout { + border-width:0 1px 0 0; + padding:16px; + } + .summary{ + padding:8px 16px; + } +} +`,Xe=c` +code[class*="language-"], +pre[class*="language-"] { + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + tab-size: 2; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: var(--light-fg) +} + +.token.punctuation { + color: var(--fg); +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color:var(--pink); +} + +.token.function-name { + color: var(--blue); +} + +.token.boolean, +.token.number, +.token.function { + color: var(--red); +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: var(--code-property-color); +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: var(--code-keyword-color); +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: var(--green); +} + +.token.operator, +.token.entity, +.token.url { + color: var(--code-operator-color); +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: green; +} +`,et=c` +.tab-panel { + border: none; +} +.tab-buttons { + height:30px; + padding: 4px 4px 0 4px; + border-bottom: 1px solid var(--light-border-color) ; + align-items: stretch; + overflow-y: hidden; + overflow-x: auto; + scrollbar-width: thin; +} +.tab-buttons::-webkit-scrollbar { + height: 1px; + background-color: var(--border-color); +} +.tab-btn { + border: none; + border-bottom: 3px solid transparent; + color: var(--light-fg); + background-color: transparent; + white-space: nowrap; + cursor:pointer; + outline:none; + font-family:var(--font-regular); + font-size:var(--font-size-small); + margin-right:16px; + padding:1px; +} +.tab-btn.active { + border-bottom: 3px solid var(--primary-color); + font-weight:bold; + color:var(--primary-color); +} + +.tab-btn:hover { + color:var(--primary-color); +} +.tab-content { + margin:-1px 0 0 0; + position:relative; + min-height: 50px; +} +`,tt=c` +.nav-bar-info:focus-visible, +.nav-bar-tag:focus-visible, +.nav-bar-path:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: -4px; +} +.nav-bar-expand-all:focus-visible, +.nav-bar-collapse-all:focus-visible, +.nav-bar-tag-icon:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: 2px; +} +.nav-bar { + width:0; + height:100%; + overflow: hidden; + color:var(--nav-text-color); + background-color: var(--nav-bg-color); + background-blend-mode: multiply; + line-height: calc(var(--font-size-small) + 4px); + display:none; + position:relative; + flex-direction:column; + flex-wrap:nowrap; + word-break:break-word; +} +::slotted([slot=nav-logo]){ + padding:16px 16px 0 16px; +} +.nav-scroll { + overflow-x: hidden; + overflow-y: auto; + overflow-y: overlay; + scrollbar-width: thin; + scrollbar-color: var(--nav-hover-bg-color) transparent; +} + +.nav-bar-tag { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} +.nav-bar.read .nav-bar-tag-icon { + display:none; +} +.nav-bar-paths-under-tag { + overflow:hidden; + transition: max-height .2s ease-out, visibility .3s; +} +.collapsed .nav-bar-paths-under-tag { + visibility: hidden; +} + +.nav-bar-expand-all { + transform: rotate(90deg); + cursor:pointer; + margin-right:10px; +} +.nav-bar-collapse-all { + transform: rotate(270deg); + cursor:pointer; +} +.nav-bar-expand-all:hover, .nav-bar-collapse-all:hover { + color: var(--primary-color); +} + +.nav-bar-tag-icon { + color: var(--nav-text-color); + font-size: 20px; +} +.nav-bar-tag-icon:hover { + color:var(--nav-hover-text-color); +} +.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transform: rotate(-90deg); + transition: transform 0.2s ease-out 0s; +} +.nav-bar.focused .nav-bar-tag-and-paths.expanded .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transition: transform 0.2s ease-out 0s; +} +.nav-scroll::-webkit-scrollbar { + width: var(--scroll-bar-width, 8px); +} +.nav-scroll::-webkit-scrollbar-track { + background:transparent; +} +.nav-scroll::-webkit-scrollbar-thumb { + background-color: var(--nav-hover-bg-color); +} + +.nav-bar-tag { + font-size: var(--font-size-regular); + color: var(--nav-accent-color); + border-left:4px solid transparent; + font-weight:bold; + padding: 15px 15px 15px 10px; + text-transform: capitalize; +} + +.nav-bar-components, +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-info, +.nav-bar-tag, +.nav-bar-path { + display:flex; + cursor: pointer; + width: 100%; + border: none; + border-radius:4px; + color: var(--nav-text-color); + background: transparent; + border-left:4px solid transparent; +} + +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-path { + font-size: calc(var(--font-size-small) + 1px); + padding: var(--nav-item-padding); +} +.nav-bar-path.small-font { + font-size: var(--font-size-small); +} + +.nav-bar-info { + font-size: var(--font-size-regular); + padding: 16px 10px; + font-weight:bold; +} +.nav-bar-section { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: var(--font-size-small); + color: var(--nav-text-color); + padding: var(--nav-item-padding); + font-weight:bold; +} +.nav-bar-section.operations { + cursor:pointer; +} +.nav-bar-section.operations:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} + +.nav-bar-section:first-child { + display: none; +} +.nav-bar-h2 {margin-left:12px;} + +.nav-bar-h1.left-bar.active, +.nav-bar-h2.left-bar.active, +.nav-bar-info.left-bar.active, +.nav-bar-tag.left-bar.active, +.nav-bar-path.left-bar.active, +.nav-bar-section.left-bar.operations.active { + border-left:4px solid var(--nav-accent-color); + color:var(--nav-hover-text-color); +} + +.nav-bar-h1.colored-block.active, +.nav-bar-h2.colored-block.active, +.nav-bar-info.colored-block.active, +.nav-bar-tag.colored-block.active, +.nav-bar-path.colored-block.active, +.nav-bar-section.colored-block.operations.active { + background-color: var(--nav-accent-color); + color: var(--nav-accent-text-color); + border-radius: 0; +} + +.nav-bar-h1:hover, +.nav-bar-h2:hover, +.nav-bar-info:hover, +.nav-bar-tag:hover, +.nav-bar-path:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} +`,rt=c` +#api-info { + font-size: calc(var(--font-size-regular) - 1px); + margin-top: 8px; + margin-left: -15px; +} + +#api-info span:before { + content: "|"; + display: inline-block; + opacity: 0.5; + width: 15px; + text-align: center; +} +#api-info span:first-child:before { + content: ""; + width: 0px; +} +`,nt=c` + +`;const ot=/[\s#:?&={}]/g,at="_rapidoc_api_key";function it(e){return new Promise((t=>setTimeout(t,e)))}function st(e,t){const r=t.target,n=document.createElement("textarea");n.value=e,n.style.position="fixed",document.body.appendChild(n),n.focus(),n.select();try{document.execCommand("copy"),r.innerText="Copied",setTimeout((()=>{r.innerText="Copy"}),5e3)}catch(e){console.error("Unable to copy",e)}document.body.removeChild(n)}function lt(e,t,r="includes"){if("includes"===r){return`${t.method} ${t.path} ${t.summary||t.description||""} ${t.operationId||""}`.toLowerCase().includes(e.toLowerCase())}return new RegExp(e,"i").test(`${t.method} ${t.path}`)}function ct(e,t=new Set){return e?(Object.keys(e).forEach((r=>{var n;if(t.add(r),e[r].properties)ct(e[r].properties,t);else if(null!==(n=e[r].items)&&void 0!==n&&n.properties){var o;ct(null===(o=e[r].items)||void 0===o?void 0:o.properties,t)}})),t):t}function pt(e,t){if(e){const r=document.createElement("a");document.body.appendChild(r),r.style="display: none",r.href=e,r.download=t,r.click(),r.remove()}}function dt(e){if(e){const t=document.createElement("a");document.body.appendChild(t),t.style="display: none",t.href=e,t.target="_blank",t.click(),t.remove()}}function ut(e){return e&&e.t&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ht=function(e){return e&&e.Math==Math&&e},ft=ht("object"==typeof globalThis&&globalThis)||ht("object"==typeof window&&window)||ht("object"==typeof self&&self)||ht("object"==typeof ft&&ft)||function(){return this}()||Function("return this")(),mt=function(e){try{return!!e()}catch(e){return!0}},yt=!mt((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")})),gt=yt,vt=Function.prototype,bt=vt.apply,xt=vt.call,wt="object"==typeof Reflect&&Reflect.apply||(gt?xt.bind(bt):function(){return xt.apply(bt,arguments)}),$t=yt,kt=Function.prototype,St=kt.bind,At=kt.call,Et=$t&&St.bind(At,At),Ot=$t?function(e){return e&&Et(e)}:function(e){return e&&function(){return At.apply(e,arguments)}},Tt=function(e){return"function"==typeof e},Ct={},jt=!mt((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),It=yt,_t=Function.prototype.call,Pt=It?_t.bind(_t):function(){return _t.apply(_t,arguments)},Rt={},Lt={}.propertyIsEnumerable,Ft=Object.getOwnPropertyDescriptor,Dt=Ft&&!Lt.call({1:2},1);Rt.f=Dt?function(e){var t=Ft(this,e);return!!t&&t.enumerable}:Lt;var Bt,Nt,qt=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},Ut=Ot,zt=Ut({}.toString),Mt=Ut("".slice),Ht=function(e){return Mt(zt(e),8,-1)},Wt=Ot,Vt=mt,Gt=Ht,Kt=ft.Object,Jt=Wt("".split),Yt=Vt((function(){return!Kt("z").propertyIsEnumerable(0)}))?function(e){return"String"==Gt(e)?Jt(e,""):Kt(e)}:Kt,Zt=ft.TypeError,Qt=function(e){if(null==e)throw Zt("Can't call method on "+e);return e},Xt=Yt,er=Qt,tr=function(e){return Xt(er(e))},rr=Tt,nr=function(e){return"object"==typeof e?null!==e:rr(e)},or={},ar=or,ir=ft,sr=Tt,lr=function(e){return sr(e)?e:void 0},cr=function(e,t){return arguments.length<2?lr(ar[e])||lr(ir[e]):ar[e]&&ar[e][t]||ir[e]&&ir[e][t]},pr=Ot({}.isPrototypeOf),dr=cr("navigator","userAgent")||"",ur=ft,hr=dr,fr=ur.process,mr=ur.Deno,yr=fr&&fr.versions||mr&&mr.version,gr=yr&&yr.v8;gr&&(Nt=(Bt=gr.split("."))[0]>0&&Bt[0]<4?1:+(Bt[0]+Bt[1])),!Nt&&hr&&(!(Bt=hr.match(/Edge\/(\d+)/))||Bt[1]>=74)&&(Bt=hr.match(/Chrome\/(\d+)/))&&(Nt=+Bt[1]);var vr=Nt,br=vr,xr=mt,wr=!!Object.getOwnPropertySymbols&&!xr((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&br&&br<41})),$r=wr&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,kr=cr,Sr=Tt,Ar=pr,Er=$r,Or=ft.Object,Tr=Er?function(e){return"symbol"==typeof e}:function(e){var t=kr("Symbol");return Sr(t)&&Ar(t.prototype,Or(e))},Cr=ft.String,jr=function(e){try{return Cr(e)}catch(e){return"Object"}},Ir=Tt,_r=jr,Pr=ft.TypeError,Rr=function(e){if(Ir(e))return e;throw Pr(_r(e)+" is not a function")},Lr=Rr,Fr=function(e,t){var r=e[t];return null==r?void 0:Lr(r)},Dr=Pt,Br=Tt,Nr=nr,qr=ft.TypeError,Ur={exports:{}},zr=ft,Mr=Object.defineProperty,Hr=ft.i||function(e,t){try{Mr(zr,e,{value:t,configurable:!0,writable:!0})}catch(r){zr[e]=t}return t}("__core-js_shared__",{}),Wr=Hr;(Ur.exports=function(e,t){return Wr[e]||(Wr[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.21.1",mode:"pure",copyright:"© 2014-2022 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.21.1/LICENSE",source:"https://github.com/zloirock/core-js"});var Vr=Qt,Gr=ft.Object,Kr=function(e){return Gr(Vr(e))},Jr=Kr,Yr=Ot({}.hasOwnProperty),Zr=Object.hasOwn||function(e,t){return Yr(Jr(e),t)},Qr=Ot,Xr=0,en=Math.random(),tn=Qr(1..toString),rn=function(e){return"Symbol("+(void 0===e?"":e)+")_"+tn(++Xr+en,36)},nn=ft,on=Ur.exports,an=Zr,sn=rn,ln=wr,cn=$r,pn=on("wks"),dn=nn.Symbol,un=dn&&dn.for,hn=cn?dn:dn&&dn.withoutSetter||sn,fn=function(e){if(!an(pn,e)||!ln&&"string"!=typeof pn[e]){var t="Symbol."+e;ln&&an(dn,e)?pn[e]=dn[e]:pn[e]=cn&&un?un(t):hn(t)}return pn[e]},mn=Pt,yn=nr,gn=Tr,vn=Fr,bn=fn,xn=ft.TypeError,wn=bn("toPrimitive"),$n=function(e,t){if(!yn(e)||gn(e))return e;var r,n=vn(e,wn);if(n){if(void 0===t&&(t="default"),r=mn(n,e,t),!yn(r)||gn(r))return r;throw xn("Can't convert object to primitive value")}return void 0===t&&(t="number"),function(e,t){var r,n;if("string"===t&&Br(r=e.toString)&&!Nr(n=Dr(r,e)))return n;if(Br(r=e.valueOf)&&!Nr(n=Dr(r,e)))return n;if("string"!==t&&Br(r=e.toString)&&!Nr(n=Dr(r,e)))return n;throw qr("Can't convert object to primitive value")}(e,t)},kn=Tr,Sn=function(e){var t=$n(e,"string");return kn(t)?t:t+""},An=nr,En=ft.document,On=An(En)&&An(En.createElement),Tn=function(e){return On?En.createElement(e):{}},Cn=Tn,jn=!jt&&!mt((function(){return 7!=Object.defineProperty(Cn("div"),"a",{get:function(){return 7}}).a})),In=jt,_n=Pt,Pn=Rt,Rn=qt,Ln=tr,Fn=Sn,Dn=Zr,Bn=jn,Nn=Object.getOwnPropertyDescriptor;Ct.f=In?Nn:function(e,t){if(e=Ln(e),t=Fn(t),Bn)try{return Nn(e,t)}catch(e){}if(Dn(e,t))return Rn(!_n(Pn.f,e,t),e[t])};var qn=mt,Un=Tt,zn=/#|\.prototype\./,Mn=function(e,t){var r=Wn[Hn(e)];return r==Gn||r!=Vn&&(Un(t)?qn(t):!!t)},Hn=Mn.normalize=function(e){return String(e).replace(zn,".").toLowerCase()},Wn=Mn.data={},Vn=Mn.NATIVE="N",Gn=Mn.POLYFILL="P",Kn=Mn,Jn=Rr,Yn=yt,Zn=Ot(Ot.bind),Qn=function(e,t){return Jn(e),void 0===t?e:Yn?Zn(e,t):function(){return e.apply(t,arguments)}},Xn={},eo=jt&&mt((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),to=ft,ro=nr,no=to.String,oo=to.TypeError,ao=function(e){if(ro(e))return e;throw oo(no(e)+" is not an object")},io=jt,so=jn,lo=eo,co=ao,po=Sn,uo=ft.TypeError,ho=Object.defineProperty,fo=Object.getOwnPropertyDescriptor;Xn.f=io?lo?function(e,t,r){if(co(e),t=po(t),co(r),"function"==typeof e&&"prototype"===t&&"value"in r&&"writable"in r&&!r.writable){var n=fo(e,t);n&&n.writable&&(e[t]=r.value,r={configurable:"configurable"in r?r.configurable:n.configurable,enumerable:"enumerable"in r?r.enumerable:n.enumerable,writable:!1})}return ho(e,t,r)}:ho:function(e,t,r){if(co(e),t=po(t),co(r),so)try{return ho(e,t,r)}catch(e){}if("get"in r||"set"in r)throw uo("Accessors not supported");return"value"in r&&(e[t]=r.value),e};var mo=Xn,yo=qt,go=jt?function(e,t,r){return mo.f(e,t,yo(1,r))}:function(e,t,r){return e[t]=r,e},vo=ft,bo=wt,xo=Ot,wo=Tt,$o=Ct.f,ko=Kn,So=or,Ao=Qn,Eo=go,Oo=Zr,To=function(e){var t=function(r,n,o){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(r);case 2:return new e(r,n)}return new e(r,n,o)}return bo(e,this,arguments)};return t.prototype=e.prototype,t},Co=function(e,t){var r,n,o,a,i,s,l,c,p=e.target,d=e.global,u=e.stat,h=e.proto,f=d?vo:u?vo[p]:(vo[p]||{}).prototype,m=d?So:So[p]||Eo(So,p,{})[p],y=m.prototype;for(o in t)r=!ko(d?o:p+(u?".":"#")+o,e.forced)&&f&&Oo(f,o),i=m[o],r&&(s=e.noTargetGet?(c=$o(f,o))&&c.value:f[o]),a=r&&s?s:t[o],r&&typeof i==typeof a||(l=e.bind&&r?Ao(a,vo):e.wrap&&r?To(a):h&&wo(a)?xo(a):a,(e.sham||a&&a.sham||i&&i.sham)&&Eo(l,"sham",!0),Eo(m,o,l),h&&(Oo(So,n=p+"Prototype")||Eo(So,n,{}),Eo(So[n],o,a),e.real&&y&&!y[o]&&Eo(y,o,a)))},jo=Math.ceil,Io=Math.floor,_o=function(e){var t=+e;return t!=t||0===t?0:(t>0?Io:jo)(t)},Po=_o,Ro=Math.max,Lo=Math.min,Fo=function(e,t){var r=Po(e);return r<0?Ro(r+t,0):Lo(r,t)},Do=_o,Bo=Math.min,No=function(e){return e>0?Bo(Do(e),9007199254740991):0},qo=No,Uo=function(e){return qo(e.length)},zo=tr,Mo=Fo,Ho=Uo,Wo=function(e){return function(t,r,n){var o,a=zo(t),i=Ho(a),s=Mo(n,i);if(e&&r!=r){for(;i>s;)if((o=a[s++])!=o)return!0}else for(;i>s;s++)if((e||s in a)&&a[s]===r)return e||s||0;return!e&&-1}},Vo={includes:Wo(!0),indexOf:Wo(!1)},Go={},Ko=Zr,Jo=tr,Yo=Vo.indexOf,Zo=Go,Qo=Ot([].push),Xo=function(e,t){var r,n=Jo(e),o=0,a=[];for(r in n)!Ko(Zo,r)&&Ko(n,r)&&Qo(a,r);for(;t.length>o;)Ko(n,r=t[o++])&&(~Yo(a,r)||Qo(a,r));return a},ea=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],ta=Xo,ra=ea,na=Object.keys||function(e){return ta(e,ra)},oa=Kr,aa=na;Co({target:"Object",stat:!0,forced:mt((function(){aa(1)}))},{keys:function(e){return aa(oa(e))}});var ia=or.Object.keys;const sa=ut({exports:{}}.exports=ia);var la=Ht,ca=Array.isArray||function(e){return"Array"==la(e)},pa={};pa[fn("toStringTag")]="z";var da="[object z]"===String(pa),ua=ft,ha=da,fa=Tt,ma=Ht,ya=fn("toStringTag"),ga=ua.Object,va="Arguments"==ma(function(){return arguments}()),ba=ha?ma:function(e){var t,r,n;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=ga(e),ya))?r:va?ma(t):"Object"==(n=ma(t))&&fa(t.callee)?"Arguments":n},xa=ba,wa=ft.String,$a=function(e){if("Symbol"===xa(e))throw TypeError("Cannot convert a Symbol value to a string");return wa(e)},ka={},Sa=jt,Aa=eo,Ea=Xn,Oa=ao,Ta=tr,Ca=na;ka.f=Sa&&!Aa?Object.defineProperties:function(e,t){Oa(e);for(var r,n=Ta(t),o=Ca(t),a=o.length,i=0;a>i;)Ea.f(e,r=o[i++],n[r]);return e};var ja,Ia=cr("document","documentElement"),_a=Ur.exports,Pa=rn,Ra=_a("keys"),La=function(e){return Ra[e]||(Ra[e]=Pa(e))},Fa=ao,Da=ka,Ba=ea,Na=Go,qa=Ia,Ua=Tn,za=La("IE_PROTO"),Ma=function(){},Ha=function(e){return" + + + + + \ No newline at end of file