mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
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:
1 parent
0b08a9a5d9
commit
5e81db2759
20 files changed
+281
-209
No files matched your search
@@ -48,7 +48,11 @@ func main() {
|
||||
|
||||
payload.Params = params
|
||||
|
||||
apiEndpoint := os.Getenv("API_ENDPOINT")
|
||||
backendUrl := os.Getenv("BACKEND_URL")
|
||||
if backendUrl == "" {
|
||||
fmt.Fprintf(os.Stderr, "BACKEND_URL is not set\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
token := os.Getenv("TOKEN")
|
||||
|
||||
jsonData, err := json.MarshalIndent(payload, "", " ")
|
||||
@@ -58,7 +62,7 @@ func main() {
|
||||
}
|
||||
|
||||
fmt.Println(string(jsonData))
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/commands", apiEndpoint), bytes.NewBuffer(jsonData))
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/commands", backendUrl), bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating request: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package main
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed index.html
|
||||
var indexHTML []byte
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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."))
|
||||
}
|
||||
Reference in new issue
Block a user