mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-29 02:41:45 +02:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
2e9bc77a5d | |||
ed178d857a | |||
e79a70b7ac | |||
779115d06b | |||
9cb315ea67 | |||
43ba00ec8d |
43
.github/workflows/docker.yml
vendored
Normal file
43
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: Build and push Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup-build-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Setup building file structure
|
||||||
|
run: |
|
||||||
|
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./docker
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
zoraxydocker/zoraxy:latest
|
||||||
|
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
40
.github/workflows/main.yml
vendored
40
.github/workflows/main.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
name: Image Publisher
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
setup-build-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.release.tag_name }}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker & GHCR
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
|
||||||
|
|
||||||
- name: Setup building file structure
|
|
||||||
run: |
|
|
||||||
cp -r $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
|
||||||
|
|
||||||
- name: Build the image
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/docker/
|
|
||||||
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
|
|
||||||
|
|
||||||
docker buildx build --push \
|
|
||||||
--provenance=false \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
|
||||||
--tag zoraxydocker/zoraxy:latest \
|
|
||||||
.
|
|
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,3 +1,19 @@
|
|||||||
|
# v3.1.2 03 Nov 2024
|
||||||
|
|
||||||
|
+ Added auto start port 80 listener on acme certificate generator
|
||||||
|
+ Added polling interval and propagation timeout option in ACME module [#300](https://github.com/tobychui/zoraxy/issues/300)
|
||||||
|
+ Added support for custom header variables [#318](https://github.com/tobychui/zoraxy/issues/318)
|
||||||
|
+ Added support for X-Remote-User
|
||||||
|
+ Added port scanner [#342](https://github.com/tobychui/zoraxy/issues/342)
|
||||||
|
+ Optimized code base for stream proxy and config file storage [#320](https://github.com/tobychui/zoraxy/issues/320)
|
||||||
|
+ Removed sorting on cert list
|
||||||
|
+ Fixed request certificate button bug
|
||||||
|
+ Fixed cert auto renew logic [#316](https://github.com/tobychui/zoraxy/issues/316)
|
||||||
|
+ Fixed unable to remove new stream proxy bug
|
||||||
|
+ Fixed many other minor bugs [#328](https://github.com/tobychui/zoraxy/issues/328) [#297](https://github.com/tobychui/zoraxy/issues/297)
|
||||||
|
+ Added more code to SSO system (disabled in release)
|
||||||
|
|
||||||
|
|
||||||
# v3.1.1. 09 Sep 2024
|
# v3.1.1. 09 Sep 2024
|
||||||
|
|
||||||
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
|
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
|
||||||
|
@ -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) {
|
func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) {
|
||||||
targetInstance, err := m.GetInstanceById(instanceId)
|
targetInstance, err := m.GetInstanceById(instanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -168,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp
|
|||||||
if username != "" {
|
if username != "" {
|
||||||
connAddr = username + "@" + remoteIpAddr
|
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")
|
configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
|
||||||
title := username + "@" + remoteIpAddr
|
title := username + "@" + remoteIpAddr
|
||||||
if remotePort != 22 {
|
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
|
package sshprox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -34,6 +36,21 @@ func IsWebSSHSupported() bool {
|
|||||||
return true
|
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
|
// Check if a given domain and port is a valid ssh server
|
||||||
func IsSSHConnectable(ipOrDomain string, port int) bool {
|
func IsSSHConnectable(ipOrDomain string, port int) bool {
|
||||||
timeout := time.Second * 3
|
timeout := time.Second * 3
|
||||||
@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool {
|
|||||||
return string(buf[:7]) == "SSH-2.0"
|
return string(buf[:7]) == "SSH-2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the port is used by other process or application
|
// Validate the username and remote address to prevent injection
|
||||||
func isPortInUse(port int) bool {
|
func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error {
|
||||||
address := fmt.Sprintf(":%d", port)
|
// Validate and sanitize the username to prevent ssh injection
|
||||||
listener, err := net.Listen("tcp", address)
|
validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||||
if err != nil {
|
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
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if !*allowSshLoopback {
|
if !*allowSshLoopback {
|
||||||
//Not allow loopback connections
|
//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
|
//Request target is loopback
|
||||||
utils.SendErrorResponse(w, "loopback web ssh connection is not enabled on this host")
|
utils.SendErrorResponse(w, "loopback web ssh connection is not enabled on this host")
|
||||||
return
|
return
|
||||||
@ -74,7 +74,7 @@ func HandleCreateProxySession(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendJSONResponse(w, string(js))
|
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) {
|
func HandleWebSshSupportCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
domain, err := utils.PostPara(r, "domain")
|
domain, err := utils.PostPara(r, "domain")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Reference in New Issue
Block a user