4 Commits

Author SHA1 Message Date
878664e0ac update: multi version build
All checks were successful
renovate / renovate (push) Successful in 35s
Docker Build and Push / build-and-push-branches (push) Successful in 6m7s
Docker Build and Push / build-and-push-tags (push) Successful in 6m6s
2025-12-31 13:48:36 +07:00
20a88df330 update: multi version build
All checks were successful
Docker Build and Push / build-and-push-tags (push) Has been skipped
renovate / renovate (push) Successful in 38s
Docker Build and Push / build-and-push-branches (push) Successful in 4m45s
2025-12-31 13:32:16 +07:00
075dd7ecad feat: add versioning system
Some checks failed
renovate / renovate (push) Successful in 38s
Docker Build and Push / build-and-push-branches (push) Has been skipped
Docker Build and Push / build-and-push-tags (push) Has been cancelled
2025-12-31 12:31:31 +07:00
ab34b34765 fix: prevent subdomain change to already-in-use subdomains
All checks were successful
renovate / renovate (push) Successful in 35s
Docker Build and Push / build-and-push (push) Successful in 5m42s
2025-12-30 19:41:33 +07:00
6 changed files with 308 additions and 8 deletions

View File

@@ -5,6 +5,8 @@ on:
branches: branches:
- main - main
- staging - staging
tags:
- 'v*'
paths: paths:
- '**.go' - '**.go'
- 'go.mod' - 'go.mod'
@@ -15,8 +17,9 @@ on:
- '.gitea/workflows/build.yml' - '.gitea/workflows/build.yml'
jobs: jobs:
build-and-push: build-and-push-branches:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref_type == 'branch'
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -32,6 +35,17 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set version variables
id: vars
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "VERSION=dev-main" >> $GITHUB_OUTPUT
else
echo "VERSION=dev-staging" >> $GITHUB_OUTPUT
fi
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "COMMIT=${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Build and push Docker image for main - name: Build and push Docker image for main
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -40,6 +54,10 @@ jobs:
tags: | tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:latest git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.vars.outputs.VERSION }}
BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }}
COMMIT=${{ steps.vars.outputs.COMMIT }}
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
- name: Build and push Docker image for staging - name: Build and push Docker image for staging
@@ -50,4 +68,85 @@ jobs:
tags: | tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:staging git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:staging
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.vars.outputs.VERSION }}
BUILD_DATE=${{ steps.vars.outputs.BUILD_DATE }}
COMMIT=${{ steps.vars.outputs.COMMIT }}
if: github.ref == 'refs/heads/staging' if: github.ref == 'refs/heads/staging'
build-and-push-tags:
runs-on: ubuntu-latest
if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: git.fossy.my.id
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract version and determine release type
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "COMMIT=${{ github.sha }}" >> $GITHUB_OUTPUT
if echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
echo "MAJOR=$MAJOR" >> $GITHUB_OUTPUT
echo "MINOR=$MINOR" >> $GITHUB_OUTPUT
if echo "$VERSION" | grep -q '-'; then
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
echo "ADDITIONAL_TAG=staging" >> $GITHUB_OUTPUT
else
echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT
echo "ADDITIONAL_TAG=latest" >> $GITHUB_OUTPUT
fi
else
echo "Invalid version format: $VERSION"
exit 1
fi
- name: Build and push Docker image for release
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}.${{ steps.version.outputs.MINOR }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.MAJOR }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.version.outputs.VERSION }}
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
COMMIT=${{ steps.version.outputs.COMMIT }}
if: steps.version.outputs.IS_PRERELEASE == 'false'
- name: Build and push Docker image for pre-release
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:v${{ steps.version.outputs.VERSION }}
git.fossy.my.id/${{ secrets.DOCKER_USERNAME }}/tunnel-please:${{ steps.version.outputs.ADDITIONAL_TAG }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.version.outputs.VERSION }}
BUILD_DATE=${{ steps.version.outputs.BUILD_DATE }}
COMMIT=${{ steps.version.outputs.COMMIT }}
if: steps.version.outputs.IS_PRERELEASE == 'true'

