mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-28 18:31: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
|
||||
|
||||
+ 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) {
|
||||
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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user