mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
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:
parent
4cf5d29692
commit
ec5c24b9b8
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
66
src/mod/sshprox/sshprox_test.go
Normal file
66
src/mod/sshprox/sshprox_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user