package main
import (
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"regexp"
"strings"
errors "git.sequentialread.com/forest/pkg-errors"
)
type ThresholdTunnelsConfig struct {
Listeners [ ] ThresholdTunnel
ServiceToLocalAddrMap map [ string ] string
}
type CaddyApp struct {
//http app
HttpPort int ` json:"http_port,omitempty" `
HttpsPort int ` json:"https_port,omitempty" `
Servers map [ string ] * CaddyServer ` json:"servers,omitempty" `
//tls app
Automation * CaddyTLSAutomation ` json:"automation,omitempty" `
}
type CaddyAutomaticHTTPS struct {
Disable bool ` json:"disable" `
DisableRedirects bool ` json:"disable_redirects" `
}
type CaddyTLSAutomation struct {
Policies [ ] CaddyTLSPolicy ` json:"policies,omitempty" `
}
type CaddyTLSPolicy struct {
Subjects [ ] string ` json:"subjects,omitempty" `
Issuers [ ] CaddyACMEIssuer ` json:"issuers,omitempty" `
}
type CaddyACMEIssuer struct {
CA string ` json:"ca" `
Email string ` json:"email" `
Module string ` json:"module" `
}
type CaddyServer struct {
AutomaticHTTPS * CaddyAutomaticHTTPS ` json:"automatic_https,omitempty" `
Listen [ ] string ` json:"listen" `
ListenerWrappers [ ] CaddyListenerWrapper ` json:"listener_wrappers,omitempty" `
Routes [ ] CaddyRoute ` json:"routes" `
Logs * CaddyServerLogs ` json:"logs,omitempty" `
}
type CaddyListenerWrapper struct {
Wrapper string ` json:"wrapper" `
Timeout string ` json:"timeout,omitempty" `
}
type CaddyServerLogs struct {
LoggerNames map [ string ] string ` json:"logger_names" `
}
type CaddyRoute struct {
Handle [ ] CaddyHandler ` json:"handle,omitempty" `
Match [ ] CaddyMatch ` json:"match,omitempty" `
Terminal bool ` json:"terminal,omitempty" `
}
// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/
type CaddyHandler struct {
Handler string ` json:"handler" `
//CaddySubrouteHandler
Routes [ ] CaddyRoute ` json:"routes,omitempty" `
//CaddyReverseProxyHandler
Upstreams [ ] CaddyUpstream ` json:"upstreams,omitempty" `
//CaddyStaticResponseHandler
Headers map [ string ] [ ] string ` json:"headers,omitempty" `
StatusCode int ` json:"status_code,omitempty" `
//CaddyFileServerHandler
Root string ` json:"root,omitempty" `
Passthrough bool ` json:"pass_thru,omitempty" `
Browse * CaddyFileServerBrowse ` json:"browse,omitempty" `
}
type CaddyFileServerBrowse struct {
TemplateFile string ` json:"template_file,omitempty" `
}
// https://caddyserver.com/docs/json/apps/http/servers/routes/handle/reverse_proxy/
type CaddyUpstream struct {
Dial string ` json:"dial" `
}
// https://caddyserver.com/docs/json/apps/http/servers/routes/match/
type CaddyMatch struct {
// match host
Host [ ] string ` json:"host,omitempty" `
// match vars regexp
VarsRegexp map [ string ] CaddyVarsRegexp ` json:"vars_regexp,omitempty" `
// match host (sni) for layer4 plugin
TLS * CaddyMatchTLS ` json:"tls,omitempty" `
}
type CaddyMatchTLS struct {
SNI [ ] string ` json:"sni,omitempty" `
}
type CaddyVarsRegexp struct {
Name string ` json:"name,omitempty" `
Pattern string ` json:"pattern,omitempty" `
}
type ConfigService struct {
ClientId string
ThresholdAdminBaseURL string
CaddyAdminBaseURL string
ThresholdClient * http . Client
CaddyAdminClient * http . Client
EmailAddress string
UseUnixSockets bool
TelemetryID string
}
// TODO make these configurable?
const caddyHTTPPort = 9575
const caddyHTTPSPort = 9576
const caddyTLSPort = 9577
const caddyHTTPSocketFile = "/var/run/greenhouse-daemon-caddy-http.sock"
const caddyHTTPSSocketFile = "/var/run/greenhouse-daemon-caddy-https.sock"
const caddyTLSSocketFile = "/var/run/greenhouse-daemon-caddy-tls.sock"
const caddyACMEIssuerURL = "https://acme-v02.api.letsencrypt.org/directory"
func ( service * ConfigService ) PrepareConfigs ( tunnels [ ] GUITunnel ) ( * ThresholdTunnelsConfig , * map [ string ] * CaddyApp , error ) {
sourceURLs := map [ string ] int { }
tunnelsWithInvalidSubdomains := [ ] string { }
// TODO: support wildcard subdomains "^([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)|\\*$"
// Right now wildcard subdomains are blocked on Caddy requiring the dns acme challenge for them.
subdomainRegex := regexp . MustCompile ( "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$" )
for _ , tunnel := range tunnels {
sourceURL := tunnel . GetStringDisplay ( true )
sourceURLs [ sourceURL ] ++
if tunnel . HasSubdomain && ! subdomainRegex . MatchString ( tunnel . Subdomain ) {
tunnelsWithInvalidSubdomains = append ( tunnelsWithInvalidSubdomains , sourceURL )
}
}
duplicatedSources := [ ] string { }
for k , v := range sourceURLs {
if v > 1 {
duplicatedSources = append ( duplicatedSources , k )
}
}
errorStrings := [ ] string { }
if len ( duplicatedSources ) > 0 {
errorStrings = append ( errorStrings , fmt . Sprintf ( "multiple tunnels exist with the URL(s) %s" , strings . Join ( duplicatedSources , ", " ) ) )
}
if len ( tunnelsWithInvalidSubdomains ) > 0 {
errorStrings = append ( errorStrings , fmt . Sprintf ( "the tunnel(s) %s have invalid subdomains" , strings . Join ( tunnelsWithInvalidSubdomains , ", " ) ) )
}
if len ( errorStrings ) > 0 {
return nil , nil , errors . New ( strings . Join ( errorStrings , ", \n" ) )
}
thresholdConfig := ThresholdTunnelsConfig {
Listeners : [ ] ThresholdTunnel { } ,
ServiceToLocalAddrMap : map [ string ] string { } ,
}
allHostnamesMap := map [ string ] bool { }
for _ , tunnel := range tunnels {
allHostnamesMap [ service . getPrototypeListenerConfig ( tunnel ) . ListenHostnameGlob ] = true
}
allHostnames := make ( [ ] string , len ( allHostnamesMap ) )
i := 0
for k := range allHostnamesMap {
allHostnames [ i ] = k
i ++
}
// TODO advanced option to allow listening on 0.0.0.0?
caddyTLSListen := fmt . Sprintf ( "127.0.0.1:%d" , caddyTLSPort )
caddyHTTPSListen := fmt . Sprintf ( "127.0.0.1:%d" , caddyHTTPSPort )
caddyHTTPListen := fmt . Sprintf ( "127.0.0.1:%d" , caddyHTTPPort )
if service . UseUnixSockets {
caddyTLSListen = fmt . Sprintf ( "unix//%s" , caddyTLSSocketFile )
caddyHTTPSListen = fmt . Sprintf ( "unix//%s" , caddyHTTPSSocketFile )
caddyHTTPListen = fmt . Sprintf ( "unix//%s" , caddyHTTPSocketFile )
}
caddyConfig := map [ string ] * CaddyApp {
"layer4" : {
Servers : map [ string ] * CaddyServer {
"layer4_server_0" : {
Listen : [ ] string { caddyTLSListen } ,
Routes : [ ] CaddyRoute { } ,
} ,
} ,
} ,
"http" : {
Servers : map [ string ] * CaddyServer {
"https_server" : {
AutomaticHTTPS : & CaddyAutomaticHTTPS {
DisableRedirects : true ,
} ,
Listen : [ ] string { caddyHTTPSListen } ,
ListenerWrappers : [ ] CaddyListenerWrapper {
{
Wrapper : "proxy_protocol" ,
Timeout : "5s" ,
} ,
{
Wrapper : "tls" ,
} ,
} ,
// Logs: {
// LoggerNames: map[string]string{
// "*": "greenhouse-daemon-http",
// },
// },
Routes : [ ] CaddyRoute { service . getFbclidRemovalRoute ( ) } ,
} ,
"http_redirector" : {
AutomaticHTTPS : & CaddyAutomaticHTTPS {
Disable : true ,
} ,
Listen : [ ] string { caddyHTTPListen } ,
// ListenerWrappers: []CaddyListenerWrapper{
// {
// Wrapper: "proxy_protocol",
// Timeout: "5s",
// },
// {
// Wrapper: "tls",
// },
// },
// // TODO disable logs for the redirector
// Logs: &CaddyServerLogs{
// LoggerNames: map[string]string{
// "*": "greenhouse-daemon-http",
// },
// },
Routes : [ ] CaddyRoute { service . getHTTPSRedirectRoute ( ) } ,
} ,
} ,
} ,
"tls" : {
Automation : & CaddyTLSAutomation {
Policies : [ ] CaddyTLSPolicy {
{
Subjects : allHostnames ,
Issuers : [ ] CaddyACMEIssuer {
{
CA : caddyACMEIssuerURL ,
Email : service . EmailAddress ,
Module : "acme" ,
} ,
} ,
} ,
} ,
} ,
} ,
}
if ! service . UseUnixSockets {
caddyConfig [ "http" ] . HttpPort = caddyHTTPPort
caddyConfig [ "http" ] . HttpsPort = caddyHTTPSPort
}
jsonBytes , _ := json . MarshalIndent ( tunnels , "" , " " )
log . Printf ( "tunnels: %s\n\n" , string ( jsonBytes ) )
for _ , tunnel := range tunnels {
if tunnel . DestinationType == "folder" && tunnel . Protocol != "https" {
return nil , nil , errors . Errorf ( "the 'folder' destination type is limted to https tunnels" )
}
destination := fmt . Sprintf ( "127.0.0.1:%d" , tunnel . DestinationPort )
if tunnel . DestinationType == "host_port" {
destination = fmt . Sprintf ( "%s:%d" , tunnel . DestinationHostname , tunnel . DestinationPort )
}
if tunnel . Protocol == "https" {
tunnelListener := service . getPrototypeListenerConfig ( tunnel )
tunnelListener . ListenPort = 80
tunnelListener . BackEndService = fmt . Sprintf ( "%s_80" , tunnel . GetServiceId ( ) )
//It's not required because all http will be redirected before it is used.
//We probably won't log http requests, just https ones
//tunnelListener.HaProxyProxyProtocol = true
thresholdConfig . Listeners = append ( thresholdConfig . Listeners , tunnelListener )
thresholdConfig . ServiceToLocalAddrMap [ tunnelListener . BackEndService ] = caddyHTTPListen
tunnelListener = service . getPrototypeListenerConfig ( tunnel )
tunnelListener . ListenPort = 443
tunnelListener . BackEndService = fmt . Sprintf ( "%s_443" , tunnel . GetServiceId ( ) )
tunnelListener . HaProxyProxyProtocol = true
thresholdConfig . Listeners = append ( thresholdConfig . Listeners , tunnelListener )
thresholdConfig . ServiceToLocalAddrMap [ tunnelListener . BackEndService ] = caddyHTTPSListen
if tunnel . DestinationType == "folder" {
caddyConfig [ "http" ] . Servers [ "https_server" ] . Routes = append (
caddyConfig [ "http" ] . Servers [ "https_server" ] . Routes ,
CaddyRoute {
Handle : [ ] CaddyHandler {
{
Handler : "file_server" ,
Root : tunnel . DestinationFolderPath ,
Passthrough : false ,
Browse : & CaddyFileServerBrowse { } ,
} ,
} ,
Match : [ ] CaddyMatch {
{
Host : [ ] string { tunnelListener . ListenHostnameGlob } ,
} ,
} ,
Terminal : true ,
} ,
)
} else {
caddyConfig [ "http" ] . Servers [ "https_server" ] . Routes = append (
caddyConfig [ "http" ] . Servers [ "https_server" ] . Routes ,
CaddyRoute {
Handle : [ ] CaddyHandler {
{
// I believe this handler sets X-Real-IP and X-Forwarded-For automatically :)
Handler : "reverse_proxy" ,
Upstreams : [ ] CaddyUpstream {
{
Dial : destination ,
} ,
} ,
} ,
} ,
Match : [ ] CaddyMatch {
{
Host : [ ] string { tunnelListener . ListenHostnameGlob } ,
} ,
} ,
Terminal : true ,
} ,
)
}
}
if tunnel . Protocol == "tls" {
tunnelListener := service . getPrototypeListenerConfig ( tunnel )
thresholdConfig . Listeners = append ( thresholdConfig . Listeners , tunnelListener )
thresholdConfig . ServiceToLocalAddrMap [ tunnelListener . BackEndService ] = caddyTLSListen
// TODO it should be possible to both read and write proxy protocol here...
// 1. add an option to caddy-layer4 to read proxy protocol by wrapping the TCP listener
// 2. caddy-layer4 terminate TLS by wrapping the TCP listener (caddy-layer4 already does this)
// 3. forward proxy protocol to the backend (caddy-layer4 already supports this)
// However, this is more of an edge case -- probably not needed by 99% of users.
// Biggest usecase would probably be email, but not really cuz email has to go via TCP anyways due to STARTTLS
caddyConfig [ "layer4" ] . Servers [ "layer4_server_0" ] . Routes = append (
caddyConfig [ "layer4" ] . Servers [ "layer4_server_0" ] . Routes ,
CaddyRoute {
Handle : [ ] CaddyHandler {
{
Handler : "tls" ,
} ,
{
Handler : "proxy" ,
Upstreams : [ ] CaddyUpstream {
{
Dial : destination ,
} ,
} ,
} ,
} ,
Match : [ ] CaddyMatch {
{
TLS : & CaddyMatchTLS {
SNI : [ ] string { tunnelListener . ListenHostnameGlob } ,
} ,
} ,
} ,
} ,
)
}
if tunnel . Protocol == "tcp" {
tunnelListener := service . getPrototypeListenerConfig ( tunnel )
tunnelListener . ListenHostnameGlob = ""
thresholdConfig . Listeners = append ( thresholdConfig . Listeners , tunnelListener )
// TODO user specifying PROXY protocol should be supported here.
thresholdConfig . ServiceToLocalAddrMap [ tunnelListener . BackEndService ] = destination
// no caddy config, TCP goes direct from threshold to the destination service
}
}
return & thresholdConfig , & caddyConfig , nil
}
func ( service * ConfigService ) ConfigureThreshold ( thresholdConfig * ThresholdTunnelsConfig ) error {
url := fmt . Sprintf ( "%s/liveconfig" , service . ThresholdAdminBaseURL )
jsonBytes , err := json . Marshal ( thresholdConfig )
if err != nil {
return errors . Wrapf ( err , "error serializing threshold json for ConfigService.Configure" , url )
}
request , err := http . NewRequest ( "PUT" , url , bytes . NewReader ( jsonBytes ) )
if err != nil {
return errors . Wrapf ( err , "error constructing http request to threshold (%s) for ConfigService.Configure" , url )
}
response , err := service . ThresholdClient . Do ( request )
if err != nil {
return errors . Wrapf ( err , "error sending http request to threshold (%s) for ConfigService.Configure" , url )
}
bodyBytes , err := ioutil . ReadAll ( response . Body )
if err != nil {
return errors . Wrapf ( err , "http read error on request to threshold (%s) for ConfigService.Configure" , url )
}
if response . StatusCode != 200 {
return errors . Errorf (
"ConfigService.Configure: got HTTP %d %s from threshold (%s): %s" ,
response . StatusCode , response . Status , url , string ( bodyBytes ) ,
)
}
return nil
}
func ( service * ConfigService ) TestThreshold ( thresholdConfig * ThresholdTunnelsConfig ) error {
url := fmt . Sprintf ( "%s/start_test" , service . ThresholdAdminBaseURL )
response , err := service . ThresholdClient . Get ( url )
if err != nil {
return err
}
if response . StatusCode != http . StatusOK {
responseContent := "http read error when trying to read error message"
responseBytes , err := ioutil . ReadAll ( response . Body )
if err == nil {
responseContent = string ( responseBytes )
}
return errors . Errorf ( "calling /start_test on the threshold client returned HTTP %d %s: %s" , response . StatusCode , response . Status , responseContent )
}
// TODO get thresholdServerAddress from the threshold admin api
thresholdServerAddress := ""
sentTokens := map [ string ] string { }
listenerConfigsByToken := map [ string ] ThresholdTunnel { }
for _ , listenerConfig := range thresholdConfig . Listeners {
// only test the tunnels belonging to this threshold client.
if listenerConfig . ClientId != service . ClientId {
continue
}
// bytez, _ := json.MarshalIndent(listenerConfig, "", " ")
// log.Printf("TestThreshold(): listenerConfig = '%s'", string(bytez))
tokenBuffer := make ( [ ] byte , 8 )
rand . Read ( tokenBuffer )
token := fmt . Sprintf ( "%x" , tokenBuffer )
sentTokens [ token ] = "sent"
listenerConfigsByToken [ token ] = listenerConfig
if listenerConfig . ListenHostnameGlob != "" && listenerConfig . ListenHostnameGlob != "*" {
hostnames := strings . Split ( listenerConfig . ListenHostnameGlob , "," )
for _ , hostname := range hostnames {
specificHostname := strings . ReplaceAll ( hostname , "*" , "a" )
if listenerConfig . ListenPort != 80 {
remoteAddress := fmt . Sprintf ( "%s:%d" , specificHostname , listenerConfig . ListenPort )
//log.Printf("TestThreshold(): remoteAddress = '%s'", remoteAddress)
connection , err := tls . Dial ( "tcp" , remoteAddress , & tls . Config { InsecureSkipVerify : true } )
if err != nil {
log . Printf ( "TestThreshold(): connecting to threshold server '%s' returned %s" , remoteAddress , err )
return err
}
connection . Write ( [ ] byte ( token ) )
responseBuffer := make ( [ ] byte , 16 )
_ , err = connection . Read ( responseBuffer )
if err != nil && err != io . EOF {
log . Printf ( "TestThreshold(): threshold server '%s' read error %s" , remoteAddress , err )
return err
}
if string ( responseBuffer ) != token {
return errors . Errorf ( "sent test token '%s' to tls://%s, recieved '%s' in response" , token , remoteAddress , string ( responseBuffer ) )
}
} else {
url := fmt . Sprintf ( fmt . Sprintf ( "http://%s/%s" , specificHostname , token ) )
response , err := http . Get ( url )
if err != nil {
return err
}
if response . StatusCode != http . StatusOK {
return errors . Errorf ( "sent test token '%s' to %s, recieved HTTP %d %s" , token , url , response . StatusCode , response . Status )
}
responseBytes , err := ioutil . ReadAll ( response . Body )
if err != nil {
return err
}
if string ( responseBytes ) != token {
return errors . Errorf ( "sent test token '%s' to %s, recieved '%s' in response" , token , url , string ( responseBytes ) )
}
}
}
} else {
tcpAddressString := fmt . Sprintf ( "%s:%d" , thresholdServerAddress , listenerConfig . ListenPort )
tcpAddress , err := net . ResolveTCPAddr ( "tcp" , tcpAddressString )
if err != nil {
return err
}
connection , err := net . DialTCP ( "tcp" , nil , tcpAddress )
if err != nil {
return err
}
connection . Write ( [ ] byte ( token ) )
responseBuffer := make ( [ ] byte , 16 )
_ , err = connection . Read ( responseBuffer )
if err != nil && err != io . EOF {
return err
}
if string ( responseBuffer ) != token {
return errors . Errorf ( "sent test token '%s' to tcp://%s, recieved '%s' in response" , token , tcpAddressString , string ( responseBuffer ) )
}
}
}
url = fmt . Sprintf ( "%s/end_test" , service . ThresholdAdminBaseURL )
response , err = service . ThresholdClient . Get ( url )
if err != nil {
return err
}
responseBytes , err := ioutil . ReadAll ( response . Body )
if err != nil {
return errors . Wrap ( err , "trying to read /end_test response" )
}
if response . StatusCode != http . StatusOK {
return errors . Errorf ( "calling /start_test on the threshold client returned HTTP %d %s: %s" , response . StatusCode , response . Status , string ( responseBytes ) )
}
lines := strings . Split ( string ( responseBytes ) , "\n" )
for _ , line := range lines {
if len ( strings . TrimSpace ( line ) ) > 0 {
state , hasState := sentTokens [ line ]
if ! hasState {
jsonBytes , _ := json . MarshalIndent ( listenerConfigsByToken , "" , " " )
return errors . Errorf ( "malformed or incorrect token '%s' was returned by threshold test. configured test tokens were: %s " , line , string ( jsonBytes ) )
}
if state != "sent" {
return errors . Errorf ( "token '%s' (%s:%d) was returned by threshold test more than once" , line , listenerConfigsByToken [ line ] . ListenHostnameGlob , listenerConfigsByToken [ line ] . ListenPort )
}
sentTokens [ line ] = "recieved"
}
}
for token , result := range sentTokens {
if result != "recieved" {
return errors . Errorf ( "token '%s' (%s:%d) was never returned by threshold test" , token , listenerConfigsByToken [ token ] . ListenHostnameGlob , listenerConfigsByToken [ token ] . ListenPort )
}
}
return nil
}
func ( service * ConfigService ) ConfigureCaddy ( caddyConfig * map [ string ] * CaddyApp ) error {
url := fmt . Sprintf ( "%s/config/apps" , service . CaddyAdminBaseURL )
jsonBytes , err := json . Marshal ( caddyConfig )
if err != nil {
return errors . Wrapf ( err , "error serializing caddy json for ConfigService.Configure" , url )
}
log . Printf ( "Caddy Config: %s\n\n" , string ( jsonBytes ) )
request , err := http . NewRequest ( "POST" , url , bytes . NewReader ( jsonBytes ) )
if err != nil {
return errors . Wrapf ( err , "error constructing http request to caddy (%s) for ConfigService.Configure" , url )
}
request . Header . Add ( "Content-Type" , "application/json" )
// TODO if/when this needs to happen... ???
// caddy may fail to accept the new config if it cannot create these files. they must not already exist... sometimes??
// however if we delete them every time, it also doesn't seem to work; they wont always be re-created.
//
// if service.UseUnixSockets {
// os.Remove(caddyHTTPSocketFile)
// os.Remove(caddyHTTPSSocketFile)
// os.Remove(caddyTLSSocketFile)
// }
response , err := service . CaddyAdminClient . Do ( request )
if err != nil {
return errors . Wrapf ( err , "error sending http request to caddy (%s) for ConfigService.Configure" , url )
}
bodyBytes , err := ioutil . ReadAll ( response . Body )
if err != nil {
return errors . Wrapf ( err , "http read error on request to caddy (%s) for ConfigService.Configure" , url )
}
if response . StatusCode != 200 {
return errors . Errorf (
"ConfigService.Configure: got HTTP %d %s from caddy (%s): %s" ,
response . StatusCode , response . Status , url , string ( bodyBytes ) ,
)
}
return nil
}
func ( service * ConfigService ) EnsureCaddyACMECompletes ( thresholdConfig * ThresholdTunnelsConfig ) error {
// TODO poll for certs present in:
// root@thingpad:/opt/greenhouse-daemon/caddyData/caddy/certificates# ls acme-v02.api.letsencrypt.org-directory
// TODO test threshold tunnels before setting up caddy.
return nil
}
func ( service * ConfigService ) TestFinalTunnels ( tunnels [ ] GUITunnel ) error {
// TODO test each tunnel
return nil
}
func ( service * ConfigService ) getPrototypeListenerConfig ( tunnel GUITunnel ) ThresholdTunnel {
domain := tunnel . Domain
if tunnel . HasSubdomain {
domain = fmt . Sprintf ( "%s.%s" , tunnel . Subdomain , domain )
}
return ThresholdTunnel {
ListenAddress : "0.0.0.0" ,
ListenHostnameGlob : domain ,
ListenPort : tunnel . PublicPort ,
BackEndService : tunnel . GetServiceId ( ) ,
ClientId : service . ClientId ,
}
}
func ( service * ConfigService ) getHTTPSRedirectRoute ( ) CaddyRoute {
// because we want to be able to listen on unix sockets we have to do the http and https servers separately,
// which means we have to do our own https redirect
return CaddyRoute {
Handle : [ ] CaddyHandler {
{
Handler : "static_response" ,
Headers : map [ string ] [ ] string {
"Location" : { "https://{http.request.host}{http.request.uri}" } ,
} ,
StatusCode : 301 ,
} ,
} ,
}
}
func ( service * ConfigService ) getFbclidRemovalRoute ( ) CaddyRoute {
// facebook adds this evil ?fbclid=xyz request parameter whenever someone clicks a link
// this handler will match all requests that have this parameter
// and it will redirect to the same URI with the parameter removed
return CaddyRoute {
Handle : [ ] CaddyHandler {
{
Handler : "static_response" ,
Headers : map [ string ] [ ] string {
"Location" : { "{http.regexp.fbclid_regex.1}" } ,
} ,
StatusCode : 302 ,
} ,
} ,
Match : [ ] CaddyMatch {
{
VarsRegexp : map [ string ] CaddyVarsRegexp {
"{http.request.uri}" : {
Name : "fbclid_regex" ,
Pattern : "^(.*?)([?&]fbclid=[a-zA-Z0-9_-]+)$" ,
} ,
} ,
} ,
} ,
}
}