Improve concurrency and resource management #2

Merged
bagas merged 12 commits from staging into main 2025-07-23 06:51:09 +00:00
16 changed files with 298 additions and 1372 deletions

90
internal/port/port.go Normal file
View File

@ -0,0 +1,90 @@
package port
import (
"fmt"
"sort"
"strconv"
"strings"
"sync"
"tunnel_pls/utils"
)
type PortManager struct {
mu sync.RWMutex
ports map[uint16]bool
sortedPorts []uint16
}
var Manager = PortManager{
ports: make(map[uint16]bool),
sortedPorts: []uint16{},
}
func init() {
rawRange := utils.Getenv("ALLOWED_PORTS")
splitRange := strings.Split(rawRange, "-")
if len(splitRange) != 2 {
Manager.AddPortRange(30000, 31000)
} else {
start, err := strconv.ParseUint(splitRange[0], 10, 16)
if err != nil {
start = 30000
}
end, err := strconv.ParseUint(splitRange[1], 10, 16)
if err != nil {
end = 31000
}
Manager.AddPortRange(uint16(start), uint16(end))
}
}
func (pm *PortManager) AddPortRange(startPort, endPort uint16) error {
pm.mu.Lock()
defer pm.mu.Unlock()
if startPort > endPort {
return fmt.Errorf("start port cannot be greater than end port")
}
for port := startPort; port <= endPort; port++ {
if _, exists := pm.ports[port]; !exists {
pm.ports[port] = false
pm.sortedPorts = append(pm.sortedPorts, port)
}
}
sort.Slice(pm.sortedPorts, func(i, j int) bool {
return pm.sortedPorts[i] < pm.sortedPorts[j]
})
return nil
}
func (pm *PortManager) GetUnassignedPort() (uint16, bool) {
pm.mu.Lock()
defer pm.mu.Unlock()
for _, port := range pm.sortedPorts {
if !pm.ports[port] {
pm.ports[port] = true
return port, true
}
}
return 0, false
}
func (pm *PortManager) SetPortStatus(port uint16, assigned bool) error {
pm.mu.Lock()
defer pm.mu.Unlock()
if _, exists := pm.ports[port]; !exists {
return fmt.Errorf("port %d is not in the allowed range", port)
}
pm.ports[port] = assigned
return nil
}
func (pm *PortManager) GetPortStatus(port uint16) (bool, bool) {
pm.mu.RLock()
defer pm.mu.RUnlock()
status, exists := pm.ports[port]
return status, exists
}

View File

@ -1,50 +0,0 @@
/*
Package proto provides byte-level interaction with HTTP request payload.
Example of HTTP payload for future references, new line symbols escaped:
POST /upload HTTP/1.1\r\n
User-Agent: Gor\r\n
Content-Length: 11\r\n
\r\n
Hello world
GET /index.html HTTP/1.1\r\n
User-Agent: Gor\r\n
\r\n
\r\n
https://github.com/buger/goreplay/blob/master/proto/proto.go
*/
package proto
import (
"bytes"
"net/http"
)
var Methods = [...]string{
http.MethodConnect, http.MethodDelete, http.MethodGet,
http.MethodHead, http.MethodOptions, http.MethodPatch,
http.MethodPost, http.MethodPut, http.MethodTrace,
}
func Method(payload []byte) []byte {
end := bytes.IndexByte(payload, ' ')
if end == -1 {
return nil
}
return payload[:end]
}
func IsHttpRequest(payload []byte) bool {
method := string(Method(payload))
var methodFound bool
for _, m := range Methods {
if methodFound = method == m; methodFound {
break
}
}
return methodFound
}

View File

