Updates 2.6.3

+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse #7
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
This commit is contained in:
Toby Chui 2023-06-08 21:42:03 +08:00
parent 5db50c1ca2
commit 5e7599756f
27 changed files with 391 additions and 194 deletions

View File

@ -38,7 +38,7 @@ var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local no
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var (
name = "Zoraxy"
version = "2.6.2"
version = "2.6.3"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()

View File

@ -278,6 +278,12 @@ func addXForwardedForHeader(req *http.Request) {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
}
}

View File

@ -57,6 +57,7 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
// Handle subdomain request
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
@ -116,6 +117,7 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
r.URL, _ = url.Parse(rewriteURL)
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket")

View File

@ -22,6 +22,7 @@ type ProxyHandler struct {
}
type RouterOption struct {
HostUUID string
Port int
UseTls bool
ForceHttpsRedirect bool

View File

@ -0,0 +1,91 @@
package geodb
import "strings"
/*
Blacklist.go
This script store the blacklist related functions
*/
//Geo Blacklist
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
//IP Blacklsits
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}

View File

@ -3,8 +3,8 @@ package geodb
import (
_ "embed"
"log"
"net"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/database"
)
@ -112,170 +112,6 @@ func (s *Store) Close() {
}
/*
Country code based black / white list
*/
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
func (s *Store) GetAllWhitelistedCountryCode() []string {
whitelistedCountryCode := []string{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedCountryCode = append(whitelistedCountryCode, ip)
}
return whitelistedCountryCode
}
/*
IP based black / whitelist
*/
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) AddIPToWhiteList(ipAddr string) {
s.sysdb.Write("whitelist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("whitelist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) GetAllWhitelistedIp() []string {
whitelistedIp := []string{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip)
}
return whitelistedIp
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
@ -341,6 +177,23 @@ func (s *Store) IsWhitelisted(ipAddr string) bool {
return false
}
// A helper function that check both blacklist and whitelist for access
// for both geoIP and ip / CIDR ranges
func (s *Store) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
ipAddr := GetRequesterIP(r)
if ipAddr == "" {

View File

@ -0,0 +1,91 @@
package geodb
import "strings"
/*
Whitelist.go
This script handles whitelist related functions
*/
//Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
}
func (s *Store) GetAllWhitelistedCountryCode() []string {
whitelistedCountryCode := []string{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedCountryCode = append(whitelistedCountryCode, ip)
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string) {
s.sysdb.Write("whitelist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isWhitelisted bool = false
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
if isWhitelisted {
return true
}
//Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIp()
for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, whitelistRules)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllWhitelistedIp() []string {
whitelistedIp := []string{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip)
}
return whitelistedIp
}

16
src/mod/sshprox/embed.go Normal file
View File

@ -0,0 +1,16 @@
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64)
// +build windows,amd64 linux,mipsle linux,riscv64
package sshprox
import "embed"
/*
Bianry embedding
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && 386
// +build linux,386
package sshprox
import "embed"
/*
Bianry embedding for i386 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_386
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && amd64
// +build linux,amd64
package sshprox
import "embed"
/*
Bianry embedding for AMD64 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_amd64
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && arm
// +build linux,arm
package sshprox
import "embed"
/*
Bianry embedding for ARM(v6/7) builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_arm
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && arm64
// +build linux,arm64
package sshprox
import "embed"
/*
Bianry embedding for ARM64 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_arm64
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -1,7 +1,6 @@
package sshprox
import (
"embed"
"errors"
"fmt"
"log"
@ -28,16 +27,6 @@ import (
online ssh terminal
*/
/*
Bianry embedding
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/*
gotty embed.FS
)
type Manager struct {
StartingPort int
Instances []*Instance

View File

@ -58,11 +58,23 @@ func forward(conn1 net.Conn, conn2 net.Conn, aTob *int64, bToa *int64) {
wg.Wait()
}
func accept(listener net.Listener) (net.Conn, error) {
func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
conn, err := listener.Accept()
if err != nil {
return nil, err
}
//Check if connection in blacklist or whitelist
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if !c.parent.Options.AccessControlHandler(conn) {
time.Sleep(300 * time.Millisecond)
conn.Close()
log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
}
}
log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
return conn, err
}
@ -203,7 +215,7 @@ func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan b
}()
for {
conn1, err := accept(listen1)
conn1, err := c.accept(listen1)
if err != nil {
if !c.Running {
return nil
@ -211,7 +223,7 @@ func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan b
continue
}
conn2, err := accept(listen2)
conn2, err := c.accept(listen2)
if err != nil {
if !c.Running {
return nil
@ -224,7 +236,7 @@ func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan b
time.Sleep(time.Duration(c.Timeout) * time.Second)
continue
}
forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
go forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
}
}
@ -248,7 +260,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
//Start blocking loop for accepting connections
for {
conn, err := accept(server)
conn, err := c.accept(server)
if conn == nil || err != nil {
if !c.Running {
//Terminate by stop chan. Exit listener loop
@ -322,7 +334,7 @@ func (c *ProxyRelayConfig) Host2host(address1, address2 string, stopChan chan bo
return nil
}
}
forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
go forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
}
return nil

View File

@ -2,6 +2,7 @@ package tcpprox
import (
"errors"
"net"
uuid "github.com/satori/go.uuid"
"imuslab.com/zoraxy/mod/database"
@ -40,11 +41,14 @@ type ProxyRelayConfig struct {
stopChan chan bool //Stop channel to stop the listener
aTobAccumulatedByteTransfer int64 //Accumulated byte transfer from A to B
bToaAccumulatedByteTransfer int64 //Accumulated byte transfer from B to A
parent *Manager `json:"-"`
}
type Options struct {
Database *database.Database
DefaultTimeout int
Database *database.Database
DefaultTimeout int
AccessControlHandler func(net.Conn) bool
}
type Manager struct {
@ -59,16 +63,34 @@ type Manager struct {
func NewTCProxy(options *Options) *Manager {
options.Database.NewTable("tcprox")
//Load relay configs from db
previousRules := []*ProxyRelayConfig{}
if options.Database.KeyExists("tcprox", "rules") {
options.Database.Read("tcprox", "rules", &previousRules)
}
return &Manager{
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
if options.AccessControlHandler == nil {
options.AccessControlHandler = func(conn net.Conn) bool {
//Always allow access
return true
}
}
//Create a new proxy manager for TCP
thisManager := Manager{
Options: options,
Configs: previousRules,
Connections: 0,
}
//Inject manager into the rules
for _, rule := range previousRules {
rule.parent = &thisManager
}
thisManager.Configs = previousRules
return &thisManager
}
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
@ -85,6 +107,8 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
stopChan: nil,
aTobAccumulatedByteTransfer: 0,
bToaAccumulatedByteTransfer: 0,
parent: m,
}
m.Configs = append(m.Configs, &thisConfig)
m.SaveConfigToDatabase()

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
@ -220,7 +221,24 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
func getWebsiteStatus(url string) (int, error) {
resp, err := http.Get(url)
if err != nil {
return 0, err
//Try replace the http with https and vise versa
rewriteURL := ""
if strings.Contains(url, "https://") {
rewriteURL = strings.ReplaceAll(url, "https://", "http://")
} else if strings.Contains(url, "http://") {
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
}
resp, err = http.Get(rewriteURL)
if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online
//return SSL handshake failed
return 525, nil
}
return 0, err
}
}
status_code := resp.StatusCode
resp.Body.Close()

View File

@ -47,6 +47,7 @@ func ReverseProxtInit() {
}
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID,
Port: inboundPort,
UseTls: useTls,
ForceHttpsRedirect: forceHttpsRedirect,

View File

@ -163,7 +163,8 @@ func startupSequence() {
//Create TCP Proxy Manager
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
Database: sysdb,
Database: sysdb,
AccessControlHandler: geodbStore.AllowConnectionAccess,
})
//Create WoL MAC storage table

View File

@ -109,7 +109,13 @@
}
ontimeRate++;
}else{
dotType = "offline";
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
//Special type of error, cause by downstream reverse proxy
dotType = "error";
}else{
dotType = "offline";
}
}
let datetime = format_time(thisStatus.Timestamp);
@ -126,12 +132,20 @@
//Check of online status now
let currentOnlineStatus = "Unknown";
let onlineStatusCss = ``;
let reminderEle = ``;
if (value[value.length - 1].Online){
currentOnlineStatus = `<i class="circle icon"></i> Online`;
onlineStatusCss = `color: #3bd671;`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
if (value[value.length - 1].StatusCode >= 500 && value[value.length - 1].StatusCode < 600){
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`;
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
}
}
//Generate the html
@ -151,6 +165,7 @@
<div class="status" style="marign-top: 1em;">
${statusDotList}
</div>
${reminderEle}
<div class="ui divider"></div>
</div>`);
}

BIN
src/web/img/public/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

BIN
src/web/img/public/bg2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

View File

@ -23,7 +23,7 @@
width: 100%;
opacity: 0.8;
z-index: -99;
background-image: url("img/public/bg.png");
background-image: url("img/public/bg.jpg");
background-size: auto 100%;
background-position: right top;
background-repeat: no-repeat;

View File

@ -23,7 +23,7 @@
width: 100%;
opacity: 0.8;
z-index: -99;
background-image: url("img/public/bg2.png");
background-image: url("img/public/bg2.jpg");
background-size: auto 100%;
background-position: right top;
background-repeat: no-repeat;

View File

@ -95,6 +95,10 @@
loadDateRange();
function isValidDateFormat(dateString) {
if (dateString.indexOf("_") >= 0){
//Replace all the _ to -
dateString = dateString.split("_").join("-");
}
// Create a regular expression pattern for the yyyy-mm-dd format
const pattern = /^\d{4}-\d{2}-\d{2}$/;

View File

@ -117,6 +117,7 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
url = "https://" + target.Domain
protocol = "https"
}
UptimeTargets = append(UptimeTargets, &uptime.Target{
ID: subd,
Name: subd,