View File

@@ -1,5 +1,9 @@
FROM golang:1.25.5-alpine AS go_builder FROM golang:1.25.5-alpine AS go_builder
ARG VERSION=dev
ARG BUILD_DATE=unknown
ARG COMMIT=unknown
RUN apk update && apk upgrade && \ RUN apk update && apk upgrade && \
apk add --no-cache ca-certificates tzdata git && \ apk add --no-cache ca-certificates tzdata git && \
update-ca-certificates update-ca-certificates
@@ -18,7 +22,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux \ CGO_ENABLED=0 GOOS=linux \
go build -trimpath \ go build -trimpath \
-ldflags="-w -s" \ -ldflags="-w -s -X tunnel_pls/version.Version=${VERSION} -X tunnel_pls/version.BuildDate=${BUILD_DATE} -X tunnel_pls/version.Commit=${COMMIT}" \
-o /app/tunnel_pls \ -o /app/tunnel_pls \
. .
@@ -28,6 +32,10 @@ RUN adduser -D -u 10001 -g '' appuser && \
FROM scratch FROM scratch
ARG VERSION=dev
ARG BUILD_DATE=unknown
ARG COMMIT=unknown
COPY --from=go_builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=go_builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=go_builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=go_builder /etc/passwd /etc/passwd COPY --from=go_builder /etc/passwd /etc/passwd
@@ -43,6 +51,9 @@ ENV TZ=Asia/Jakarta
EXPOSE 2200 8080 8443 EXPOSE 2200 8080 8443
LABEL org.opencontainers.image.title="Tunnel Please" \ LABEL org.opencontainers.image.title="Tunnel Please" \
org.opencontainers.image.description="SSH-based tunnel server" org.opencontainers.image.description="SSH-based tunnel server" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${COMMIT}" \
org.opencontainers.image.created="${BUILD_DATE}"
ENTRYPOINT ["/app/tunnel_pls"] ENTRYPOINT ["/app/tunnel_pls"]

10
main.go
View File