@ -1,846 +0,0 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-12 {
margin-bottom: 3rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mt-16 {
margin-top: 4rem;
}
.mt-4 {
margin-top: 1rem;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.h-12 {
height: 3rem;
}
.h-5 {
height: 1.25rem;
}
.h-6 {
height: 1.5rem;
}
.min-h-screen {
min-height: 100vh;
}
.w-12 {
width: 3rem;
}
.w-5 {
width: 1.25rem;
}
.w-6 {
width: 1.5rem;
}
.max-w-2xl {
max-width: 42rem;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-8 {
gap: 2rem;
}
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1rem * var(--tw-space-x-reverse));
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
}
.rounded {
border-radius: 0.25rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
}
.bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
}
.bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
}
.bg-gradient-to-b {
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
}
.from-gray-900 {
--tw-gradient-from: #111827 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-gray-800 {
--tw-gradient-to: #1f2937 var(--tw-gradient-to-position);
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-16 {
padding-top: 4rem;
padding-bottom: 4rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.text-center {
text-align: center;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
.text-blue-400 {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity, 1));
}
.text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.text-green-400 {
--tw-text-opacity: 1;
color: rgb(74 222 128 / var(--tw-text-opacity, 1));
}
.text-purple-400 {
--tw-text-opacity: 1;
color: rgb(192 132 252 / var(--tw-text-opacity, 1));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.hover\:bg-blue-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
}
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
@media (min-width: 768px) {
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}

View File

@ -8,14 +8,29 @@ import (
)
func (s *Server) handleConnection(conn net.Conn) {
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.Config)
sshConn, chans, forwardingReqs, err := ssh.NewServerConn(conn, s.Config)
if err != nil {
log.Printf("failed to establish SSH connection: %v", err)
conn.Close()
err := conn.Close()
if err != nil {
log.Printf("failed to close SSH connection: %v", err)
return
}
return
}
log.Println("SSH connection established:", sshConn.User())
session.New(sshConn, chans, reqs)
newSession := session.New(sshConn, forwardingReqs)
for ch := range chans {
newSession.ChannelChan <- ch
}
defer func(newSession *session.Session) {
err := newSession.Close()
if err != nil {
log.Printf("failed to close session: %v", err)
}
}(newSession)
return
}

View File

@ -5,17 +5,14 @@ import (
"bytes"
"errors"
"fmt"
"golang.org/x/net/context"
"log"
"net"
"strconv"
"strings"
"time"
"tunnel_pls/session"
"tunnel_pls/utils"
)
var redirectTLS bool = false
var redirectTLS = false
func NewHTTPServer() error {
listener, err := net.Listen("tcp", ":80")
@ -81,23 +78,10 @@ func Handler(conn net.Conn) {
conn.Close()
return
}
keepalive, timeout := parseConnectionDetails(headers)
var ctx context.Context
var cancel context.CancelFunc
if keepalive {
if timeout >= 300 {
timeout = 300
}
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Duration(timeout)*time.Second))
} else {
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
}
sshSession.HandleForwardedConnection(session.UserConnection{
Reader: reader,
Writer: conn,
Context: ctx,
Cancel: cancel,
Reader: reader,
Writer: conn,
}, sshSession.Connection)
return
}
@ -131,42 +115,3 @@ func parseHostFromHeader(data []byte) string {
}
return ""
}
func parseConnectionDetails(data []byte) (keepAlive bool, timeout int) {
keepAlive = false
timeout = 30
lines := strings.Split(string(data), "\r\n")
for _, line := range lines {
if strings.HasPrefix(strings.ToLower(line), "connection:") {
value := strings.TrimSpace(strings.TrimPrefix(strings.ToLower(line), "connection:"))
keepAlive = (value == "keep-alive")
break
}
}
if keepAlive {
for _, line := range lines {
if strings.HasPrefix(strings.ToLower(line), "keep-alive:") {
value := strings.TrimSpace(strings.TrimPrefix(line, "Keep-Alive:"))
if strings.Contains(value, "timeout=") {
parts := strings.Split(value, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "timeout=") {
timeoutStr := strings.TrimPrefix(part, "timeout=")
if t, err := strconv.Atoi(timeoutStr); err == nil {
timeout = t
}
}
}
}
break
}
}
}
return keepAlive, timeout
}

View File

@ -4,11 +4,9 @@ import (
"bufio"
"crypto/tls"
"errors"
"golang.org/x/net/context"
"log"
"net"
"strings"
"time"
"tunnel_pls/session"
"tunnel_pls/utils"
)
@ -70,23 +68,10 @@ func HandlerTLS(conn net.Conn) {
conn.Close()
return
}
keepalive, timeout := parseConnectionDetails(headers)
var ctx context.Context
var cancel context.CancelFunc
if keepalive {
if timeout >= 300 {
timeout = 300
}
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Duration(timeout)*time.Second))
} else {
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
}
sshSession.HandleForwardedConnection(session.UserConnection{
Reader: reader,
Writer: conn,
Context: ctx,
Cancel: cancel,
Reader: reader,
Writer: conn,
}, sshSession.Connection)
return
}

