diff --git a/src/mod/acme/autorenew.go b/src/mod/acme/autorenew.go index 33a5fc1..77ccae3 100644 --- a/src/mod/acme/autorenew.go +++ b/src/mod/acme/autorenew.go @@ -308,7 +308,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) { } if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) { //This cert is expired - DNSName, err := ExtractDomains(certBytes) if err != nil { //Maybe self signed. Ignore this diff --git a/src/mod/dynamicproxy/dpcore/dpcore.go b/src/mod/dynamicproxy/dpcore/dpcore.go index 862f3da..5ae55bc 100644 --- a/src/mod/dynamicproxy/dpcore/dpcore.go +++ b/src/mod/dynamicproxy/dpcore/dpcore.go @@ -109,6 +109,8 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2 thisTransporter.(*http.Transport).DisableCompression = true + //TODO: Add user adjustable timeout option here + if dpcOptions.IgnoreTLSVerification { //Ignore TLS certificate validation error thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true diff --git a/src/mod/sshprox/sshprox.go b/src/mod/sshprox/sshprox.go index ed1b92c..113cb45 100644 --- a/src/mod/sshprox/sshprox.go +++ b/src/mod/sshprox/sshprox.go @@ -50,21 +50,6 @@ func NewSSHProxyManager() *Manager { } } -// Get the next free port in the list -func (m *Manager) GetNextPort() int { - nextPort := m.StartingPort - occupiedPort := make(map[int]bool) - for _, instance := range m.Instances { - occupiedPort[instance.AssignedPort] = true - } - for { - if !occupiedPort[nextPort] { - return nextPort - } - nextPort++ - } -} - func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) { targetInstance, err := m.GetInstanceById(instanceId) if err != nil { @@ -168,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp if username != "" { connAddr = username + "@" + remoteIpAddr } + + //Trim the space in the username and remote address + username = strings.TrimSpace(username) + remoteIpAddr = strings.TrimSpace(remoteIpAddr) + + //Validate the username and remote address + err := ValidateUsernameAndRemoteAddr(username, remoteIpAddr) + if err != nil { + return err + } + configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty") title := username + "@" + remoteIpAddr if remotePort != 22 { diff --git a/src/mod/sshprox/sshprox_test.go b/src/mod/sshprox/sshprox_test.go new file mode 100644 index 0000000..36a9ab5 --- /dev/null +++ b/src/mod/sshprox/sshprox_test.go @@ -0,0 +1,66 @@ +package sshprox + +import ( + "testing" +) + +func TestInstance_Destroy(t *testing.T) { + manager := NewSSHProxyManager() + instance, err := manager.NewSSHProxy("/tmp") + if err != nil { + t.Fatalf("Failed to create new SSH proxy: %v", err) + } + + instance.Destroy() + + if len(manager.Instances) != 0 { + t.Errorf("Expected Instances to be empty, got %d", len(manager.Instances)) + } +} + +func TestInstance_ValidateUsernameAndRemoteAddr(t *testing.T) { + tests := []struct { + username string + remoteAddr string + expectError bool + }{ + {"validuser", "127.0.0.1", false}, + {"valid.user", "example.com", false}, + {"; bash ;", "example.com", true}, + {"valid-user", "example.com", false}, + {"invalid user", "127.0.0.1", true}, + {"validuser", "invalid address", true}, + {"invalid@user", "127.0.0.1", true}, + {"validuser", "invalid@address", true}, + {"injection; rm -rf /", "127.0.0.1", true}, + {"validuser", "127.0.0.1; rm -rf /", true}, + {"$(reboot)", "127.0.0.1", true}, + {"validuser", "$(reboot)", true}, + {"validuser", "127.0.0.1; $(reboot)", true}, + {"validuser", "127.0.0.1 | ls", true}, + {"validuser", "127.0.0.1 & ls", true}, + {"validuser", "127.0.0.1 && ls", true}, + {"validuser", "127.0.0.1 |& ls", true}, + {"validuser", "127.0.0.1 ; ls", true}, + {"validuser", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false}, + {"validuser", "2001:db8::ff00:42:8329", false}, + {"validuser", "2001:db8:0:1234:0:567:8:1", false}, + {"validuser", "2001:db8::1234:0:567:8:1", false}, + {"validuser", "2001:db8:0:0:0:0:2:1", false}, + {"validuser", "2001:db8::2:1", false}, + {"validuser", "2001:db8:0:0:8:800:200c:417a", false}, + {"validuser", "2001:db8::8:800:200c:417a", false}, + {"validuser", "2001:db8:0:0:8:800:200c:417a; rm -rf /", true}, + {"validuser", "2001:db8::8:800:200c:417a; rm -rf /", true}, + } + + for _, test := range tests { + err := ValidateUsernameAndRemoteAddr(test.username, test.remoteAddr) + if test.expectError && err == nil { + t.Errorf("Expected error for username %s and remoteAddr %s, but got none", test.username, test.remoteAddr) + } + if !test.expectError && err != nil { + t.Errorf("Did not expect error for username %s and remoteAddr %s, but got %v", test.username, test.remoteAddr, err) + } + } +} diff --git a/src/mod/sshprox/utils.go b/src/mod/sshprox/utils.go index 082c8d9..0e4d271 100644 --- a/src/mod/sshprox/utils.go +++ b/src/mod/sshprox/utils.go @@ -1,9 +1,11 @@ package sshprox import ( + "errors" "fmt" "net" "net/url" + "regexp" "runtime" "strings" "time" @@ -34,6 +36,21 @@ func IsWebSSHSupported() bool { return true } +// Get the next free port in the list +func (m *Manager) GetNextPort() int { + nextPort := m.StartingPort + occupiedPort := make(map[int]bool) + for _, instance := range m.Instances { + occupiedPort[instance.AssignedPort] = true + } + for { + if !occupiedPort[nextPort] { + return nextPort + } + nextPort++ + } +} + // Check if a given domain and port is a valid ssh server func IsSSHConnectable(ipOrDomain string, port int) bool { timeout := time.Second * 3 @@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool { return string(buf[:7]) == "SSH-2.0" } -// Check if the port is used by other process or application -func isPortInUse(port int) bool { - address := fmt.Sprintf(":%d", port) - listener, err := net.Listen("tcp", address) - if err != nil { +// Validate the username and remote address to prevent injection +func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error { + // Validate and sanitize the username to prevent ssh injection + validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + if !validUsername.MatchString(username) { + return errors.New("invalid username, only alphanumeric characters, dots, underscores and dashes are allowed") + } + + //Check if the remoteIpAddr is a valid ipv4 or ipv6 address + if net.ParseIP(remoteIpAddr) != nil { + //A valid IP address do not need further validation + return nil + } + + // Validate and sanitize the remote domain to prevent injection + validRemoteAddr := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + if !validRemoteAddr.MatchString(remoteIpAddr) { + return errors.New("invalid remote address, only alphanumeric characters, dots, underscores and dashes are allowed") + } + + return nil +} + +// Check if the given ip or domain is a loopback address +// or resolves to a loopback address +func IsLoopbackIPOrDomain(ipOrDomain string) bool { + if strings.EqualFold(strings.TrimSpace(ipOrDomain), "localhost") || strings.TrimSpace(ipOrDomain) == "127.0.0.1" { return true } - listener.Close() + + //Check if the ipOrDomain resolves to a loopback address + ips, err := net.LookupIP(ipOrDomain) + if err != nil { + return false + } + + for _, ip := range ips { + if ip.IsLoopback() { + return true + } + } + return false } diff --git a/src/web/components/cert.html b/src/web/components/cert.html index 64c2ac5..bb35c34 100644 --- a/src/web/components/cert.html +++ b/src/web/components/cert.html @@ -101,7 +101,7 @@
Upload Default Keypairs
diff --git a/src/web/darktheme.css b/src/web/darktheme.css index 8260122..eac711b 100644 --- a/src/web/darktheme.css +++ b/src/web/darktheme.css @@ -144,11 +144,7 @@ body.darkTheme .ui.toggle.checkbox input:checked ~ label::before{ background-color: var(--theme_bg) !important; color: var(--text_color) !important; border: 1px solid var(--divider_color) !important; -} - -.toobar #mainmenu a.item:hover{ - background-color: var(--theme_highlight) !important; -} +}s body.darkTheme .ui.segment:not(.basic) { background-color: var(--theme_bg) !important; @@ -156,6 +152,12 @@ body.darkTheme .ui.segment:not(.basic) { border: 1px solid transparent !important; } +body.darkTheme .ui.segment{ + background-color: transparent !important; + color: var(--text_color) !important; + border: 1px solid transparent !important; +} + body.darkTheme .sub.header { color: var(--text_color) !important; } @@ -565,7 +567,7 @@ body.darkTheme .ui.selection.fluid.dropdown#accessRuleSelector .dropdown.icon { /* Tab Menu in access control */ body.darkTheme .ui.top.attached.tabular.menu { - background-color: var(--theme_bg) !important; + background-color: transparent !important; color: var(--text_color) !important; } @@ -579,7 +581,7 @@ body.darkTheme .ui.top.attached.tabular.menu .item:hover { } body.darkTheme .ui.top.attached.tabular.menu .item.active { - background-color: var(--theme_bg) !important; + background-color: var(--theme_bg_primary) !important; color: var(--text_color) !important; } @@ -706,3 +708,48 @@ body.darkTheme .ui.selection.dropdown#defaultCA .dropdown.icon { color: var(--text_color) !important; } +/* + +ZeroTier + +*/ + +body.darkTheme #gan .ui.list .item .icon { + color: var(--icon_color) !important; +} + +body.darkTheme #gan .ui.list .item .content .header { + color: var(--text_color) !important; +} + +body.darkTheme #gan .ui.list .item .content .description { + color: var(--text_color_secondary) !important; +} + +body.darkTheme #gan .clickable.iprange.active { + background-color: var(--theme_highlight) !important; +} + +body.darkTheme #gan thead th { + background-color: var(--theme_bg_secondary) !important; + color: var(--text_color) !important; + border-color: var(--divider_color) !important; +} + + +/* + + Uptime Monitor + +*/ + +body.darkTheme #utm .standardContainer { + background-color: var(--theme_bg) !important; + color: var(--text_color) !important; + border: 1px solid var(--divider_color) !important; +} + +body.darkTheme #utm .standardContainer .padding.statusDot { + background-color: var(--theme_bg) !important; + border: 0.2px solid var(--theme_bg_primary) !important; +} \ No newline at end of file diff --git a/src/web/snippet/dockerContainersList.html b/src/web/snippet/dockerContainersList.html index c71c09a..d032968 100644 --- a/src/web/snippet/dockerContainersList.html +++ b/src/web/snippet/dockerContainersList.html @@ -2,7 +2,7 @@ - + @@ -10,6 +10,19 @@