Compare commits
6 Commits
fbf182025b
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 878664e0ac | |||
| 20a88df330 | |||
| 075dd7ecad | |||
| ab34b34765 | |||
| 514c4f9de1 | |||
| d8330c684f |
@@ -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'
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -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
10
main.go
@@ -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"
|
||||||
|
|||||||
@@ -19,7 +19,28 @@ var blockedReservedPorts = []uint16{1080, 1433, 1521, 1900, 2049, 3306, 3389, 54
|
|||||||
func (s *SSHSession) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) {
|
func (s *SSHSession) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) {
|
||||||
for req := range GlobalRequest {
|
for req := range GlobalRequest {
|
||||||
switch req.Type {
|
switch req.Type {
|
||||||
case "shell", "pty-req", "window-change":
|
case "shell", "pty-req":
|
||||||
|
err := req.Reply(true, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "window-change":
|
||||||
|
p := req.Payload
|
||||||
|
if len(p) < 16 {
|
||||||
|
log.Println("invalid window-change payload")
|
||||||
|
err := req.Reply(false, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to reply to request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cols := binary.BigEndian.Uint32(p[0:4])
|
||||||
|
rows := binary.BigEndian.Uint32(p[4:8])
|
||||||
|
|
||||||
|
s.interaction.SetWH(int(cols), int(rows))
|
||||||
|
|
||||||
err := req.Reply(true, nil)
|
err := req.Reply(true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to reply to request:", err)
|
log.Println("Failed to reply to request:", err)
|
||||||
@@ -104,7 +125,7 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
|||||||
if portToBind == 80 || portToBind == 443 {
|
if portToBind == 80 || portToBind == 443 {
|
||||||
s.HandleHTTPForward(req, portToBind)
|
s.HandleHTTPForward(req, portToBind)
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
if portToBind == 0 {
|
if portToBind == 0 {
|
||||||
unassign, success := portUtil.Default.GetUnassignedPort()
|
unassign, success := portUtil.Default.GetUnassignedPort()
|
||||||
portToBind = unassign
|
portToBind = unassign
|
||||||
@@ -134,12 +155,12 @@ func (s *SSHSession) HandleTCPIPForward(req *ssh.Request) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := portUtil.Default.SetPortStatus(portToBind, true)
|
err = portUtil.Default.SetPortStatus(portToBind, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to set port status:", err)
|
log.Println("Failed to set port status:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
s.HandleTCPForward(req, addr, portToBind)
|
s.HandleTCPForward(req, addr, portToBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,12 +196,6 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
|||||||
}
|
}
|
||||||
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
log.Printf("HTTP forwarding approved on port: %d", portToBind)
|
||||||
|
|
||||||
domain := utils.Getenv("DOMAIN", "localhost")
|
|
||||||
protocol := "http"
|
|
||||||
if utils.Getenv("TLS_ENABLED", "false") == "true" {
|
|
||||||
protocol = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
err = req.Reply(true, buf.Bytes())
|
err = req.Reply(true, buf.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to reply to request:", err)
|
log.Println("Failed to reply to request:", err)
|
||||||
@@ -195,7 +210,6 @@ func (s *SSHSession) HandleHTTPForward(req *ssh.Request, portToBind uint16) {
|
|||||||
s.forwarder.SetType(types.HTTP)
|
s.forwarder.SetType(types.HTTP)
|
||||||
s.forwarder.SetForwardedPort(portToBind)
|
s.forwarder.SetForwardedPort(portToBind)
|
||||||
s.slugManager.Set(slug)
|
s.slugManager.Set(slug)
|
||||||
log.Printf("HTTP tunnel established: %s://%s.%s", protocol, slug, domain)
|
|
||||||
s.lifecycle.SetStatus(types.RUNNING)
|
s.lifecycle.SetStatus(types.RUNNING)
|
||||||
s.interaction.Start()
|
s.interaction.Start()
|
||||||
}
|
}
|
||||||
@@ -253,7 +267,6 @@ func (s *SSHSession) HandleTCPForward(req *ssh.Request, addr string, portToBind
|
|||||||
s.forwarder.SetType(types.TCP)
|
s.forwarder.SetType(types.TCP)
|
||||||
s.forwarder.SetListener(listener)
|
s.forwarder.SetListener(listener)
|
||||||
s.forwarder.SetForwardedPort(portToBind)
|
s.forwarder.SetForwardedPort(portToBind)
|
||||||
log.Printf("TCP tunnel established: tcp://%s:%d", utils.Getenv("DOMAIN", "localhost"), s.forwarder.GetForwardedPort())
|
|
||||||
s.lifecycle.SetStatus(types.RUNNING)
|
s.lifecycle.SetStatus(types.RUNNING)
|
||||||
go s.forwarder.AcceptTCPConnections()
|
go s.forwarder.AcceptTCPConnections()
|
||||||
s.interaction.Start()
|
s.interaction.Start()
|
||||||
|
|||||||
@@ -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": {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Controller interface {
|
|||||||
SetLifecycle(lifecycle Lifecycle)
|
SetLifecycle(lifecycle Lifecycle)
|
||||||
SetSlugModificator(func(oldSlug, newSlug string) bool)
|
SetSlugModificator(func(oldSlug, newSlug string) bool)
|
||||||
Start()
|
Start()
|
||||||
|
SetWH(w, h int)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Forwarder interface {
|
type Forwarder interface {
|
||||||
@@ -48,6 +49,15 @@ type Interaction struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Interaction) SetWH(w, h int) {
|
||||||
|
if i.program != nil {
|
||||||
|
i.program.Send(tea.WindowSizeMsg{
|
||||||
|
Width: w,
|
||||||
|
Height: h,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type commandItem struct {
|
type commandItem struct {
|
||||||
name string
|
name string
|
||||||
desc string
|
desc string
|
||||||
@@ -69,6 +79,8 @@ type model struct {
|
|||||||
slugInput textinput.Model
|
slugInput textinput.Model
|
||||||
slugError string
|
slugError string
|
||||||
interaction *Interaction
|
interaction *Interaction
|
||||||
|
width int
|
||||||
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
type keymap struct {
|
type keymap struct {
|
||||||
@@ -115,6 +127,31 @@ func (i *Interaction) Stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getResponsiveWidth(screenWidth, padding, minWidth, maxWidth int) int {
|
||||||
|
width := screenWidth - padding
|
||||||
|
if width > maxWidth {
|
||||||
|
width = maxWidth
|
||||||
|
}
|
||||||
|
if width < minWidth {
|
||||||
|
width = minWidth
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUseCompactLayout(width int, threshold int) bool {
|
||||||
|
return width < threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(s string, maxLength int) string {
|
||||||
|
if len(s) <= maxLength {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if maxLength < 4 {
|
||||||
|
return s[:maxLength]
|
||||||
|
}
|
||||||
|
return s[:maxLength-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
func (i commandItem) FilterValue() string { return i.name }
|
func (i commandItem) FilterValue() string { return i.name }
|
||||||
func (i commandItem) Title() string { return i.name }
|
func (i commandItem) Title() string { return i.name }
|
||||||
func (i commandItem) Description() string { return i.desc }
|
func (i commandItem) Description() string { return i.desc }
|
||||||
@@ -138,8 +175,16 @@ 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 tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
m.commandList.SetWidth(msg.Width)
|
m.commandList.SetWidth(msg.Width)
|
||||||
m.commandList.SetHeight(msg.Height - 4)
|
m.commandList.SetHeight(msg.Height - 4)
|
||||||
|
|
||||||
|
if msg.Width < 80 {
|
||||||
|
m.slugInput.Width = msg.Width - 10
|
||||||
|
} else {
|
||||||
|
m.slugInput.Width = 50
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.QuitMsg:
|
case tea.QuitMsg:
|
||||||
@@ -160,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 = ""
|
||||||
@@ -240,21 +298,35 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.showingComingSoon {
|
if m.showingComingSoon {
|
||||||
|
isCompact := shouldUseCompactLayout(m.width, 60)
|
||||||
|
|
||||||
|
var boxPadding int
|
||||||
|
var boxMargin int
|
||||||
|
if isCompact {
|
||||||
|
boxPadding = 1
|
||||||
|
boxMargin = 1
|
||||||
|
} else {
|
||||||
|
boxPadding = 3
|
||||||
|
boxMargin = 2
|
||||||
|
}
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
PaddingTop(1).
|
PaddingTop(1).
|
||||||
PaddingBottom(1)
|
PaddingBottom(1)
|
||||||
|
|
||||||
|
messageBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
||||||
messageBoxStyle := lipgloss.NewStyle().
|
messageBoxStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FAFAFA")).
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
Background(lipgloss.Color("#1A1A2E")).
|
Background(lipgloss.Color("#1A1A2E")).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
Padding(1, 3).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(2).
|
MarginBottom(boxMargin).
|
||||||
|
Width(messageBoxWidth).
|
||||||
Align(lipgloss.Center)
|
Align(lipgloss.Center)
|
||||||
|
|
||||||
helpStyle := lipgloss.NewStyle().
|
helpStyle := lipgloss.NewStyle().
|
||||||
@@ -264,16 +336,53 @@ func (m model) View() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(titleStyle.Render("⏳ Coming Soon"))
|
|
||||||
|
var title string
|
||||||
|
if shouldUseCompactLayout(m.width, 40) {
|
||||||
|
title = "Coming Soon"
|
||||||
|
} else {
|
||||||
|
title = "⏳ Coming Soon"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(title))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(messageBoxStyle.Render("🚀 This feature is coming very soon!\n Stay tuned for updates."))
|
|
||||||
|
var message string
|
||||||
|
if shouldUseCompactLayout(m.width, 50) {
|
||||||
|
message = "Coming soon!\nStay tuned."
|
||||||
|
} else {
|
||||||
|
message = "🚀 This feature is coming very soon!\n Stay tuned for updates."
|
||||||
|
}
|
||||||
|
b.WriteString(messageBoxStyle.Render(message))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(helpStyle.Render("This message will disappear in 5 seconds or press any key..."))
|
|
||||||
|
var helpText string
|
||||||
|
if shouldUseCompactLayout(m.width, 60) {
|
||||||
|
helpText = "Press any key..."
|
||||||
|
} else {
|
||||||
|
helpText = "This message will disappear in 5 seconds or press any key..."
|
||||||
|
}
|
||||||
|
b.WriteString(helpStyle.Render(helpText))
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.editingSlug {
|
if m.editingSlug {
|
||||||
|
isCompact := shouldUseCompactLayout(m.width, 70)
|
||||||
|
isVeryCompact := shouldUseCompactLayout(m.width, 50)
|
||||||
|
|
||||||
|
var boxPadding int
|
||||||
|
var boxMargin int
|
||||||
|
if isVeryCompact {
|
||||||
|
boxPadding = 1
|
||||||
|
boxMargin = 1
|
||||||
|
} else if isCompact {
|
||||||
|
boxPadding = 1
|
||||||
|
boxMargin = 1
|
||||||
|
} else {
|
||||||
|
boxPadding = 2
|
||||||
|
boxMargin = 2
|
||||||
|
}
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
@@ -287,9 +396,9 @@ func (m model) View() string {
|
|||||||
inputBoxStyle := lipgloss.NewStyle().
|
inputBoxStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
Padding(1, 2).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(2)
|
MarginBottom(boxMargin)
|
||||||
|
|
||||||
helpStyle := lipgloss.NewStyle().
|
helpStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#666666")).
|
Foreground(lipgloss.Color("#666666")).
|
||||||
@@ -302,52 +411,88 @@ func (m model) View() string {
|
|||||||
Bold(true).
|
Bold(true).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FF0000")).
|
BorderForeground(lipgloss.Color("#FF0000")).
|
||||||
Padding(0, 2).
|
Padding(0, boxPadding).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
MarginBottom(1)
|
MarginBottom(1)
|
||||||
|
|
||||||
|
rulesBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
||||||
rulesBoxStyle := lipgloss.NewStyle().
|
rulesBoxStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FAFAFA")).
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
Padding(0, 2).
|
Padding(0, boxPadding).
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
MarginBottom(1)
|
MarginBottom(1).
|
||||||
|
Width(rulesBoxWidth)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(titleStyle.Render("🔧 Edit Subdomain"))
|
var title string
|
||||||
|
if isVeryCompact {
|
||||||
|
title = "Edit Subdomain"
|
||||||
|
} else {
|
||||||
|
title = "🔧 Edit Subdomain"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(title))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
if m.tunnelType != types.HTTP {
|
if m.tunnelType != types.HTTP {
|
||||||
|
warningBoxWidth := getResponsiveWidth(m.width, 10, 30, 60)
|
||||||
warningBoxStyle := lipgloss.NewStyle().
|
warningBoxStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#FFA500")).
|
Foreground(lipgloss.Color("#FFA500")).
|
||||||
Background(lipgloss.Color("#3D2000")).
|
Background(lipgloss.Color("#3D2000")).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FFA500")).
|
BorderForeground(lipgloss.Color("#FFA500")).
|
||||||
Padding(1, 2).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(2)
|
MarginBottom(boxMargin).
|
||||||
|
Width(warningBoxWidth)
|
||||||
|
|
||||||
b.WriteString(warningBoxStyle.Render("⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization. "))
|
var warningText string
|
||||||
|
if isVeryCompact {
|
||||||
|
warningText = "⚠️ TCP tunnels don't support custom subdomains."
|
||||||
|
} else {
|
||||||
|
warningText = "⚠️ TCP tunnels cannot have custom subdomains. Only HTTP/HTTPS tunnels support subdomain customization."
|
||||||
|
}
|
||||||
|
b.WriteString(warningBoxStyle.Render(warningText))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(helpStyle.Render("Press Enter or Esc to go back"))
|
|
||||||
|
var helpText string
|
||||||
|
if isVeryCompact {
|
||||||
|
helpText = "Press any key to go back"
|
||||||
|
} else {
|
||||||
|
helpText = "Press Enter or Esc to go back"
|
||||||
|
}
|
||||||
|
b.WriteString(helpStyle.Render(helpText))
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesContent := "📋 Rules: \n\t• 3-20 chars \n\t• a-z, 0-9, - \n\t• No leading/trailing -"
|
var rulesContent string
|
||||||
|
if isVeryCompact {
|
||||||
|
rulesContent = "Rules:\n3-20 chars\na-z, 0-9, -\nNo leading/trailing -"
|
||||||
|
} else if isCompact {
|
||||||
|
rulesContent = "📋 Rules:\n • 3-20 chars\n • a-z, 0-9, -\n • No leading/trailing -"
|
||||||
|
} else {
|
||||||
|
rulesContent = "📋 Rules: \n\t• 3-20 chars \n\t• a-z, 0-9, - \n\t• No leading/trailing -"
|
||||||
|
}
|
||||||
b.WriteString(rulesBoxStyle.Render(rulesContent))
|
b.WriteString(rulesBoxStyle.Render(rulesContent))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
b.WriteString(instructionStyle.Render("Enter your custom subdomain:"))
|
var instruction string
|
||||||
|
if isVeryCompact {
|
||||||
|
instruction = "Custom subdomain:"
|
||||||
|
} else {
|
||||||
|
instruction = "Enter your custom subdomain:"
|
||||||
|
}
|
||||||
|
b.WriteString(instructionStyle.Render(instruction))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
if m.slugError != "" {
|
if m.slugError != "" {
|
||||||
errorInputBoxStyle := lipgloss.NewStyle().
|
errorInputBoxStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("#FF0000")).
|
BorderForeground(lipgloss.Color("#FF0000")).
|
||||||
Padding(1, 2).
|
Padding(1, boxPadding).
|
||||||
MarginTop(2).
|
MarginTop(boxMargin).
|
||||||
MarginBottom(1)
|
MarginBottom(1)
|
||||||
b.WriteString(errorInputBoxStyle.Render(m.slugInput.View()))
|
b.WriteString(errorInputBoxStyle.Render(m.slugInput.View()))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -359,19 +504,33 @@ func (m model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
previewURL := buildURL(m.protocol, m.slugInput.Value(), m.domain)
|
previewURL := buildURL(m.protocol, m.slugInput.Value(), m.domain)
|
||||||
|
previewWidth := getResponsiveWidth(m.width, 10, 30, 80)
|
||||||
|
|
||||||
|
if len(previewURL) > previewWidth-10 {
|
||||||
|
previewURL = truncateString(previewURL, previewWidth-10)
|
||||||
|
}
|
||||||
|
|
||||||
previewStyle := lipgloss.NewStyle().
|
previewStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#04B575")).
|
Foreground(lipgloss.Color("#04B575")).
|
||||||
Italic(true).
|
Italic(true).
|
||||||
Width(80)
|
Width(previewWidth)
|
||||||
b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL)))
|
b.WriteString(previewStyle.Render(fmt.Sprintf("Preview: %s", previewURL)))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
b.WriteString(helpStyle.Render("Press Enter to save • CTRL+R for random • Esc to cancel"))
|
var helpText string
|
||||||
|
if isVeryCompact {
|
||||||
|
helpText = "Enter: save • CTRL+R: random • Esc: cancel"
|
||||||
|
} else {
|
||||||
|
helpText = "Press Enter to save • CTRL+R for random • Esc to cancel"
|
||||||
|
}
|
||||||
|
b.WriteString(helpStyle.Render(helpText))
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.showingCommands {
|
if m.showingCommands {
|
||||||
|
isCompact := shouldUseCompactLayout(m.width, 60)
|
||||||
|
|
||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
@@ -385,11 +544,25 @@ func (m model) View() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(titleStyle.Render("⚡ Commands"))
|
|
||||||
|
var title string
|
||||||
|
if shouldUseCompactLayout(m.width, 40) {
|
||||||
|
title = "Commands"
|
||||||
|
} else {
|
||||||
|
title = "⚡ Commands"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(title))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
b.WriteString(m.commandList.View())
|
b.WriteString(m.commandList.View())
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(helpStyle.Render("↑/↓ Navigate • Enter Select • Esc Cancel"))
|
|
||||||
|
var helpText string
|
||||||
|
if isCompact {
|
||||||
|
helpText = "↑/↓ Nav • Enter Select • Esc Cancel"
|
||||||
|
} else {
|
||||||
|
helpText = "↑/↓ Navigate • Enter Select • Esc Cancel"
|
||||||
|
}
|
||||||
|
b.WriteString(helpStyle.Render(helpText))
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -397,49 +570,151 @@ func (m model) View() string {
|
|||||||
titleStyle := lipgloss.NewStyle().
|
titleStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
PaddingTop(1).
|
PaddingTop(1)
|
||||||
PaddingBottom(1)
|
|
||||||
|
|
||||||
subtitleStyle := lipgloss.NewStyle().
|
subtitleStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#7D56F4")).
|
Foreground(lipgloss.Color("#888888")).
|
||||||
Italic(true)
|
Italic(true)
|
||||||
|
|
||||||
urlStyle := lipgloss.NewStyle().
|
urlStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#04B575")).
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
Underline(true)
|
Underline(true).
|
||||||
|
Italic(true)
|
||||||
|
|
||||||
sectionTitleStyle := lipgloss.NewStyle().
|
urlBoxStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#FAFAFA")).
|
|
||||||
MarginTop(1).
|
|
||||||
MarginBottom(1)
|
|
||||||
|
|
||||||
forwardingStyle := lipgloss.NewStyle().
|
|
||||||
Bold(true).
|
|
||||||
Foreground(lipgloss.Color("#04B575")).
|
Foreground(lipgloss.Color("#04B575")).
|
||||||
Border(lipgloss.RoundedBorder()).
|
Bold(true).
|
||||||
BorderForeground(lipgloss.Color("#04B575")).
|
Italic(true)
|
||||||
Padding(0, 2).
|
|
||||||
MarginTop(1).
|
keyHintStyle := lipgloss.NewStyle().
|
||||||
MarginBottom(1)
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("🚇 Tunnel Pls"))
|
isCompact := shouldUseCompactLayout(m.width, 85)
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(subtitleStyle.Render("Project by Bagas"))
|
var asciiArtMargin int
|
||||||
|
if isCompact {
|
||||||
|
asciiArtMargin = 0
|
||||||
|
} else {
|
||||||
|
asciiArtMargin = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
asciiArtStyle := lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("#7D56F4")).
|
||||||
|
MarginBottom(asciiArtMargin)
|
||||||
|
|
||||||
|
var asciiArt string
|
||||||
|
if shouldUseCompactLayout(m.width, 50) {
|
||||||
|
asciiArt = "TUNNEL PLS"
|
||||||
|
} else if isCompact {
|
||||||
|
asciiArt = `
|
||||||
|
▀█▀ █ █ █▄ █ █▄ █ ██▀ █ ▄▀▀ █ ▄▀▀
|
||||||
|
█ ▀▄█ █ ▀█ █ ▀█ █▄▄ █▄▄ ▄█▀ █▄▄ ▄█▀`
|
||||||
|
} else {
|
||||||
|
asciiArt = `
|
||||||
|
████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗ ██████╗ ██╗ ███████╗
|
||||||
|
╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║ ██╔══██╗██║ ██╔════╝
|
||||||
|
██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║ ██████╔╝██║ ███████╗
|
||||||
|
██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║ ██╔═══╝ ██║ ╚════██║
|
||||||
|
██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗ ██║ ███████╗███████║
|
||||||
|
╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝`
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(asciiArtStyle.Render(asciiArt))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
if !shouldUseCompactLayout(m.width, 60) {
|
||||||
|
b.WriteString(subtitleStyle.Render("Secure tunnel service by Bagas • "))
|
||||||
b.WriteString(urlStyle.Render("https://fossy.my.id"))
|
b.WriteString(urlStyle.Render("https://fossy.my.id"))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
b.WriteString(sectionTitleStyle.Render("Welcome to Tunnel!"))
|
boxMaxWidth := getResponsiveWidth(m.width, 10, 40, 80)
|
||||||
|
var boxPadding int
|
||||||
|
var boxMargin int
|
||||||
|
if isCompact {
|
||||||
|
boxPadding = 1
|
||||||
|
boxMargin = 1
|
||||||
|
} else {
|
||||||
|
boxPadding = 2
|
||||||
|
boxMargin = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
responsiveInfoBox := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||||
|
Padding(1, boxPadding).
|
||||||
|
MarginTop(boxMargin).
|
||||||
|
MarginBottom(boxMargin).
|
||||||
|
Width(boxMaxWidth)
|
||||||
|
|
||||||
|
urlDisplay := m.tunnelURL
|
||||||
|
if shouldUseCompactLayout(m.width, 80) && len(m.tunnelURL) > m.width-20 {
|
||||||
|
maxLen := m.width - 25
|
||||||
|
if maxLen > 10 {
|
||||||
|
urlDisplay = truncateString(m.tunnelURL, maxLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var infoContent string
|
||||||
|
if shouldUseCompactLayout(m.width, 70) {
|
||||||
|
infoContent = fmt.Sprintf("🌐 %s", urlBoxStyle.Render(urlDisplay))
|
||||||
|
} else if isCompact {
|
||||||
|
infoContent = fmt.Sprintf("🌐 Forwarding to:\n\n %s", urlBoxStyle.Render(urlDisplay))
|
||||||
|
} else {
|
||||||
|
infoContent = fmt.Sprintf("🌐 F O R W A R D I N G T O:\n\n %s", urlBoxStyle.Render(urlDisplay))
|
||||||
|
}
|
||||||
|
b.WriteString(responsiveInfoBox.Render(infoContent))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
var quickActionsTitle string
|
||||||
|
if shouldUseCompactLayout(m.width, 50) {
|
||||||
|
quickActionsTitle = "Actions"
|
||||||
|
} else if isCompact {
|
||||||
|
quickActionsTitle = "Quick Actions"
|
||||||
|
} else {
|
||||||
|
quickActionsTitle = "✨ Quick Actions"
|
||||||
|
}
|
||||||
|
b.WriteString(titleStyle.Render(quickActionsTitle))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
forwardingText := fmt.Sprintf("🌐 Forwarding your traffic to:\n %s", m.tunnelURL)
|
|
||||||
b.WriteString(forwardingStyle.Render(forwardingText))
|
|
||||||
|
|
||||||
b.WriteString(m.helpView())
|
var featureMargin int
|
||||||
|
if isCompact {
|
||||||
|
featureMargin = 1
|
||||||
|
} else {
|
||||||
|
featureMargin = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
compactFeatureStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#FAFAFA")).
|
||||||
|
MarginLeft(featureMargin)
|
||||||
|
|
||||||
|
var commandsText string
|
||||||
|
var quitText string
|
||||||
|
if shouldUseCompactLayout(m.width, 60) {
|
||||||
|
commandsText = fmt.Sprintf(" %s Commands", keyHintStyle.Render("[C]"))
|
||||||
|
quitText = fmt.Sprintf(" %s Quit", keyHintStyle.Render("[Q]"))
|
||||||
|
} else {
|
||||||
|
commandsText = fmt.Sprintf(" %s Open commands menu", keyHintStyle.Render("[C]"))
|
||||||
|
quitText = fmt.Sprintf(" %s Quit application", keyHintStyle.Render("[Q]"))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(compactFeatureStyle.Render(commandsText))
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(compactFeatureStyle.Render(quitText))
|
||||||
|
|
||||||
|
if !shouldUseCompactLayout(m.width, 70) {
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
footerStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#666666")).
|
||||||
|
Italic(true)
|
||||||
|
b.WriteString(footerStyle.Render("Press 'C' to customize your tunnel settings"))
|
||||||
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -517,6 +792,7 @@ func (i *Interaction) Start() {
|
|||||||
tea.WithMouseCellMotion(),
|
tea.WithMouseCellMotion(),
|
||||||
tea.WithoutSignals(),
|
tea.WithoutSignals(),
|
||||||
tea.WithoutSignalHandler(),
|
tea.WithoutSignalHandler(),
|
||||||
|
tea.WithFPS(30),
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := i.program.Run()
|
_, err := i.program.Run()
|
||||||
@@ -537,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request, sshChan <-chan
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session.HandleTCPIPForward(tcpipReq)
|
go session.HandleTCPIPForward(tcpipReq)
|
||||||
})
|
})
|
||||||
go session.HandleGlobalRequest(reqs)
|
session.HandleGlobalRequest(reqs)
|
||||||
}
|
}
|
||||||
if err := session.lifecycle.Close(); err != nil {
|
if err := session.lifecycle.Close(); err != nil {
|
||||||
log.Printf("failed to close session: %v", err)
|
log.Printf("failed to close session: %v", err)
|
||||||
|
|||||||
17
version/version.go
Normal file
17
version/version.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user