Added more darktheme

- Added more dark theme css
- Merged main branch fixes and new features
- Added todo tag for custom timeout
This commit is contained in:
Toby Chui 2024-11-14 21:18:05 +08:00
parent 4cf5d29692
commit ec5c24b9b8
9 changed files with 276 additions and 82 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -101,7 +101,7 @@
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
<button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
</div>
</div>
<div class="ui divider"></div>

View File

@ -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;
}

View File

@ -2,7 +2,7 @@
<html>
<head>
<!-- Notes: This should be open in its original path-->
<meta charset="utf-8">
<meta charset="utf-8" />
<link rel="stylesheet" href="../script/semantic/semantic.min.css" />
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
@ -10,6 +10,19 @@
<body>
<br />
<div class="ui container">
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="showUnexposed" class="hidden" />
<label for="showUnexposed"
>Show Containers with Unexposed Ports
<br />
<small
>Please make sure Zoraxy and the target container share a
network</small
>
</label>
</div>
</div>
<div class="ui header">
<div class="content">
List of Docker Containers
@ -33,56 +46,70 @@
</div>
<script>
const lines = {};
const linesAded = {};
let lines = {};
let linesAdded = {};
document
.getElementById("showUnexposed")
.addEventListener("change", () => {
console.log("showUnexposed", $("#showUnexposed").is(":checked"));
$("#containersList").html('<div class="ui loader active"></div>');
$("#containersAddedList").empty();
$("#containersAddedListHeader").attr("hidden", true);
lines = {};
linesAdded = {};
getDockerContainers();
});
function getDockerContainers() {
const hostRequest = $.get("/api/proxy/list?type=host");
const dockerRequest = $.get("/api/docker/containers");
// Wait for both requests to complete
Promise.all([hostRequest, dockerRequest])
.then(([hostData, dockerData]) => {
if (
dockerData.error === undefined &&
hostData.error === undefined
) {
if (!dockerData.error && !hostData.error) {
const { containers, network } = dockerData;
const bridge = network.find(({ Name }) => Name === "bridge");
const {
IPAM: {
Config: [{ Gateway: gateway }],
},
} = bridge;
const existedDomains = hostData.reduce((acc, { ActiveOrigins }) => {
return acc.concat(ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain));
}, []);
const existingTargets = new Set(
hostData.flatMap(({ ActiveOrigins }) =>
ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
)
);
for (const container of containers) {
const {
Ports,
Names: [name],
} = container;
const Ports = container.Ports;
const name = container.Names[0].replace(/^\//, "");
for (const portObject of Ports.filter(
({ IP: ip }) => ip === "::" || ip === '0.0.0.0'
)) {
const { IP: ip, PublicPort: port } = portObject;
for (const portObject of Ports) {
let port = portObject.PublicPort;
if (!port) {
if (!$("#showUnexposed").is(":checked")) {
continue;
}
port = portObject.PrivatePort;
}
const key = `${name}-${port}`;
// if port is not exposed, use container's name and let docker handle the routing
// BUT this will only work if the container is on the same network as Zoraxy
const targetAddress = portObject.IP || name;
if (
existedDomains.some((item) => item === `${gateway}:${port}`) &&
!linesAded[key]
existingTargets.has(`${targetAddress}:${port}`) &&
!linesAdded[key]
) {
linesAded[key] = {
name: name.replace(/^\//, ""),
ip: gateway,
linesAdded[key] = {
name,
ip: targetAddress,
port,
};
} else if (!lines[key]) {
lines[key] = {
name: name.replace(/^\//, ""),
ip: gateway,
name,
ip: targetAddress,
port,
};
}
@ -92,29 +119,31 @@
for (const [key, line] of Object.entries(lines)) {
$("#containersList").append(
`<div class="item">
<div class="right floated content">
<div class="ui button" onclick="addContainerItem('${key}');">Add</div>
</div>
<div class="content">
<div class="header">${line.name}</div>
<div class="description">
${line.ip}:${line.port}
</div>
</div>`
<div class="right floated content">
<div class="ui button" onclick="addContainerItem('${key}');">Add</div>
</div>
<div class="content">
<div class="header">${line.name}</div>
<div class="description">
${line.ip}:${line.port}
</div>
</div>`
);
}
for (const [key, line] of Object.entries(linesAded)) {
for (const [key, line] of Object.entries(linesAdded)) {
$("#containersAddedList").append(
`<div class="item">
<div class="content">
<div class="header">${line.name}</div>
<div class="description">
${line.ip}:${line.port}
</div>
</div>`
<div class="content">
<div class="header">${line.name}</div>
<div class="description">
${line.ip}:${line.port}
</div>
</div>`
);
}
Object.entries(linesAded).length &&
Object.entries(linesAdded).length &&
$("#containersAddedListHeader").removeAttr("hidden");
$("#containersList .loader").removeClass("active");
} else {
@ -122,7 +151,11 @@
`Error loading data: ${dockerData.error || hostData.error}`,
false
);
$("#containersList").html(`<div class="ui basic segment"><i class="ui red times icon"></i> ${dockerData.error || hostData.error}</div>`);
$("#containersList").html(
`<div class="ui basic segment"><i class="ui red times icon"></i> ${
dockerData.error || hostData.error
}</div>`
);
}
})
.catch((error) => {

View File

@ -42,7 +42,7 @@ func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) {
if !*allowSshLoopback {
//Not allow loopback connections
if strings.EqualFold(strings.TrimSpace(ipaddr), "localhost") || strings.TrimSpace(ipaddr) == "127.0.0.1" {
if sshprox.IsLoopbackIPOrDomain(ipaddr) {
//Request target is loopback
utils.SendErrorResponse(w, "loopback web ssh connection is not enabled on this host")
return
@ -74,7 +74,7 @@ func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js))
}
//Check if the host support ssh, or if the target domain (and port, optional) support ssh
// Check if the host support ssh, or if the target domain (and port, optional) support ssh
func HandleWebSshSupportCheck(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {