feat: connect terminal service to backend

feat: put terminal service into container

refactor: separate main into terminal and websocket

feat: create zsh using commander in home

feat: pass backendUrl to container

feat: pass token to ws server to cmd-hook

feat: gen terminal access token for frontend

feat: success to connect through traefik

fix: handle invalid jwt

feat: connect to session container via traefik
This commit is contained in:
Justin Lin committed 2025-05-05 19:53:33 +08:00
1 parent 0b08a9a5d9
commit 5e81db2759
20 files changed
+281 -209

No files matched your search

+2 -2
View File
@@ -4,8 +4,8 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
bin = "./bin/terminal-service"
cmd = "go build -o ./bin/terminal-service ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
+6
View File
@@ -0,0 +1,6 @@
package main
import _ "embed"
//go:embed index.html
var indexHTML []byte
+5 -1
View File
@@ -39,9 +39,13 @@
term.open(document.getElementById('terminal'))
fitAddon.fit()
const token = new URLSearchParams(window.location.search).get('token')
// Create WebSocket connection
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`)
const ws = new WebSocket(
`${protocol}//${window.location.host}${window.location.pathname}/ws?token=${token}`
)
// Handle WebSocket connection
ws.onopen = () => {
+20 -124
View File
@@ -2,136 +2,16 @@ package main
import (
"flag"
"io"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"time"
"github.com/creack/pty"
"github.com/gorilla/websocket"
)
var (
addr = flag.String("addr", ":8080", "http service address")
addr = flag.String("addr", ":9090", "http service address")
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Maximum message size allowed from peer.
maxMessageSize = 8192
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Time to wait before force close on connection.
closeGracePeriod = 10 * time.Second
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// Allow all origins for now - we'll restrict this in production
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type terminal struct {
cmd *exec.Cmd
pty *os.File
}
func newTerminal() (*terminal, error) {
// Start a new shell
cmd := exec.Command("bash")
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
}
return &terminal{
cmd: cmd,
pty: ptmx,
}, nil
}
func (t *terminal) Close() error {
if t.pty != nil {
t.pty.Close()
}
if t.cmd != nil {
t.cmd.Process.Kill()
t.cmd.Wait()
}
return nil
}
func pumpStdin(ws *websocket.Conn, w io.Writer) {
defer ws.Close()
ws.SetReadLimit(maxMessageSize)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := ws.ReadMessage()
if err != nil {
break
}
if _, err := w.Write(message); err != nil {
break
}
}
}
func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) {
defer func() {
}()
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if err != nil {
break
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, buf[:n]); err != nil {
ws.Close()
break
}
}
close(done)
ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
time.Sleep(closeGracePeriod)
ws.Close()
}
func ping(ws *websocket.Conn, done chan struct{}) {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
log.Println("ping:", err)
}
case <-done:
return
}
}
}
func internalError(ws *websocket.Conn, msg string, err error) {
log.Println(msg, err)
ws.WriteMessage(websocket.TextMessage, []byte("Internal server error."))
}
func serveWs(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
@@ -140,8 +20,14 @@ func serveWs(w http.ResponseWriter, r *http.Request) {
}
defer ws.Close()
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Create new terminal
term, err := newTerminal()
term, err := newTerminal(token)
if err != nil {
internalError(ws, "start:", err)
return
@@ -167,7 +53,15 @@ func serveHome(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
http.ServeFile(w, r, "index.html")
w.Header().Set("Content-Type", "text/html")
w.Write(indexHTML)
}
func serveHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
for _, e := range os.Environ() {
fmt.Fprintln(w, e)
}
}
func main() {
@@ -175,5 +69,7 @@ func main() {
log.SetFlags(0)
http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", serveWs)
http.HandleFunc("/health", serveHealth)
fmt.Println("Starting terminal service on", *addr)
log.Fatal(http.ListenAndServe(*addr, nil))
}
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"os"
"os/exec"
"github.com/creack/pty"
)
type terminal struct {
cmd *exec.Cmd
pty *os.File
}
func newTerminal(token string) (*terminal, error) {
// Start a new shell in the home directory
cmd := exec.Command("/usr/bin/zsh")
cmd.Dir = os.Getenv("HOME")
cmd.Env = append(os.Environ(), "ZDOTDIR=/etc/zsh", "TOKEN="+token)
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
}
return &terminal{
cmd: cmd,
pty: ptmx,
}, nil
}
func (t *terminal) Close() error {
if t.pty != nil {
t.pty.Close()
}
if t.cmd != nil {
t.cmd.Process.Kill()
t.cmd.Wait()
}
return nil
}
+95
View File
@@ -0,0 +1,95 @@
package main
import (
"io"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Maximum message size allowed from peer.
maxMessageSize = 8192
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Time to wait before force close on connection.
closeGracePeriod = 10 * time.Second
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// Allow all origins for now - we'll restrict this in production
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func pumpStdin(ws *websocket.Conn, w io.Writer) {
defer ws.Close()
ws.SetReadLimit(maxMessageSize)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := ws.ReadMessage()
if err != nil {
break
}
if _, err := w.Write(message); err != nil {
break
}
}
}
func pumpStdout(ws *websocket.Conn, r io.Reader, done chan struct{}) {
defer func() {
}()
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if err != nil {
break
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, buf[:n]); err != nil {
ws.Close()
break
}
}
close(done)
ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
time.Sleep(closeGracePeriod)
ws.Close()
}
func ping(ws *websocket.Conn, done chan struct{}) {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
log.Println("ping:", err)
}
case <-done:
return
}
}
}
func internalError(ws *websocket.Conn, msg string, err error) {
log.Println(msg, err)
ws.WriteMessage(websocket.TextMessage, []byte("Internal server error."))
}