View File

@ -3,6 +3,7 @@ package session
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
@ -13,9 +14,9 @@ import (
"strings"
"sync"
"time"
portUtil "tunnel_pls/internal/port"
"golang.org/x/crypto/ssh"
"golang.org/x/net/context"
"tunnel_pls/utils"
)
@ -28,10 +29,8 @@ const (
)
type UserConnection struct {
Reader io.Reader
Writer net.Conn
Context context.Context
Cancel context.CancelFunc
Reader io.Reader
Writer net.Conn
}
var (
@ -39,19 +38,6 @@ var (
Clients = make(map[string]*Session)
)
type Session struct {
Connection *ssh.ServerConn
ConnChannels []ssh.Channel
GlobalRequest <-chan *ssh.Request
Listener net.Listener
TunnelType TunnelType
ForwardedPort uint16
Status SessionStatus
Slug string
SlugChannel chan bool
Done chan bool
}
func registerClient(slug string, session *Session) bool {
clientsMutex.Lock()
defer clientsMutex.Unlock()
@ -90,45 +76,66 @@ func updateClientSlug(oldSlug, newSlug string) bool {
return true
}
func (s *Session) Close() {
func (s *Session) safeClose() {
s.once.Do(func() {
close(s.ChannelChan)
close(s.Done)
})
}
func (s *Session) Close() error {
if s.Listener != nil {
s.Listener.Close()
err := s.Listener.Close()
if err != nil && !errors.Is(err, net.ErrClosed) {
fmt.Println("1")
return err
}
}
for _, ch := range s.ConnChannels {
ch.Close()
if s.ConnChannel != nil {
err := s.ConnChannel.Close()
if err != nil && !errors.Is(err, io.EOF) {
fmt.Println("2")
return err
}
}
if s.Connection != nil {
s.Connection.Close()
err := s.Connection.Close()
if err != nil && !errors.Is(err, net.ErrClosed) {
fmt.Println("3")
return err
}
}
if s.Slug != "" {
unregisterClient(s.Slug)
}
close(s.Done)
if s.TunnelType == TCP {
err := portUtil.Manager.SetPortStatus(s.ForwardedPort, false)
if err != nil {
fmt.Println("4")
return err
}
}
s.safeClose()
return nil
}
func (s *Session) handleGlobalRequest() {
ticker := time.NewTicker(1 * time.Second)
for {
select {
case req := <-s.GlobalRequest:
ticker.Stop()
if req == nil {
return
}
if req.Type == "tcpip-forward" {
s.handleTCPIPForward(req)
} else {
req.Reply(false, nil)
}
case <-s.Done:
func (s *Session) HandleGlobalRequest(GlobalRequest <-chan *ssh.Request) {
for req := range GlobalRequest {
switch req.Type {
case "tcpip-forward":
s.handleTCPIPForward(req)
return
case <-ticker.C:
s.sendMessage(fmt.Sprintf("Please specify the forwarding tunnel. For example: 'ssh %s -p %s -R 443:localhost:8080' \r\n\n\n", utils.Getenv("domain"), utils.Getenv("port")))
s.Close()
case "shell", "pty-req", "window-change":
req.Reply(true, nil)
default:
log.Println("Unknown request type:", req.Type)
req.Reply(false, nil)
}
}
}
@ -146,41 +153,69 @@ func (s *Session) handleTCPIPForward(req *ssh.Request) {
return
}
var portToBind uint32
if err := binary.Read(reader, binary.BigEndian, &portToBind); err != nil {
var rawPortToBind uint32
if err := binary.Read(reader, binary.BigEndian, &rawPortToBind); err != nil {
log.Println("Failed to read port from payload:", err)
s.sendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port.\r\n", portToBind))
s.sendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (02) \r\n", rawPortToBind))
req.Reply(false, nil)
s.Close()
return
}
if rawPortToBind > 65535 {
s.sendMessage(fmt.Sprintf("Port %d is larger then allowed port of 65535. (02)\r\n", rawPortToBind))
req.Reply(false, nil)
s.Close()
return
}
portToBind := uint16(rawPortToBind)
if isBlockedPort(portToBind) {
s.sendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port.\r\n", portToBind))
s.sendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (02)\r\n", portToBind))
req.Reply(false, nil)
s.Close()
return
}
s.sendMessage("\033[H\033[2J")
showWelcomeMessage(s.ConnChannels[0])
showWelcomeMessage(s.ConnChannel)
s.Status = RUNNING
go s.handleUserInput()
if portToBind == 80 || portToBind == 443 {
s.handleHTTPForward(req, portToBind)
return
} else {
if portToBind == 0 {
unassign, success := portUtil.Manager.GetUnassignedPort()
portToBind = unassign
if !success {
s.sendMessage(fmt.Sprintf("No available port\r\n", portToBind))
req.Reply(false, nil)
s.Close()
return
}
} else if isUse, isExist := portUtil.Manager.GetPortStatus(portToBind); !isExist || isUse {
s.sendMessage(fmt.Sprintf("Port %d is already in use or restricted. Please choose a different port. (03)\r\n", portToBind))
req.Reply(false, nil)
s.Close()
return
}
portUtil.Manager.SetPortStatus(portToBind, true)
}
s.handleTCPForward(req, addr, portToBind)
}
var blockedReservedPorts = []uint32{1080, 1433, 1521, 1900, 2049, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 9000, 9200, 27017}
var blockedReservedPorts = []uint16{1080, 1433, 1521, 1900, 2049, 3306, 3389, 5432, 5900, 6379, 8080, 8443, 9000, 9200, 27017}
func isBlockedPort(port uint32) bool {
func isBlockedPort(port uint16) bool {
if port == 80 || port == 443 {
return false
}
if port < 1024 {
if port < 1024 && port != 0 {
return true
}
for _, p := range blockedReservedPorts {
@ -191,7 +226,7 @@ func isBlockedPort(port uint32) bool {
return false
}
func (s *Session) handleHTTPForward(req *ssh.Request, portToBind uint32) {
func (s *Session) handleHTTPForward(req *ssh.Request, portToBind uint16) {
s.TunnelType = HTTP
s.ForwardedPort = uint16(portToBind)
@ -220,7 +255,7 @@ func (s *Session) handleHTTPForward(req *ssh.Request, portToBind uint32) {
req.Reply(true, buf.Bytes())
}
func (s *Session) handleTCPForward(req *ssh.Request, addr string, portToBind uint32) {
func (s *Session) handleTCPForward(req *ssh.Request, addr string, portToBind uint16) {
s.TunnelType = TCP
log.Printf("Requested forwarding on %s:%d", addr, portToBind)
@ -255,9 +290,8 @@ func (s *Session) acceptTCPConnections() {
}
go s.HandleForwardedConnection(UserConnection{
Reader: nil,
Writer: conn,
Context: context.Background(),
Reader: nil,
Writer: conn,
}, s.Connection)
}
}
@ -300,33 +334,19 @@ func (s *Session) waitForRunningStatus() {
}
func (s *Session) sendMessage(message string) {
if len(s.ConnChannels) > 0 {
s.ConnChannels[0].Write([]byte(message))
if s.ConnChannel != nil {
s.ConnChannel.Write([]byte(message))
}
}
func (s *Session) HandleSessionChannel(newChannel ssh.NewChannel) {
connection, requests, err := newChannel.Accept()
if err != nil {
log.Printf("Could not accept channel: %s", err)
return
}
s.ConnChannels = append(s.ConnChannels, connection)
go s.handleUserInput(connection)
go s.handleChannelRequests(connection, requests)
}
func (s *Session) handleUserInput(connection ssh.Channel) {
func (s *Session) handleUserInput() {
var commandBuffer bytes.Buffer
buf := make([]byte, 1)
inSlugEditMode := false
editSlug := s.Slug
for {
n, err := connection.Read(buf)
n, err := s.ConnChannel.Read(buf)
if err != nil {
if err != io.EOF {
log.Printf("Error reading from client: %s", err)
@ -338,16 +358,16 @@ func (s *Session) handleUserInput(connection ssh.Channel) {
char := buf[0]
if inSlugEditMode {
s.handleSlugEditMode(connection, &inSlugEditMode, &editSlug, char, &commandBuffer)
s.handleSlugEditMode(s.ConnChannel, &inSlugEditMode, &editSlug, char, &commandBuffer)
continue
}
connection.Write(buf[:n])
s.ConnChannel.Write(buf[:n])
if char == 8 || char == 127 {
if commandBuffer.Len() > 0 {
commandBuffer.Truncate(commandBuffer.Len() - 1)
connection.Write([]byte("\b \b"))
s.ConnChannel.Write([]byte("\b \b"))
}
continue
}
@ -360,7 +380,7 @@ func (s *Session) handleUserInput(connection ssh.Channel) {
if commandBuffer.Len() > 0 {
if char == 13 {
s.handleCommand(connection, commandBuffer.String(), &inSlugEditMode, &editSlug, &commandBuffer)
s.handleCommand(s.ConnChannel, commandBuffer.String(), &inSlugEditMode, &editSlug, &commandBuffer)
continue
}
commandBuffer.WriteByte(char)
@ -494,7 +514,7 @@ func (s *Session) handleCommand(connection ssh.Channel, command string, inSlugEd
connection.Write([]byte("\r\nAvailable commands: /bye, /help, /clear, /slug"))
case "/clear":
connection.Write([]byte("\033[H\033[2J"))
showWelcomeMessage(s.ConnChannels[0])
showWelcomeMessage(s.ConnChannel)
domain := utils.Getenv("domain")
if s.TunnelType == HTTP {
protocol := "http"
@ -523,20 +543,6 @@ func (s *Session) handleCommand(connection ssh.Channel, command string, inSlugEd
commandBuffer.Reset()
}
func (s *Session) handleChannelRequests(connection ssh.Channel, requests <-chan *ssh.Request) {
go s.handleGlobalRequest()
for req := range requests {
switch req.Type {
case "shell", "pty-req", "window-change":
req.Reply(true, nil)
default:
log.Println("Unknown request type:", req.Type)
req.Reply(false, nil)
}
}
}
func (s *Session) HandleForwardedConnection(conn UserConnection, sshConn *ssh.ServerConn) {
defer conn.Writer.Close()
@ -554,45 +560,74 @@ func (s *Session) HandleForwardedConnection(conn UserConnection, sshConn *ssh.Se
}
defer channel.Close()
go handleChannelRequests(reqs, conn, channel, s.SlugChannel)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in request handler: %v", r)
}
}()
for req := range reqs {
req.Reply(false, nil)
}
}()
if conn.Reader == nil {
conn.Reader = bufio.NewReader(conn.Writer)
}
go io.Copy(channel, conn.Reader)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in reader copy: %v", r)
}
cancel()
}()
_, err := io.Copy(channel, conn.Reader)
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
log.Printf("Error copying from conn.Reader to channel: %v", err)
}
cancel()
}()
reader := bufio.NewReader(channel)
_, err = reader.Peek(1)
if err == io.EOF {
s.sendMessage(fmt.Sprintf("\033[33m%s -> [%s] WARNING -- \"%s\"\033[0m\r\n", conn.Writer.RemoteAddr().String(), s.TunnelType, "Could not forward request to the tunnel addr"))
peekChan := make(chan error, 1)
go func() {
_, err := reader.Peek(1)
peekChan <- err
}()
select {
case err := <-peekChan:
if err == io.EOF {
s.sendMessage(fmt.Sprintf("\033[33m%s -> [%s] WARNING -- \"Could not forward request to the tunnel addr\"\033[0m\r\n", conn.Writer.RemoteAddr().String(), s.TunnelType))
sendBadGatewayResponse(conn.Writer)
return
}
if err != nil {
log.Printf("Error peeking channel data: %v", err)
s.sendMessage(fmt.Sprintf("\033[33m%s -> [%s] WARNING -- \"Could not forward request to the tunnel addr\"\033[0m\r\n", conn.Writer.RemoteAddr().String(), s.TunnelType))
sendBadGatewayResponse(conn.Writer)
return
}
case <-time.After(5 * time.Second):
log.Printf("Timeout waiting for channel data from %s", conn.Writer.RemoteAddr())
s.sendMessage(fmt.Sprintf("\033[33m%s -> [%s] WARNING -- \"Could not forward request to the tunnel addr\"\033[0m\r\n", conn.Writer.RemoteAddr().String(), s.TunnelType))
sendBadGatewayResponse(conn.Writer)
conn.Writer.Close()
channel.Close()
return
case <-ctx.Done():
return
}
s.sendMessage(fmt.Sprintf("\033[32m %s -> [%s] TUNNEL ADDRESS -- \"%s\" \r\n \033[0m", conn.Writer.RemoteAddr().String(), s.TunnelType, timestamp))
s.sendMessage(fmt.Sprintf("\033[32m%s -> [%s] TUNNEL ADDRESS -- \"%s\"\033[0m\r\n", conn.Writer.RemoteAddr().String(), s.TunnelType, timestamp))
io.Copy(conn.Writer, reader)
}
func handleChannelRequests(reqs <-chan *ssh.Request, conn UserConnection, channel ssh.Channel, slugChannel <-chan bool) {
select {
case <-reqs:
for req := range reqs {
req.Reply(false, nil)
}
case <-conn.Context.Done():
conn.Writer.Close()
channel.Close()
log.Println("Connection closed by timeout")
return
case <-slugChannel:
conn.Writer.Close()
channel.Close()
log.Println("Connection closed by slug change")
return
_, err = io.Copy(conn.Writer, reader)
if err != nil && !errors.Is(err, io.EOF) {
log.Printf("Error copying from channel to conn.Writer: %v", err)
}
}

View File

@ -2,6 +2,8 @@ package session
import (
"golang.org/x/crypto/ssh"
"net"
"sync"
)
type TunnelType string
@ -13,21 +15,39 @@ const (
UNKNOWN TunnelType = "unknown"
)
func New(conn *ssh.ServerConn, sshChannel <-chan ssh.NewChannel, req <-chan *ssh.Request) *Session {
type Session struct {
Connection *ssh.ServerConn
ConnChannel ssh.Channel
Listener net.Listener
TunnelType TunnelType
ForwardedPort uint16
Status SessionStatus
Slug string
ChannelChan chan ssh.NewChannel
Done chan bool
once sync.Once
}
func New(conn *ssh.ServerConn, forwardingReq <-chan *ssh.Request) *Session {
session := &Session{
Status: SETUP,
Slug: "",
ConnChannels: []ssh.Channel{},
Connection: conn,
GlobalRequest: req,
TunnelType: UNKNOWN,
SlugChannel: make(chan bool),
Done: make(chan bool),
Status: SETUP,
Slug: "",
ConnChannel: nil,
Connection: conn,
TunnelType: UNKNOWN,
ChannelChan: make(chan ssh.NewChannel),
Done: make(chan bool),
}
go func() {
for newChannel := range sshChannel {
go session.HandleSessionChannel(newChannel)
for channel := range session.ChannelChan {
ch, reqs, _ := channel.Accept()
if session.ConnChannel == nil {
session.ConnChannel = ch
session.Status = RUNNING
go session.HandleGlobalRequest(forwardingReq)
}
go session.HandleGlobalRequest(reqs)
}
}()

View File

@ -1,10 +0,0 @@
@echo off
REM Start the Go server using Air
start "" air
REM Watch for changes in Tailwind CSS
start "" npx tailwindcss -i ./public/input.css -o ./public/output.css --watch
REM Watch for changes in templates and proxy to Go server
start "" cmd /k "templ generate -watch"

View File

@ -1,11 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./view/**/*.{html,js,templ}",
"./public/*.js"
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -1,87 +0,0 @@
package indexView
import "tunnel_pls/view/layout"
templ Main(title, domain string) {
@layout.Base(title) {
<div class="min-h-screen bg-gradient-to-b from-gray-900 to-gray-800 text-white">
<header class="container mx-auto px-4 py-8">
<nav class="flex justify-between items-center">
<h1 class="text-2xl font-bold">Tunnel Pls</h1>
</nav>
</header>
<main class="container mx-auto px-4 py-16 text-center">
<h2 class="text-4xl font-bold mb-6">Secure Remote Access Made Simple</h2>
<p class="text-xl mb-12 max-w-2xl mx-auto">
SSH Tunnel provides a secure way to access your local services remotely.
Connect to your development environment from anywhere, safely.
</p>
<div class="bg-gray-800 p-6 rounded-lg shadow-lg inline-block">
<h3 class="text-2xl font-semibold mb-4 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
Example Command
</h3>
<code id="sshCommand" class="text-green-400 text-lg block mb-4 bg-gray-900 p-4 rounded">
ssh {domain} -p 2200 -R 80:localhost:8000
</code>
<button id="copyButton" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors flex items-center justify-center mx-auto">
Copy Command
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
<div class="mt-16 grid md:grid-cols-3 gap-8">
<div class="bg-gray-800 p-6 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
<h3 class="text-xl font-semibold mb-2">Secure</h3>
<p>End-to-end encryption ensures your data remains private and protected.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<h3 class="text-xl font-semibold mb-2">Easy to Use</h3>
<p>Simple command-line interface for quick setup and connection.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
<h3 class="text-xl font-semibold mb-2">Flexible</h3>
<p>Connect to various local services and ports with ease.</p>
</div>
</div>
</main>
<footer class="container mx-auto px-4 py-8 text-center text-gray-400">
<p>&copy; 2025 SSH Tunnel. All rights reserved.</p>
<div class="mt-4 flex justify-center space-x-4">
<a href="https://github.com/fossyy" target="_blank" rel="noopener noreferrer" class="hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path>
</svg>
</a>
<a href="mailto:bagas@fossy.my.id" class="hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</a>
</div>
</footer>
<script>
document.getElementById('copyButton').addEventListener('click', function() {
const sshCommand = document.getElementById('sshCommand').textContent;
navigator.clipboard.writeText(sshCommand).then(function() {
alert('Command copied to clipboard!');
}, function(err) {
console.error('Could not copy text: ', err);
});
});
</script>
</div>
}
}

View File

@ -1,73 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package indexView
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "tunnel_pls/view/layout"
func Main(title, domain string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"min-h-screen bg-gradient-to-b from-gray-900 to-gray-800 text-white\"><header class=\"container mx-auto px-4 py-8\"><nav class=\"flex justify-between items-center\"><h1 class=\"text-2xl font-bold\">Tunnel Pls</h1></nav></header><main class=\"container mx-auto px-4 py-16 text-center\"><h2 class=\"text-4xl font-bold mb-6\">Secure Remote Access Made Simple</h2><p class=\"text-xl mb-12 max-w-2xl mx-auto\">SSH Tunnel provides a secure way to access your local services remotely. Connect to your development environment from anywhere, safely.</p><div class=\"bg-gray-800 p-6 rounded-lg shadow-lg inline-block\"><h3 class=\"text-2xl font-semibold mb-4 flex items-center justify-center\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"></path></svg> Example Command</h3><code id=\"sshCommand\" class=\"text-green-400 text-lg block mb-4 bg-gray-900 p-4 rounded\">ssh ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(domain)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/index/index.templ`, Line: 27, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " -p 2200 -R 80:localhost:8000</code> <button id=\"copyButton\" class=\"mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors flex items-center justify-center mx-auto\">Copy Command <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5 ml-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"></path></svg></button></div><div class=\"mt-16 grid md:grid-cols-3 gap-8\"><div class=\"bg-gray-800 p-6 rounded-lg\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 mx-auto mb-4 text-blue-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"></path></svg><h3 class=\"text-xl font-semibold mb-2\">Secure</h3><p>End-to-end encryption ensures your data remains private and protected.</p></div><div class=\"bg-gray-800 p-6 rounded-lg\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 mx-auto mb-4 text-green-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"></path></svg><h3 class=\"text-xl font-semibold mb-2\">Easy to Use</h3><p>Simple command-line interface for quick setup and connection.</p></div><div class=\"bg-gray-800 p-6 rounded-lg\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 mx-auto mb-4 text-purple-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 8l4 4m0 0l-4 4m4-4H3\"></path></svg><h3 class=\"text-xl font-semibold mb-2\">Flexible</h3><p>Connect to various local services and ports with ease.</p></div></div></main><footer class=\"container mx-auto px-4 py-8 text-center text-gray-400\"><p>&copy; 2025 SSH Tunnel. All rights reserved.</p><div class=\"mt-4 flex justify-center space-x-4\"><a href=\"https://github.com/fossyy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"hover:text-white\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"></path></svg></a> <a href=\"mailto:bagas@fossy.my.id\" class=\"hover:text-white\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"></path></svg></a></div></footer><script>\r\n document.getElementById('copyButton').addEventListener('click', function() {\r\n const sshCommand = document.getElementById('sshCommand').textContent;\r\n navigator.clipboard.writeText(sshCommand).then(function() {\r\n alert('Command copied to clipboard!');\r\n }, function(err) {\r\n console.error('Could not copy text: ', err);\r\n });\r\n });\r\n </script></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = layout.Base(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,2 +0,0 @@
<div class=\"min-h-screen bg-gradient-to-b from-gray-900 to-gray-800 text-white\"><header class=\"container mx-auto px-4 py-8\"><nav class=\"flex justify-between items-center\"><h1 class=\"text-2xl font-bold\">Tunnel Pls</h1></nav></header><main class=\"container mx-auto px-4 py-16 text-center\"><h2 class=\"text-4xl font-bold mb-6\">Secure Remote Access Made Simple</h2><p class=\"text-xl mb-12 max-w-2xl mx-auto\">SSH Tunnel provides a secure way to access your local services remotely. Connect to your development environment from anywhere, safely.</p><div class=\"bg-gray-800 p-6 rounded-lg shadow-lg inline-block\"><h3 class=\"text-2xl font-semibold mb-4 flex items-center justify-center\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6 mr-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"></path></svg> Example Command</h3><code id=\"sshCommand\" class=\"text-green-400 text-lg block mb-4 bg-gray-900 p-4 rounded\">ssh
-p 2200 -R 80:localhost:8000</code> <button id=\"copyButton\" class=\"mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors flex items-center justify-center mx-auto\">Copy Command <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-5 w-5 ml-2\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\"></path></svg></button></div><div class=\"mt-16 grid md:grid-cols-3 gap-8\"><div class=\"bg-gray-800 p-6 rounded-lg\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 mx-auto mb-4 text-blue-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z\"></path></svg><h3 class=\"text-xl font-semibold mb-2\">Secure</h3><p>End-to-end encryption ensures your data remains private and protected.</p></div><div class=\"bg-gray-800 p-6 rounded-lg\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 mx-auto mb-4 text-green-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\"></path></svg><h3 class=\"text-xl font-semibold mb-2\">Easy to Use</h3><p>Simple command-line interface for quick setup and connection.</p></div><div class=\"bg-gray-800 p-6 rounded-lg\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-12 w-12 mx-auto mb-4 text-purple-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M17 8l4 4m0 0l-4 4m4-4H3\"></path></svg><h3 class=\"text-xl font-semibold mb-2\">Flexible</h3><p>Connect to various local services and ports with ease.</p></div></div></main><footer class=\"container mx-auto px-4 py-8 text-center text-gray-400\"><p>&copy; 2025 SSH Tunnel. All rights reserved.</p><div class=\"mt-4 flex justify-center space-x-4\"><a href=\"https://github.com/fossyy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"hover:text-white\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"></path></svg></a> <a href=\"mailto:bagas@fossy.my.id\" class=\"hover:text-white\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-6 w-6\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"></path></svg></a></div></footer><script>\r\n document.getElementById('copyButton').addEventListener('click', function() {\r\n const sshCommand = document.getElementById('sshCommand').textContent;\r\n navigator.clipboard.writeText(sshCommand).then(function() {\r\n alert('Command copied to clipboard!');\r\n }, function(err) {\r\n console.error('Could not copy text: ', err);\r\n });\r\n });\r\n </script></div>

View File

@ -1,21 +0,0 @@
package layout
templ Base(title string){
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Secure and reliable file hosting service. Upload, organize, and share your documents, images, videos, and more. Sign up now to keep your files always within reach." />
<meta name="keywords" content="file hosting, file sharing, cloud storage, data storage, secure file hosting, filekeeper, drive, mega" />
<meta name="author" content="Filekeeper" />
<link href="/public/output.css" rel="stylesheet"/>
<title>{ title }</title>
</head>
<body>
<div id="content">
{ children... }
</div>
</body>
</html>
}

View File

@ -1,61 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package layout
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Base(title string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"Secure and reliable file hosting service. Upload, organize, and share your documents, images, videos, and more. Sign up now to keep your files always within reach.\"><meta name=\"keywords\" content=\"file hosting, file sharing, cloud storage, data storage, secure file hosting, filekeeper, drive, mega\"><meta name=\"author\" content=\"Filekeeper\"><link href=\"/public/output.css\" rel=\"stylesheet\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 13, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title></head><body><div id=\"content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,3 +0,0 @@
<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"Secure and reliable file hosting service. Upload, organize, and share your documents, images, videos, and more. Sign up now to keep your files always within reach.\"><meta name=\"keywords\" content=\"file hosting, file sharing, cloud storage, data storage, secure file hosting, filekeeper, drive, mega\"><meta name=\"author\" content=\"Filekeeper\"><link href=\"/public/output.css\" rel=\"stylesheet\"><title>
</title></head><body><div id=\"content\">
</div></body></html>