Public Internet facing gateway (TCP reverse tunnel) for greenhouse 🏔️⛰️🛤️⛰️🏔️
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

756 lines
25 KiB

package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
tunnel "git.sequentialread.com/forest/threshold/tunnel-lib"
)
type ServerConfig struct {
DebugLog bool
ListenPort int // default 9056
// Domain is only used for validating the TLS client certificates
// when TLS is used. the cert's Subject CommonName is expected to be <ClientId>@<Domain>
// I did this because I believe this is a standard for TLS client certs,
// based on domain users/email addresses.
Domain string
// MultiTenantMode ON:
// tenantId is required. ClientId must be formatted `<tenantId>.<nodeId>`
// clients will not be allowed to register listeners capturing all packets on a given port,
// they must specify a hostname, and they must prove that they own it (via a TXT record for example).
// Exception: Each client will get a few allocated ports for SSH & maybe etc???
//
// MultiTenantMode OFF:
// tenantId is N/A. ClientId must be formatted `<nodeId>`
// clients can register listeners with any hostname including null, on any open port.
//
MultiTenantMode bool
MultiTenantInternalAPIListenPort int // default 9057
MultiTenantInternalAPICaCertificateFile string
CaCertificateFilesGlob string
ServerTlsKeyFile string
ServerTlsCertificateFile string
Metrics MetricsConfig
}
type ClientState struct {
CurrentState string
LastState string
}
type ManagementHttpHandler struct {
Domain string
MultiTenantMode bool
ControlHandler http.Handler
}
type BandwidthCounter struct {
Inbound int64
Outbound int64
}
type MultiTenantInternalAPI struct {
InboundByTenant map[string]int64
OutboundByTenant map[string]int64
}
type PrometheusMetricsAPI struct {
MultiTenantServerMode bool
InboundByTenant map[string]int64
OutboundByTenant map[string]int64
InboundByService map[string]int64
OutboundByService map[string]int64
}
type Tenant struct {
PortStart int
PortEnd int
AuthorizedDomains []string
}
// Server State
var listenersByTenant map[string][]ListenerConfig
var clientStatesMutex = &sync.Mutex{}
var tenantStatesMutex = &sync.Mutex{}
var clientStatesByTenant map[string]map[string]ClientState
var tenants map[string]Tenant
var server *tunnel.Server
func runServer(configFileName *string) {
configBytes := getConfigBytes(configFileName)
var config ServerConfig
err := json.Unmarshal(configBytes, &config)
if err != nil {
fmt.Printf("runServer(): can't json.Unmarshal(configBytes, &config) because %s \n", err)
os.Exit(1)
}
configToLog, _ := json.MarshalIndent(config, "", " ")
log.Printf("threshold server is starting up using config:\n%s\n", string(configToLog))
clientStateChangeChannel := make(chan *tunnel.ClientStateChange)
listenersByTenant = map[string][]ListenerConfig{}
var metricChannel chan tunnel.BandwidthMetric = nil
// the Server should only collect metrics when in multi-tenant mode -- this is needed for billing
if config.MultiTenantMode {
metricChannel = make(chan tunnel.BandwidthMetric)
}
tunnelServerConfig := &tunnel.ServerConfig{
StateChanges: clientStateChangeChannel,
ValidateCertificate: validateCertificate,
MultitenantMode: config.MultiTenantMode,
Bandwidth: metricChannel,
Domain: config.Domain,
DebugLog: config.DebugLog,
}
server, err = tunnel.NewServer(tunnelServerConfig)
if err != nil {
fmt.Printf("runServer(): can't create tunnel server because %s \n", err)
os.Exit(1)
}
clientStatesByTenant = make(map[string]map[string]ClientState)
go (func() {
for {
clientStateChange := <-clientStateChangeChannel
previousState := ""
currentState := clientStateChange.Current.String()
tenantId := ""
if config.MultiTenantMode {
tenantIdNodeId := strings.Split(clientStateChange.Identifier, ".")
if len(tenantIdNodeId) != 2 {
fmt.Printf("runServer(): go func(): can't handle clientStateChange with malformed Identifier '%s' \n", clientStateChange.Identifier)
break
}
tenantId = tenantIdNodeId[0]
}
clientStatesMutex.Lock()
if _, hasTenant := clientStatesByTenant[tenantId]; !hasTenant {
clientStatesByTenant[tenantId] = map[string]ClientState{}
}
fromMap, wasInMap := clientStatesByTenant[tenantId][clientStateChange.Identifier]
if wasInMap {
previousState = fromMap.CurrentState
} else {
previousState = clientStateChange.Previous.String()
}
if clientStateChange.Error != nil && clientStateChange.Error != io.EOF {
log.Printf("runServer(): recieved a client state change with an error: %s \n", clientStateChange.Error)
currentState = "ClientError"
}
clientStatesByTenant[tenantId][clientStateChange.Identifier] = ClientState{
CurrentState: currentState,
LastState: previousState,
}
clientStatesMutex.Unlock()
}
})()
certificates, err := filepath.Glob(config.CaCertificateFilesGlob)
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
for _, filename := range certificates {
log.Printf("loading certificate %s, clients who have a key signed by this certificat will be allowed to connect", filename)
caCert, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
caCertPool.AppendCertsFromPEM(caCert)
}
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()
httpsManagementServer := &http.Server{
Addr: fmt.Sprintf(":%d", config.ListenPort),
TLSConfig: tlsConfig,
Handler: &(ManagementHttpHandler{
Domain: config.Domain,
MultiTenantMode: config.MultiTenantMode,
ControlHandler: server,
}),
}
if config.MultiTenantMode {
go (func() {
caCertPool := x509.NewCertPool()
caCert, err := ioutil.ReadFile(config.MultiTenantInternalAPICaCertificateFile)
if err != nil {
log.Fatal(err)
}
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()
internalHandler := &MultiTenantInternalAPI{
InboundByTenant: map[string]int64{},
OutboundByTenant: map[string]int64{},
}
multiTenantInternalServer := &http.Server{
Addr: fmt.Sprintf(":%d", config.MultiTenantInternalAPIListenPort),
TLSConfig: tlsConfig,
Handler: internalHandler,
}
go (func() {
for {
metric := <-metricChannel
tenantIdNodeId := strings.Split(metric.ClientId, ".")
if len(tenantIdNodeId) != 2 {
panic(fmt.Errorf("malformed metric.ClientId '%s', expected <tenantId>.<nodeId>", metric.ClientId))
}
if metric.Inbound {
internalHandler.InboundByTenant[tenantIdNodeId[0]] += int64(metric.Bytes)
} else {
internalHandler.OutboundByTenant[tenantIdNodeId[0]] += int64(metric.Bytes)
}
}
})()
err = multiTenantInternalServer.ListenAndServeTLS(config.ServerTlsCertificateFile, config.ServerTlsKeyFile)
panic(err)
})()
}
log.Print("runServer(): the server should be running now\n")
err = httpsManagementServer.ListenAndServeTLS(config.ServerTlsCertificateFile, config.ServerTlsKeyFile)
panic(err)
}
func validateCertificate(domain string, multiTenantMode bool, request *http.Request) (identifier string, tenantId string, err error) {
if len(request.TLS.PeerCertificates) != 1 {
return "", "", fmt.Errorf("expected exactly 1 TLS client certificate, got %d", len(request.TLS.PeerCertificates))
}
certCommonName := request.TLS.PeerCertificates[0].Subject.CommonName
clientIdDomain := strings.Split(certCommonName, "@")
if len(clientIdDomain) != 2 {
return "", "", fmt.Errorf(
"expected TLS client certificate common name '%s' to match format '<clientId>@<domain>'", certCommonName,
)
}
if clientIdDomain[1] != domain {
return "", "", fmt.Errorf(
"expected TLS client certificate common name domain '%s' to match server domain '%s'",
clientIdDomain[1], domain,
)
}
identifier = clientIdDomain[0]
nodeId := identifier
if multiTenantMode {
tenantIdNodeId := strings.Split(identifier, ".")
if len(tenantIdNodeId) != 2 {
return "", "", fmt.Errorf(
"expected TLS client certificate common name '%s' to match format '<tenantId>.<nodeId>@<domain>'", certCommonName,
)
}
tenantId = tenantIdNodeId[0]
nodeId = tenantIdNodeId[1]
}
mustMatchRegexp := regexp.MustCompile("(?i)^[a-z0-9]+([a-z0-9-_]*[a-z0-9]+)?$")
if !mustMatchRegexp.MatchString(nodeId) {
return "", "", fmt.Errorf("expected TLS client certificate common name nodeId '%s' to be a valid subdomain", nodeId)
}
if tenantId != "" && !mustMatchRegexp.MatchString(tenantId) {
return "", "", fmt.Errorf("expected TLS client certificate common name tenantId '%s' to be a valid subdomain", tenantId)
}
return identifier, tenantId, nil
}
func setListeners(tenantId string, listenerConfigs []ListenerConfig) (int, string) {
currentListenersThatCanKeepRunning := make([]ListenerConfig, 0)
newListenersThatHaveToBeAdded := make([]ListenerConfig, 0)
// I think its probably a good idea to allow the user to create tunnels for nodes that aren't connected
// If a node has a temporary outage (or a threshold server respawns during a node outage)
// that should not prevent the user from setting the tunnels
// If the server tries to dial a disconnected node, it will simply fail dailing with an error.
// for _, newListenerConfig := range listenerConfigs {
// clientState, everHeardOfClientBefore := clientStatesByTenant[tenantId][newListenerConfig.ClientId]
// if !everHeardOfClientBefore {
// return http.StatusNotFound, fmt.Sprintf("Client %s Not Found", newListenerConfig.ClientId)
// }
// if clientState.CurrentState != tunnel.ClientConnected.String() {
// return http.StatusNotFound, fmt.Sprintf("Client %s is not connected it is %s", newListenerConfig.ClientId, clientState.CurrentState)
// }
// }
for _, existingListener := range listenersByTenant[tenantId] {
canKeepRunning := false
for _, newListenerConfig := range listenerConfigs {
if compareListenerConfigs(existingListener, newListenerConfig) {
canKeepRunning = true
}
}
if !canKeepRunning {
listenAddress := net.ParseIP(existingListener.ListenAddress)
if listenAddress == nil {
return http.StatusBadRequest, fmt.Sprintf("Bad Request: \"%s\" is not an IP address.", existingListener.ListenAddress)
}
server.DeleteAddr(listenAddress, existingListener.ListenPort, existingListener.ListenHostnameGlob)
} else {
currentListenersThatCanKeepRunning = append(currentListenersThatCanKeepRunning, existingListener)
}
}
for _, newListenerConfig := range listenerConfigs {
hasToBeAdded := true
for _, existingListener := range listenersByTenant[tenantId] {
if compareListenerConfigs(existingListener, newListenerConfig) {
hasToBeAdded = false
}
}
if hasToBeAdded {
listenAddress := net.ParseIP(newListenerConfig.ListenAddress)
//fmt.Printf("str: %s, listenAddress: %s\n\n", newListenerConfig.ListenAddress, listenAddress)
if listenAddress == nil {
return http.StatusBadRequest, fmt.Sprintf("Bad Request: \"%s\" is not an IP address.", newListenerConfig.ListenAddress)
}
err := server.AddAddr(
listenAddress,
newListenerConfig.ListenPort,
newListenerConfig.ListenHostnameGlob,
newListenerConfig.ClientId,
newListenerConfig.HaProxyProxyProtocol,
newListenerConfig.BackEndService,
)
if err != nil {
if strings.Contains(err.Error(), "already in use") {
return http.StatusConflict, fmt.Sprintf("Port Conflict: Port %s:%d is reserved or already in use", listenAddress, newListenerConfig.ListenPort)
}
log.Printf("setListeners(): can't net.Listen(\"tcp\", \"%s\") because %s \n", listenAddress, err)
return http.StatusInternalServerError, "Unknown Listening Error"
}
newListenersThatHaveToBeAdded = append(newListenersThatHaveToBeAdded, newListenerConfig)
}
}
listenersByTenant[tenantId] = append(currentListenersThatCanKeepRunning, newListenersThatHaveToBeAdded...)
return http.StatusOK, "ok"
}
// TODO move the prometheus metrics to the client
// func exportMetrics(config MetricsConfig, multiTenantServerMode bool, bandwidth <-chan tunnel.BandwidthMetric) {
// metricsAPI := &PrometheusMetricsAPI{
// MultiTenantServerMode: multiTenantServerMode,
// InboundByTenant: map[string]int64{},
// OutboundByTenant: map[string]int64{},
// InboundByService: map[string]int64{},
// OutboundByService: map[string]int64{},
// }
// go (func() {
// for {
// metric := <-bandwidth
// if multiTenantServerMode {
// tenantIdNodeId := strings.Split(metric.ClientId, ".")
// if len(tenantIdNodeId) != 2 {
// panic(fmt.Errorf("malformed metric.ClientId '%s', expected <tenantId>.<nodeId>", metric.ClientId))
// }
// if metric.Inbound {
// metricsAPI.InboundByTenant[tenantIdNodeId[0]] += int64(metric.Bytes)
// } else {
// metricsAPI.OutboundByTenant[tenantIdNodeId[0]] += int64(metric.Bytes)
// }
// } else {
// // TODO shouldn't this be done on the client side only ??
// if metric.Inbound {
// metricsAPI.InboundByService[metric.Service] += int64(metric.Bytes)
// } else {
// metricsAPI.OutboundByService[metric.Service] += int64(metric.Bytes)
// }
// }
// }
// })()
// go (func() {
// metricsServer := &http.Server{
// Addr: fmt.Sprintf(":%d", config.PrometheusMetricsAPIPort),
// Handler: metricsAPI,
// }
// err := metricsServer.ListenAndServe()
// panic(err)
// })()
// }
// // TODO move this to the management API / to the client side.
// func (s *PrometheusMetricsAPI) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
// getMillisecondsSinceUnixEpoch := func() int64 {
// return time.Now().UnixNano() / int64(time.Millisecond)
// }
// writeMetric := func(inbound map[string]int64, outbound map[string]int64, name, tag, desc string) {
// timestamp := getMillisecondsSinceUnixEpoch()
// responseWriter.Write([]byte(fmt.Sprintf("# HELP %s %s\n", name, desc)))
// responseWriter.Write([]byte(fmt.Sprintf("# TYPE %s counter\n", name)))
// for id, bytez := range inbound {
// responseWriter.Write([]byte(fmt.Sprintf("%s{%s=\"%s\",direction=\"inbound\"} %d %d\n", name, tag, id, bytez, timestamp)))
// }
// for id, bytez := range outbound {
// responseWriter.Write([]byte(fmt.Sprintf("%s{%s=\"%s\",direction=\"outbound\"} %d %d\n", name, tag, id, bytez, timestamp)))
// }
// }
// if strings.Contains(request.Header.Get("Accept"), "application/json") && !strings.Contains(request.Header.Get("Accept"), "text/plain") {
// var bytez []byte
// var err error
// if s.MultiTenantServerMode {
// bytez, err = json.MarshalIndent(PrometheusMetricsAPI{
// InboundByTenant: s.InboundByTenant,
// OutboundByTenant: s.OutboundByTenant,
// }, "", " ")
// } else {
// // TODO this should probably only be supported on the client side
// bytez, err = json.MarshalIndent(PrometheusMetricsAPI{
// InboundByService: s.InboundByService,
// OutboundByService: s.OutboundByService,
// }, "", " ")
// }
// if err != nil {
// log.Printf(fmt.Sprintf("500 internal server error: %s", err))
// http.Error(responseWriter, "500 internal server error", http.StatusInternalServerError)
// return
// }
// responseWriter.Header().Set("Content-Type", "application/json")
// responseWriter.Write(bytez)
// } else {
// responseWriter.Header().Set("Content-Type", "text/plain; version=0.0.4")
// if s.MultiTenantServerMode {
// writeMetric(s.InboundByTenant, s.OutboundByTenant, "bandwidth_by_tenant", "tenant", "bandwidth usage by tenant in bytes, excluding usage from control protocol.")
// } else {
// writeMetric(s.InboundByService, s.OutboundByService, "bandwidth_by_service", "service", "bandwidth usage by service in bytes.")
// }
// }
// }
func compareListenerConfigs(a, b ListenerConfig) bool {
return (a.ListenPort == b.ListenPort &&
a.ListenAddress == b.ListenAddress &&
a.ListenHostnameGlob == b.ListenHostnameGlob &&
a.BackEndService == b.BackEndService &&
a.ClientId == b.ClientId &&
a.HaProxyProxyProtocol == b.HaProxyProxyProtocol)
}
func (s *MultiTenantInternalAPI) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
switch path.Clean(request.URL.Path) {
case "/tenantInfo":
tenantId := request.URL.Query().Get("tenantId")
clientStates, hasClientStatesByTenant := clientStatesByTenant[tenantId]
listeners, hasListenersByTenant := listenersByTenant[tenantId]
if tenantId == "" || tenantId == "0" {
http.Error(responseWriter, "400 Bad Request: tenantId url parameter is required", http.StatusBadRequest)
}
if !hasClientStatesByTenant {
clientStates = map[string]ClientState{}
}
if !hasListenersByTenant {
listeners = []ListenerConfig{}
}
resultObject := struct {
ClientStates map[string]ClientState
Listeners []ListenerConfig
}{
ClientStates: clientStates,
Listeners: listeners,
}
bytes, err := json.Marshal(resultObject)
if err != nil {
http.Error(responseWriter, "500 JSON Marshal Error", http.StatusInternalServerError)
return
}
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.Write(bytes)
case "/tenants":
if request.Method == "GET" || request.Method == "PUT" {
if request.Method == "PUT" {
if request.Header.Get("Content-Type") != "application/json" {
http.Error(responseWriter, "415 Unsupported Media Type: Content-Type must be application/json", http.StatusUnsupportedMediaType)
} else {
bodyBytes, err := ioutil.ReadAll(request.Body)
if err != nil {
http.Error(responseWriter, "500 Read Error", http.StatusInternalServerError)
return
}
var newTenants map[string]Tenant
err = json.Unmarshal(bodyBytes, &newTenants)
if err != nil {
log.Printf("422 Unprocessable Entity: Can't Parse JSON: %+v\n", err)
http.Error(responseWriter, "422 Unprocessable Entity: Can't Parse JSON", http.StatusUnprocessableEntity)
return
}
tenantStatesMutex.Lock()
tenants = newTenants
tenantStatesMutex.Unlock()
}
}
tenantStatesMutex.Lock()
bytes, err := json.Marshal(tenants)
tenantStatesMutex.Unlock()
if err != nil {
http.Error(responseWriter, "500 JSON Marshal Error", http.StatusInternalServerError)
return
}
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.Write(bytes)
} else {
responseWriter.Header().Add("Allow", "PUT")
responseWriter.Header().Add("Allow", "GET")
http.Error(responseWriter, "405 Method Not Allowed, try GET or PUT", http.StatusMethodNotAllowed)
}
case "/consumeMetrics":
if request.Method == "GET" {
bytez, err := json.Marshal(struct {
InboundByTenant map[string]int64
OutboundByTenant map[string]int64
}{
InboundByTenant: s.InboundByTenant,
OutboundByTenant: s.OutboundByTenant,
})
if err != nil {
http.Error(responseWriter, "500 JSON Marshal Error", http.StatusInternalServerError)
return
}
inboundToDelete := []string{}
outboundToDelete := []string{}
for k := range s.InboundByTenant {
inboundToDelete = append(inboundToDelete, k)
}
for k := range s.OutboundByTenant {
outboundToDelete = append(outboundToDelete, k)
}
for _, k := range inboundToDelete {
delete(s.InboundByTenant, k)
}
for _, k := range outboundToDelete {
delete(s.OutboundByTenant, k)
}
// bytez2, err := json.Marshal(struct {
// InboundByTenant map[string]int64
// OutboundByTenant map[string]int64
// }{
// InboundByTenant: s.InboundByTenant,
// OutboundByTenant: s.OutboundByTenant,
// })
//log.Printf("returnedBytes: %s\n\ncurrentBytes: %s\n\n", string(bytez), string(bytez2))
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.Write(bytez)
} else {
responseWriter.Header().Set("Allow", "GET")
http.Error(responseWriter, "405 method not allowed, try GET", http.StatusMethodNotAllowed)
}
case "/ping":
if request.Method == "GET" {
fmt.Fprint(responseWriter, "pong")
} else {
responseWriter.Header().Set("Allow", "GET")
http.Error(responseWriter, "405 method not allowed, try GET", http.StatusMethodNotAllowed)
}
default:
http.Error(responseWriter, "404 Not Found, try /tenants or /ping", http.StatusNotFound)
}
}
func (s *ManagementHttpHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
_, tenantId, err := validateCertificate(s.Domain, s.MultiTenantMode, request)
if err != nil {
http.Error(responseWriter, fmt.Sprintf("400 bad request: %s", err.Error()), http.StatusBadRequest)
return
}
if _, hasTenant := clientStatesByTenant[tenantId]; !hasTenant {
clientStatesByTenant[tenantId] = map[string]ClientState{}
}
if _, hasTenant := listenersByTenant[tenantId]; !hasTenant {
listenersByTenant[tenantId] = []ListenerConfig{}
}
switch path.Clean(request.URL.Path) {
case "/clients":
if request.Method == "GET" {
clientStatesMutex.Lock()
bytes, err := json.Marshal(clientStatesByTenant[tenantId])
clientStatesMutex.Unlock()
if err != nil {
http.Error(responseWriter, "500 JSON Marshal Error", http.StatusInternalServerError)
return
}
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.Write(bytes)
} else {
responseWriter.Header().Set("Allow", "GET")
http.Error(responseWriter, "405 Method Not Allowed, try GET", http.StatusMethodNotAllowed)
}
case "/tunnels":
if request.Method == "PUT" {
if request.Header.Get("Content-Type") != "application/json" {
http.Error(responseWriter, "415 Unsupported Media Type: Content-Type must be application/json", http.StatusUnsupportedMediaType)
} else {
bodyBytes, err := ioutil.ReadAll(request.Body)
if err != nil {
http.Error(responseWriter, "500 Read Error", http.StatusInternalServerError)
return
}
var listenerConfigs []ListenerConfig
err = json.Unmarshal(bodyBytes, &listenerConfigs)
if err != nil {
http.Error(responseWriter, "422 Unprocessable Entity: Can't Parse JSON", http.StatusUnprocessableEntity)
return
}
if s.MultiTenantMode {
for _, listenerConfig := range listenerConfigs {
tenantIdNodeId := strings.Split(listenerConfig.ClientId, ".")
if len(tenantIdNodeId) != 2 {
http.Error(
responseWriter,
fmt.Sprintf(
"400 Bad Request: invalid ClientId '%s'. It needs to be in the form '<tenantId>.<nodeId>'",
listenerConfig.ClientId,
),
http.StatusBadRequest,
)
return
}
tenant, hasTenant := tenants[tenantIdNodeId[0]]
if !hasTenant {
http.Error(
responseWriter,
fmt.Sprintf("400 Bad Request: unknown tenantId '%s'", tenantIdNodeId[0]),
http.StatusBadRequest,
)
return
}
isAuthorizedDomain := false
for _, tenantAuthorizedDomain := range tenant.AuthorizedDomains {
isSubdomain := strings.HasSuffix(listenerConfig.ListenHostnameGlob, fmt.Sprintf(".%s", tenantAuthorizedDomain))
if (tenantAuthorizedDomain == listenerConfig.ListenHostnameGlob) || isSubdomain {
isAuthorizedDomain = true
break
}
}
if listenerConfig.ListenHostnameGlob != "" && !isAuthorizedDomain {
http.Error(
responseWriter,
fmt.Sprintf(
`400 Bad Request: ListenHostnameGlob '%s' is not covered by any of your authorized domains [%s].
If you are trying to use a reserved port, leave ListenHostnameGlob blank.`,
listenerConfig.ListenHostnameGlob,
strings.Join(stringSliceMap(tenant.AuthorizedDomains, func(x string) string { return fmt.Sprintf("'%s'", x) }), ", "),
),
http.StatusBadRequest,
)
}
if listenerConfig.ListenHostnameGlob == "" {
isReservedPort := (listenerConfig.ListenPort >= tenant.PortStart && listenerConfig.ListenPort <= tenant.PortEnd)
if !isReservedPort {
http.Error(
responseWriter,
fmt.Sprintf(
"400 Bad Request: ListenHostnameGlob is empty and ListenPort '%d' is not one of your reserved ports %d..%d",
listenerConfig.ListenPort,
tenant.PortStart, tenant.PortEnd,
),
http.StatusBadRequest,
)
}
}
}
}
statusCode, errorMessage := setListeners(tenantId, listenerConfigs)
if statusCode != 200 {
http.Error(responseWriter, errorMessage, statusCode)
return
}
}
}
if request.Method == "PUT" || request.Method == "GET" {
bytes, err := json.Marshal(listenersByTenant[tenantId])
if err != nil {
http.Error(responseWriter, "500 JSON Marshal Error", http.StatusInternalServerError)
return
}
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.Write(bytes)
} else {
responseWriter.Header().Add("Allow", "PUT")
responseWriter.Header().Add("Allow", "GET")
http.Error(responseWriter, "405 Method Not Allowed, try GET or PUT", http.StatusMethodNotAllowed)
}
case "/ping":
if request.Method == "GET" {
fmt.Fprint(responseWriter, "pong")
} else {
responseWriter.Header().Set("Allow", "GET")
http.Error(responseWriter, "405 method not allowed, try GET", http.StatusMethodNotAllowed)
}
default:
s.ControlHandler.ServeHTTP(responseWriter, request)
}
}