@@ -8,14 +8,22 @@ import (
"os" "os"
"tunnel_pls/server" "tunnel_pls/server"
"tunnel_pls/utils" "tunnel_pls/utils"
"tunnel_pls/version"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func main() { func main() {
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
fmt.Println(version.GetVersion())
os.Exit(0)
}
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("Starting %s", version.GetVersion())
pprofEnabled := utils.Getenv("PPROF_ENABLED", "false") pprofEnabled := utils.Getenv("PPROF_ENABLED", "false")
if pprofEnabled == "true" { if pprofEnabled == "true" {
pprofPort := utils.Getenv("PPROF_PORT", "6060") pprofPort := utils.Getenv("PPROF_PORT", "6060")
@@ -30,7 +38,7 @@ func main() {
sshConfig := &ssh.ServerConfig{ sshConfig := &ssh.ServerConfig{
NoClientAuth: true, NoClientAuth: true,
ServerVersion: "SSH-2.0-TunnlPls-1.0", ServerVersion: fmt.Sprintf("SSH-2.0-TunnlPls-%s", version.GetShortVersion()),
} }
sshKeyPath := "certs/ssh/id_rsa" sshKeyPath := "certs/ssh/id_rsa"

View File

@@ -22,6 +22,131 @@ const (
paddingRight = 4 paddingRight = 4
) )
var forbiddenSlugs = []string{ var forbiddenSlugs = map[string]struct{}{
"ping", "ping": {},
"staging": {},
"admin": {},
"root": {},
"api": {},
"www": {},
"support": {},
"help": {},
"status": {},
"health": {},
"login": {},
"logout": {},
"signup": {},
"register": {},
"settings": {},
"config": {},
"null": {},
"undefined": {},
"example": {},
"test": {},
"dev": {},
"system": {},
"administrator": {},
"dashboard": {},
"account": {},
"profile": {},
"user": {},
"users": {},
"auth": {},
"oauth": {},
"callback": {},
"webhook": {},
"webhooks": {},
"static": {},
"assets": {},
"cdn": {},
"mail": {},
"email": {},
"ftp": {},
"ssh": {},
"git": {},
"svn": {},
"blog": {},
"news": {},
"about": {},
"contact": {},
"terms": {},
"privacy": {},
"legal": {},
"billing": {},
"payment": {},
"checkout": {},
"cart": {},
"shop": {},
"store": {},
"download": {},
"uploads": {},
"images": {},
"img": {},
"css": {},
"js": {},
"fonts": {},
"public": {},
"private": {},
"internal": {},
"external": {},
"proxy": {},
"cache": {},
"debug": {},
"metrics": {},
"monitoring": {},
"graphql": {},
"rest": {},
"rpc": {},
"socket": {},
"ws": {},
"wss": {},
"app": {},
"apps": {},
"mobile": {},
"desktop": {},
"embed": {},
"widget": {},
"docs": {},
"documentation": {},
"wiki": {},
"forum": {},
"community": {},
"feedback": {},
"report": {},
"abuse": {},
"spam": {},
"security": {},
"verify": {},
"confirm": {},
"reset": {},
"password": {},
"recovery": {},
"unsubscribe": {},
"subscribe": {},
"notifications": {},
"alerts": {},
"messages": {},
"inbox": {},
"outbox": {},
"sent": {},
"draft": {},
"trash": {},
"archive": {},
"search": {},
"explore": {},
"discover": {},
"trending": {},
"popular": {},
"featured": {},
"new": {},
"latest": {},
"top": {},
"best": {},
"hot": {},
"random": {},
"all": {},
"any": {},
"none": {},
"true": {},
"false": {},
} }

View File

@@ -205,7 +205,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(tea.ClearScreen, textinput.Blink) return m, tea.Batch(tea.ClearScreen, textinput.Blink)
case "enter": case "enter":
inputValue := m.slugInput.Value() inputValue := m.slugInput.Value()
m.interaction.updateClientSlug(m.interaction.slugManager.Get(), inputValue)
if isForbiddenSlug(inputValue) {
m.slugError = "This subdomain is reserved. Please choose a different one."
return m, nil
} else if !isValidSlug(inputValue) {
m.slugError = "Invalid subdomain. Follow the rules."
return m, nil
}
if !m.interaction.updateClientSlug(m.interaction.slugManager.Get(), inputValue) {
m.slugError = "Someone already uses this subdomain."
return m, nil
}
m.tunnelURL = buildURL(m.protocol, inputValue, m.domain) m.tunnelURL = buildURL(m.protocol, inputValue, m.domain)
m.editingSlug = false m.editingSlug = false
m.slugError = "" m.slugError = ""
@@ -800,3 +813,30 @@ func buildURL(protocol, subdomain, domain string) string {
func generateRandomSubdomain() string { func generateRandomSubdomain() string {
return utils.GenerateRandomString(20) return utils.GenerateRandomString(20)
} }
func isValidSlug(slug string) bool {
if len(slug) < minSlugLength || len(slug) > maxSlugLength {
return false
}
if slug[0] == '-' || slug[len(slug)-1] == '-' {
return false
}
for _, c := range slug {
if !isValidSlugChar(byte(c)) {
return false
}
}
return true
}
func isValidSlugChar(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'
}
func isForbiddenSlug(slug string) bool {
_, ok := forbiddenSlugs[slug]
return ok
}

17
version/version.go Normal file
View File

@@ -0,0 +1,17 @@
package version
import "fmt"
var (
Version = "dev"
BuildDate = "unknown"
Commit = "unknown"
)
func GetVersion() string {
return fmt.Sprintf("tunnel_pls %s (commit: %s, built: %s)", Version, Commit, BuildDate)
}
func GetShortVersion() string {
return Version
}