mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 22:09:09 +02:00
create reverse proxy domain manager and api
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
package domains
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/http/util"
|
||||||
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
manager manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterEndpoints(router *mux.Router) {
|
||||||
|
h := &handler{}
|
||||||
|
|
||||||
|
router.HandleFunc("/domains", h.getAllDomains).Methods("GET", "OPTIONS")
|
||||||
|
router.HandleFunc("/domains", h.createCustomDomain).Methods("POST", "OPTIONS")
|
||||||
|
router.HandleFunc("/domains/{domainId}", h.deleteCustomDomain).Methods("DELETE", "OPTIONS")
|
||||||
|
router.HandleFunc("/domains/{domainId}/validate", h.triggerCustomDomainValidation).Methods("GET", "OPTIONS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func domainTypeToApi(t domainType) api.ReverseProxyDomainType {
|
||||||
|
switch t {
|
||||||
|
case domainTypeCustom:
|
||||||
|
return api.ReverseProxyDomainTypeCustom
|
||||||
|
case domainTypeFree:
|
||||||
|
return api.ReverseProxyDomainTypeFree
|
||||||
|
}
|
||||||
|
// By default return as a "free" domain as that is more restrictive.
|
||||||
|
// TODO: is this correct?
|
||||||
|
return api.ReverseProxyDomainTypeFree
|
||||||
|
}
|
||||||
|
|
||||||
|
func domainToApi(d domain) api.ReverseProxyDomain {
|
||||||
|
return api.ReverseProxyDomain{
|
||||||
|
Domain: d.Domain,
|
||||||
|
Id: d.ID,
|
||||||
|
Type: domainTypeToApi(d.Type),
|
||||||
|
Validated: d.Validated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) getAllDomains(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domains, err := h.manager.GetDomains(r.Context(), userAuth.AccountId)
|
||||||
|
|
||||||
|
var ret []api.ReverseProxyDomain
|
||||||
|
for _, d := range domains {
|
||||||
|
ret = append(ret, domainToApi(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
util.WriteJSONObject(r.Context(), w, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) createCustomDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req api.PostApiReverseProxyDomainsJSONRequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := h.manager.CreateDomain(r.Context(), userAuth.AccountId, req.Domain)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.WriteJSONObject(r.Context(), w, domainToApi(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) deleteCustomDomain(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domainID := mux.Vars(r)["domainId"]
|
||||||
|
if domainID == "" {
|
||||||
|
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "domain ID is required"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.manager.DeleteDomain(r.Context(), userAuth.AccountId, domainID); err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) triggerCustomDomainValidation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(r.Context(), err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
domainID := mux.Vars(r)["domainId"]
|
||||||
|
if domainID == "" {
|
||||||
|
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "domain ID is required"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go h.manager.ValidateDomain(userAuth.AccountId, domainID)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package domains
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type domainType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
domainTypeFree domainType = "free"
|
||||||
|
domainTypeCustom domainType = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type domain struct {
|
||||||
|
ID string
|
||||||
|
Domain string
|
||||||
|
Type domainType
|
||||||
|
Validated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type store interface {
|
||||||
|
GetAccountDomainNonce(ctx context.Context, accountID string) (nonce string, err error)
|
||||||
|
GetCustomDomain(ctx context.Context, accountID string, domainID string) (domain, error)
|
||||||
|
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
|
||||||
|
ListCustomDomains(ctx context.Context, accountID string) ([]domain, error)
|
||||||
|
CreateCustomDomain(ctx context.Context, accountID string, domainName string, validated bool) (domain, error)
|
||||||
|
UpdateCustomDomain(ctx context.Context, accountID string, d domain) (domain, error)
|
||||||
|
DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type manager struct {
|
||||||
|
store store
|
||||||
|
validator Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m manager) GetDomains(ctx context.Context, accountID string) ([]domain, error) {
|
||||||
|
nonce, err := m.store.GetAccountDomainNonce(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get account domain nonce: %w", err)
|
||||||
|
}
|
||||||
|
free, err := m.store.ListFreeDomains(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list free domains: %w", err)
|
||||||
|
}
|
||||||
|
domains, err := m.store.ListCustomDomains(ctx, accountID)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: check for "no records" type error. Because that is a success condition.
|
||||||
|
return nil, fmt.Errorf("list custom domains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend each free domain with the account nonce and then add it to the domain
|
||||||
|
// array to be returned.
|
||||||
|
// This account nonce is added to free domains to prevent users being able to
|
||||||
|
// query free domain usage across accounts and simplifies tracking free domain
|
||||||
|
// usage across accounts.
|
||||||
|
for _, name := range free {
|
||||||
|
domains = append(domains, domain{
|
||||||
|
Domain: nonce + "." + name,
|
||||||
|
Type: domainTypeFree,
|
||||||
|
Validated: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return domains, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m manager) CreateDomain(ctx context.Context, accountID, domainName string) (domain, error) {
|
||||||
|
// Attempt an initial validation; however, a failure is still acceptable for creation
|
||||||
|
// because the user may not yet have configured their DNS records, or the DNS update
|
||||||
|
// has not yet reached the servers that are queried by the validation resolver.
|
||||||
|
var validated bool
|
||||||
|
// TODO: retrieve in use reverse proxy addresses from somewhere!
|
||||||
|
var reverseProxyAddresses []string
|
||||||
|
if m.validator.IsValid(ctx, domainName, reverseProxyAddresses) {
|
||||||
|
validated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := m.store.CreateCustomDomain(ctx, accountID, domainName, validated)
|
||||||
|
if err != nil {
|
||||||
|
return d, fmt.Errorf("create domain in store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m manager) DeleteDomain(ctx context.Context, accountID, domainID string) error {
|
||||||
|
if err := m.store.DeleteCustomDomain(ctx, accountID, domainID); err != nil {
|
||||||
|
// TODO: check for "no records" type error. Because that is a success condition.
|
||||||
|
return fmt.Errorf("delete domain from store: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m manager) ValidateDomain(accountID, domainID string) {
|
||||||
|
d, err := m.store.GetCustomDomain(context.Background(), accountID, domainID)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: something? Log?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: retrieve in use reverse proxy addresses from somewhere!
|
||||||
|
var reverseProxyAddresses []string
|
||||||
|
if m.validator.IsValid(context.Background(), d.Domain, reverseProxyAddresses) {
|
||||||
|
d.Validated = true
|
||||||
|
if _, err := m.store.UpdateCustomDomain(context.Background(), accountID, d); err != nil {
|
||||||
|
// TODO: something? Log?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-10
@@ -1,13 +1,4 @@
|
|||||||
// Package domainvalidation provides a mechanism for verifying ownership of
|
package domains
|
||||||
// a domain.
|
|
||||||
// It is intended to be used before custom domains can be assigned to reverse
|
|
||||||
// proxy services.
|
|
||||||
// Acceptable domains should be set to the known domains that reverse proxy
|
|
||||||
// servers are hosted at.
|
|
||||||
//
|
|
||||||
// After a custom domain is validated, it should be pinned to a single account
|
|
||||||
// to prevent domain abuse across accounts.
|
|
||||||
package domainvalidation
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
package domainvalidation_test
|
package domains_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/modules/services/domainvalidation"
|
"github.com/netbirdio/netbird/management/internals/modules/services/domains"
|
||||||
)
|
)
|
||||||
|
|
||||||
type resolver struct {
|
type resolver struct {
|
||||||
@@ -46,7 +46,7 @@ func TestIsValid(t *testing.T) {
|
|||||||
|
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
validator := domainvalidation.NewValidator(test.resolver)
|
validator := domains.NewValidator(test.resolver)
|
||||||
actual := validator.IsValid(t.Context(), test.domain, test.accept)
|
actual := validator.IsValid(t.Context(), test.domain, test.accept)
|
||||||
if test.expect != actual {
|
if test.expect != actual {
|
||||||
t.Errorf("Incorrect return value:\nexpect: %v\nactual: %v", test.expect, actual)
|
t.Errorf("Incorrect return value:\nexpect: %v\nactual: %v", test.expect, actual)
|
||||||
@@ -2581,6 +2581,40 @@ components:
|
|||||||
description: Whether link auth is enabled
|
description: Whether link auth is enabled
|
||||||
required:
|
required:
|
||||||
- enabled
|
- enabled
|
||||||
|
ReverseProxyDomainType:
|
||||||
|
type: string
|
||||||
|
description: Type of Reverse Proxy Domain
|
||||||
|
enum:
|
||||||
|
- free
|
||||||
|
- custom
|
||||||
|
example: free
|
||||||
|
ReverseProxyDomain:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Domain ID
|
||||||
|
domain:
|
||||||
|
type: string
|
||||||
|
description: Domain name
|
||||||
|
validated:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the domain has been validated
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/ReverseProxyDomainType'
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- domain
|
||||||
|
- validated
|
||||||
|
- type
|
||||||
|
ReverseProxyDomainRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
type: string
|
||||||
|
description: Domain name
|
||||||
|
required:
|
||||||
|
- domain
|
||||||
InstanceStatus:
|
InstanceStatus:
|
||||||
type: object
|
type: object
|
||||||
description: Instance status information
|
description: Instance status information
|
||||||
@@ -5929,3 +5963,116 @@ paths:
|
|||||||
"$ref": "#/components/responses/not_found"
|
"$ref": "#/components/responses/not_found"
|
||||||
'500':
|
'500':
|
||||||
"$ref": "#/components/responses/internal_error"
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/reverse-proxy/domains:
|
||||||
|
get:
|
||||||
|
summary: Retrieve Reverse Proxy Domains
|
||||||
|
description: Get information about domains that can be used for Reverse Proxy endpoints.
|
||||||
|
tags: [ Reverse Proxy ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
- TokenAuth: [ ]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: A JSON Array of ReverseProxyDomains
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ReverseProxyDomain'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'404':
|
||||||
|
"$ref": "#/components/responses/not_found"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
post:
|
||||||
|
summary: Create a Custom domain
|
||||||
|
description: Create a new Custom domain for use with Reverse Proxy endpoints, this will trigger an initial validation check
|
||||||
|
tags: [ Reverse Proxy ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
- TokenAuth: [ ]
|
||||||
|
requestBody:
|
||||||
|
description: Custom domain creation request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ReverseProxyDomainRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Reverse proxy created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ReverseProxy'
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'404':
|
||||||
|
"$ref": "#/components/responses/not_found"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/reverse-proxy/domains/{domainId}:
|
||||||
|
delete:
|
||||||
|
summary: Delete a Custom domain
|
||||||
|
description: Delete an existing reverse proxy custom domain
|
||||||
|
tags: [ Reverse Proxy ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
- TokenAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: domainId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The custom domain ID
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Reverse proxy custom domain deleted
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'404':
|
||||||
|
"$ref": "#/components/responses/not_found"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
/api/reverse-proxy/domains/{domainId}/validate:
|
||||||
|
get:
|
||||||
|
summary: Validate a custom domain
|
||||||
|
description: Trigger domain ownership validation for a custom domain
|
||||||
|
tags: [ Reverse Proxy ]
|
||||||
|
security:
|
||||||
|
- BearerAuth: [ ]
|
||||||
|
- TokenAuth: [ ]
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: domainId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: The custom domain ID
|
||||||
|
responses:
|
||||||
|
'202':
|
||||||
|
description: Reverse proxy custom domain validation triggered
|
||||||
|
'400':
|
||||||
|
"$ref": "#/components/responses/bad_request"
|
||||||
|
'401':
|
||||||
|
"$ref": "#/components/responses/requires_authentication"
|
||||||
|
'403':
|
||||||
|
"$ref": "#/components/responses/forbidden"
|
||||||
|
'404':
|
||||||
|
"$ref": "#/components/responses/not_found"
|
||||||
|
'500':
|
||||||
|
"$ref": "#/components/responses/internal_error"
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ const (
|
|||||||
ReverseProxyAuthConfigTypePin ReverseProxyAuthConfigType = "pin"
|
ReverseProxyAuthConfigTypePin ReverseProxyAuthConfigType = "pin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Defines values for ReverseProxyDomainType.
|
||||||
|
const (
|
||||||
|
ReverseProxyDomainTypeCustom ReverseProxyDomainType = "custom"
|
||||||
|
ReverseProxyDomainTypeFree ReverseProxyDomainType = "free"
|
||||||
|
)
|
||||||
|
|
||||||
// Defines values for ReverseProxyTargetProtocol.
|
// Defines values for ReverseProxyTargetProtocol.
|
||||||
const (
|
const (
|
||||||
ReverseProxyTargetProtocolHttp ReverseProxyTargetProtocol = "http"
|
ReverseProxyTargetProtocolHttp ReverseProxyTargetProtocol = "http"
|
||||||
@@ -1776,6 +1782,30 @@ type ReverseProxyAuthConfig struct {
|
|||||||
// ReverseProxyAuthConfigType Authentication type
|
// ReverseProxyAuthConfigType Authentication type
|
||||||
type ReverseProxyAuthConfigType string
|
type ReverseProxyAuthConfigType string
|
||||||
|
|
||||||
|
// ReverseProxyDomain defines model for ReverseProxyDomain.
|
||||||
|
type ReverseProxyDomain struct {
|
||||||
|
// Domain Domain name
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
|
||||||
|
// Id Domain ID
|
||||||
|
Id string `json:"id"`
|
||||||
|
|
||||||
|
// Type Type of Reverse Proxy Domain
|
||||||
|
Type ReverseProxyDomainType `json:"type"`
|
||||||
|
|
||||||
|
// Validated Whether the domain has been validated
|
||||||
|
Validated bool `json:"validated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseProxyDomainRequest defines model for ReverseProxyDomainRequest.
|
||||||
|
type ReverseProxyDomainRequest struct {
|
||||||
|
// Domain Domain name
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseProxyDomainType Type of Reverse Proxy Domain
|
||||||
|
type ReverseProxyDomainType string
|
||||||
|
|
||||||
// ReverseProxyRequest defines model for ReverseProxyRequest.
|
// ReverseProxyRequest defines model for ReverseProxyRequest.
|
||||||
type ReverseProxyRequest struct {
|
type ReverseProxyRequest struct {
|
||||||
Auth ReverseProxyAuthConfig `json:"auth"`
|
Auth ReverseProxyAuthConfig `json:"auth"`
|
||||||
@@ -2380,6 +2410,9 @@ type PutApiPostureChecksPostureCheckIdJSONRequestBody = PostureCheckUpdate
|
|||||||
// PostApiReverseProxyJSONRequestBody defines body for PostApiReverseProxy for application/json ContentType.
|
// PostApiReverseProxyJSONRequestBody defines body for PostApiReverseProxy for application/json ContentType.
|
||||||
type PostApiReverseProxyJSONRequestBody = ReverseProxyRequest
|
type PostApiReverseProxyJSONRequestBody = ReverseProxyRequest
|
||||||
|
|
||||||
|
// PostApiReverseProxyDomainsJSONRequestBody defines body for PostApiReverseProxyDomains for application/json ContentType.
|
||||||
|
type PostApiReverseProxyDomainsJSONRequestBody = ReverseProxyDomainRequest
|
||||||
|
|
||||||
// PutApiReverseProxyProxyIdJSONRequestBody defines body for PutApiReverseProxyProxyId for application/json ContentType.
|
// PutApiReverseProxyProxyIdJSONRequestBody defines body for PutApiReverseProxyProxyId for application/json ContentType.
|
||||||
type PutApiReverseProxyProxyIdJSONRequestBody = ReverseProxyRequest
|
type PutApiReverseProxyProxyIdJSONRequestBody = ReverseProxyRequest
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user