Compare commits

..

No commits in common. "master" and "37eafbe7d855f0bc4887cf3fd77af5ab868303cf" have entirely different histories.

42 changed files with 9258 additions and 5376 deletions

View File

@ -1,20 +0,0 @@
kind: pipeline
name: default
steps:
- name: build
image: golang:1.17.2
commands:
- git tag $DRONE_TAG
- bash make.sh
- echo -n "latest,${DRONE_TAG#v}" > .tags
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: gitea_api_key
base_url: https://kumoly.io
files: dist/*
checksum:
- sha256
trigger:
event: tag

2
.gitignore vendored
View File

@ -1,3 +1 @@
node_modules
.parcel-cache
dist

View File

@ -1,40 +1 @@
# gterm
## Usage
```shell
Usage: gterm [options]
-addr string
address to bind (default ":8000")
-allow string
restrict ip
-arg value
additional args to pass to cmd, multiple args can be passed, ex. -arg a -arg b
-dev
set the system to development mode
-dir string
the working dir that the shell will start from
-log-level int
log level, error:1 debug:2 warn:4 info:8 (default 9)
-name string
the application name (default "gterm")
-profile
print default profile, could be invoked with <(..)
-shell string
the shell to use (default "bash")
-v show version
```
### run bash with default profile
```shell
gterm -arg "--rcfile" -arg <(gterm -profile)
```
## Install
```shell
sudo rm -f /usr/local/bin/gterm
sudo sh -c "curl -fsSL RELEASE_URL | tar -C /usr/local/bin/ -xz"
```
# goshell

View File

@ -1,13 +0,0 @@
#!/bin/bash
VERSION=$(git describe --tags --abbrev=0)
BUILD=$(git rev-parse --short HEAD)
PROJ=gterm
DIST=dist
LDFLAGS="-ldflags \"-X main.Version=${VERSION} -X main.Build=${BUILD} -w -s\""
BIN_FILENAME="${PROJ}"
CMD="CGO_ENABLED=0 go build ${LDFLAGS} -o ${DIST}/${BIN_FILENAME} cmd/gterm/main.go"
echo "${CMD}"
eval $CMD

View File

@ -1,122 +0,0 @@
package main
import (
"flag"
"fmt"
"net"
"net/http"
"os"
"strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"kumoly.io/lib/guard"
"kumoly.io/tools/gterm"
)
type arrayFlags []string
func (i *arrayFlags) String() string {
return strings.Join(*i, ", ")
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, strings.TrimSpace(value))
return nil
}
var Version = "0.0.0"
var Build = "alpha"
var (
flagAllowIPNet string
flagAppName string
flagAddr string
flagShell string
flagDir string
flagLogLevel int
flagLogPretty bool
flagDev bool
flagVer bool
flagArgs arrayFlags
flagProfile bool
flagSalt string
flagUsr string
flagPasswd string
)
func init() {
flag.StringVar(&flagAppName, "name", "gterm", "the application name")
flag.StringVar(&flagAddr, "addr", ":8000", "address to bind")
flag.StringVar(&flagShell, "shell", "bash", "the shell to use")
flag.StringVar(&flagDir, "dir", "", "the working dir that the shell will start from")
flag.Var(&flagArgs, "arg", "additional args to pass to cmd, multiple args can be passed, ex. -arg a -arg b")
flag.BoolVar(&flagDev, "dev", false, "set the system to development mode")
flag.IntVar(&flagLogLevel, "log-level", 1, "log level [-1(trace):5(panic)] 7 to disable")
flag.StringVar(&flagAllowIPNet, "allow", "", "restrict ip in a specific ip net")
flag.BoolVar(&flagProfile, "profile", false, "print default profile, could be invoked with <(..)")
flag.StringVar(&flagSalt, "salt", "", "add salt to encoded keyparam")
flag.BoolVar(&flagVer, "v", false, "show version")
flag.BoolVar(&flagLogPretty, "pretty", false, "log message in human readable format (the original log is json)")
flag.StringVar(&flagUsr, "usr", "", "username, use basic auth for authentication if user and password are set")
flag.StringVar(&flagPasswd, "passwd", "", "password, use basic auth for authentication if user and password are set")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: gterm [options]\n")
flag.PrintDefaults()
}
}
func main() {
flag.Parse()
if flagVer {
fmt.Printf("%v - %v\n", Version, Build)
return
}
if flagProfile {
fmt.Println(gterm.BashProfile)
return
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.SetGlobalLevel(zerolog.Level(flagLogLevel))
if flagLogPretty {
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006/01/02 15:04:05",
})
}
if flagDev {
log.Logger = log.With().Caller().Logger()
}
log.Logger = log.With().Str("mod", "gtrem").Logger()
g := gterm.New()
g.AppName = flagAppName
g.Cmd = flagShell
g.Args = flagArgs
g.Dir = flagDir
g.Salt = flagSalt
gd := guard.New()
if flagAllowIPNet != "" {
_, ipnet, err := net.ParseCIDR(flagAllowIPNet)
if err != nil {
log.Panic().Err(err).Msg("")
}
gd.AllowIPNet = ipnet
}
if flagUsr != "" && flagPasswd != "" {
gd.SetBasicAuth(flagUsr, flagPasswd)
}
server := &http.Server{
Addr: flagAddr,
Handler: gd.Guard(g),
}
log.Info().Msgf("gterm starting at %s", flagAddr)
err := server.ListenAndServe()
if err != nil {
panic(err)
}
}

7
go.mod
View File

@ -1,18 +1,15 @@
module kumoly.io/tools/gterm
module kumoly.io/tools/goshell
go 1.17
require (
github.com/creack/pty v1.1.17
github.com/gorilla/websocket v1.4.2
github.com/rs/zerolog v1.26.0
kumoly.io/lib/guard v0.1.1
kumoly.io/lib/klog v0.0.8
kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298
kumoly.io/lib/xorencrypt v0.1.0
)
require (
github.com/mattn/go-isatty v0.0.14 // indirect
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
kumoly.io/lib/klog v0.0.8 // indirect
)

35
go.sum
View File

@ -1,48 +1,13 @@
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
kumoly.io/lib/guard v0.1.0 h1:PvFM0bcWbgfZQv4QfRwDmrK9FAjv9QYwfJY3Ffg6JA0=
kumoly.io/lib/guard v0.1.0/go.mod h1:yWg9RDSI6YXkOPmP6Ad93aMqzlxhgW8LOe/ZRjjYX3U=
kumoly.io/lib/guard v0.1.1 h1:aUcn0qVtX6TqRhp7bWSjIRlbWRWtuK0cjCArILTbAcY=
kumoly.io/lib/guard v0.1.1/go.mod h1:yWg9RDSI6YXkOPmP6Ad93aMqzlxhgW8LOe/ZRjjYX3U=
kumoly.io/lib/klog v0.0.8 h1:6hTfDlZh7KGnPrd2tUrauCKRImSnyyN9DHXpey3Czn8=
kumoly.io/lib/klog v0.0.8/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M=
kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298 h1:0raqoIXmNpD6s1SrJbieAyIIkDyhe+aqfaXvx8wenrI=
kumoly.io/lib/ksrv v0.0.2-0.20211112060911-0d61b343a298/go.mod h1:pwd+NspxnoxPJAETRY2V4i2qZc+orKLxvWzGUBiqBW8=
kumoly.io/lib/xorencrypt v0.1.0 h1:VssGocaBAPyLn+QURVY8FdFYRBmwFr26z7ame0zwV44=
kumoly.io/lib/xorencrypt v0.1.0/go.mod h1:+L3JtdD/CTlcXvE8X7AvOJBVbN16qvnxxv3zIWPZgQM=

362
gterm.go
View File

@ -1,362 +0,0 @@
package gterm
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"strings"
"time"
"github.com/creack/pty"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
"kumoly.io/lib/ksrv"
"kumoly.io/lib/ksrv/engine"
"kumoly.io/lib/xorencrypt"
"kumoly.io/tools/gterm/public"
)
//go:embed public/index.html
var index string
//go:embed profile
var BashProfile string
var tmpl *engine.Engine
var servePublic = http.FileServer(http.FS(public.FS))
var pool map[string]chan struct{}
var locks map[string]chan struct{}
func init() {
tmpl = engine.Must(engine.New("").Parse(index))
pool = map[string]chan struct{}{}
locks = map[string]chan struct{}{}
}
type GTerm struct {
AppName string
// Arguments is a list of strings to pass as arguments to the specified Command
Args []string
// Command is the path to the binary we should create a TTY for
Cmd string
// Dir working dir
Dir string
// Envs env pairs to pass to the command
Envs []string
// ErrorLimit defines the number of consecutive errors that can happen
// before a connection is considered unusable
ErrorLimit int
// Timeout defines the maximum duration between which a ping and pong
// cycle should be tolerated, beyond this the connection should be deemed dead
Timeout time.Duration
BufferSize int
Salt string
}
func New() *GTerm {
// if g.Cmd == "" {
// g.Cmd = "bash"
// }
// if len(g.Envs) == 0 {
// g.Envs = os.Environ()
// }
// if g.Timeout < time.Second {
// g.Timeout = 20 * time.Second
// }
// if g.ErrorLimit == 0 {
// g.ErrorLimit = 10
// }
// if g.BufferSize == 0 {
// g.BufferSize = 512
// }
go func() {
//print pool
for {
cons := []string{}
for k := range pool {
cons = append(cons, k)
}
log.Info().Interface("connections", cons).Msg("")
time.Sleep(5 * time.Minute)
}
}()
return &GTerm{
AppName: "GTERM",
Cmd: "bash",
Envs: append(os.Environ(), "TERM=xterm-256color"),
Timeout: 20 * time.Second,
ErrorLimit: 10,
BufferSize: 512,
}
}
func (g *GTerm) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.String(), "/ws") {
g.WS(w, r)
} else {
g.App(w, r)
}
}
type App struct {
AppName string
}
func (g *GTerm) App(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
tmpl.Execute(w, App{g.AppName})
return
}
file, err := public.FS.Open(strings.TrimPrefix(r.URL.String(), "/"))
if err != nil {
// tmpl.Execute(w, App{g.AppName})
http.NotFound(w, r)
return
}
stat, err := file.Stat()
if err != nil || stat.IsDir() {
// tmpl.Execute(w, App{g.AppName})
http.NotFound(w, r)
return
}
servePublic.ServeHTTP(w, r)
}
type NewCmdRequest struct {
Cmd string `json:"cmd"`
Envs []string `json:"envs"`
Args []string `json:"args"`
Dir string `json:"dir"`
Block bool `json:"block"`
}
func (g *GTerm) defaultCmd() *exec.Cmd {
cmd := exec.Command(g.Cmd, g.Args...)
cmd.Env = g.Envs
if g.Dir != "" && g.Dir != "." {
cmd.Dir = g.Dir
}
return cmd
}
func (g *GTerm) echo(msg string) *exec.Cmd {
cmd := exec.Command("echo", msg)
cmd.Env = g.Envs
return cmd
}
func (g *GTerm) WS(w http.ResponseWriter, r *http.Request) {
id := fmt.Sprintf("[%02d] %s", ctr(), ksrv.GetIP(r))
pool[id] = make(chan struct{}, 1)
defer func() {
close(pool[id])
delete(pool, id)
}()
l := log.With().Str("id", id).Logger()
var cmd *exec.Cmd
if newCmd := r.URL.Query().Get("cmd"); newCmd != "" {
param, _ := xorencrypt.Decrypt(newCmd, g.Salt)
req := &NewCmdRequest{}
err := json.Unmarshal([]byte(param), req)
if err != nil {
l.Error().Err(err).Str("base", newCmd).Str("decrypt", param).Msg("cmd decode failed")
cmd = g.echo("cmd decode failed")
} else {
cmd = exec.Command(req.Cmd, req.Args...)
cmd.Env = append(req.Envs, "TERM=xterm-256color")
cmd.Dir = req.Dir
if req.Block {
lock, ok := locks[newCmd]
if !ok {
locks[newCmd] = make(chan struct{}, 1)
}
if len(lock) == 1 {
cmd = g.echo("cmd already running")
} else {
l.Info().Interface("cmd", req).Msg("starting cmd")
locks[newCmd] <- struct{}{}
defer func() {
<-locks[newCmd]
close(locks[newCmd])
delete(locks, newCmd)
}()
}
}
}
} else {
cmd = g.defaultCmd()
}
upgrader := websocket.Upgrader{
HandshakeTimeout: 0,
ReadBufferSize: g.BufferSize,
WriteBufferSize: g.BufferSize,
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
l.Error().Err(err).Msg("upgrade error")
return
}
l.Info().Msg("connection established.")
tty, err := pty.Start(cmd)
if err != nil {
l.Error().Err(err).Msg("start tty error")
conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
defer func() {
if err := cmd.Process.Kill(); err != nil {
l.Error().Err(err).Msg("proccess kill error")
}
if _, err := cmd.Process.Wait(); err != nil {
l.Error().Err(err).Msg("proccess wait error")
}
if err := tty.Close(); err != nil {
l.Error().Err(err).Msg("tty close error")
}
if err := conn.Close(); err != nil {
l.Error().Err(err).Msg("conn close error")
}
}()
var connectionClosed bool
waiter := make(chan struct{}, 1)
// this is a keep-alive loop that ensures connection does not hang-up itself
lastPongTime := time.Now()
conn.SetPongHandler(func(msg string) error {
lastPongTime = time.Now()
return nil
})
go func() {
for {
if err := conn.WriteMessage(websocket.PingMessage, []byte("keepalive")); err != nil {
l.Warn().Msg("failed to write ping message")
return
}
time.Sleep(g.Timeout / 2)
if time.Since(lastPongTime) > g.Timeout {
l.Warn().Msg("failed to get response from ping, triggering disconnect now...")
waiter <- struct{}{}
return
}
l.Debug().Msg("received response from ping successfully")
}
}()
// tty >> xterm.js
go func() {
errorCounter := 0
for {
// consider the connection closed/errored out so that the socket handler
// can be terminated - this frees up memory so the service doesn't get
// overloaded
if errorCounter > g.ErrorLimit {
waiter <- struct{}{}
break
}
buffer := make([]byte, g.BufferSize)
readLength, err := tty.Read(buffer)
if err != nil {
l.Warn().Err(err).Msg("failed to read from tty")
if err := conn.WriteMessage(websocket.TextMessage, []byte("bye!")); err != nil {
l.Warn().Err(err).Msg("failed to send termination message from tty to xterm.js")
}
waiter <- struct{}{}
return
}
if err := conn.WriteMessage(websocket.BinaryMessage, buffer[:readLength]); err != nil {
l.Warn().Msgf("failed to send %s bytes from tty to xterm.js", readLength)
errorCounter++
continue
}
l.Debug().Msgf("sent message of size %s bytes from tty to xterm.js", readLength)
errorCounter = 0
}
}()
// tty << xterm.js
go func() {
for {
// data processing
messageType, data, err := conn.ReadMessage()
if err != nil {
if !connectionClosed {
l.Warn().Err(err).Msg("failed to get next reader")
}
return
}
dataLength := len(data)
dataBuffer := bytes.Trim(data, "\x00")
dataType, ok := WebsocketMessageType[messageType]
if !ok {
dataType = "uunknown"
}
l.Debug().Msgf("received %s (type: %v) message of size %v byte(s) from xterm.js with key sequence: %v", dataType, messageType, dataLength, dataBuffer)
// process
if dataLength == -1 { // invalid
l.Warn().Msg("failed to get the correct number of bytes read, ignoring message")
continue
}
// handle resizing
if messageType == websocket.BinaryMessage {
if dataBuffer[0] == 1 {
ttySize := &TTYSize{}
resizeMessage := bytes.Trim(dataBuffer[1:], " \n\r\t\x00\x01")
if err := json.Unmarshal(resizeMessage, ttySize); err != nil {
l.Warn().Err(err).Msgf("failed to unmarshal received resize message '%s'", string(resizeMessage))
continue
}
l.Debug().Int("rows", int(ttySize.Rows)).Int("columns", int(ttySize.Cols)).Msg("resizing tty...")
if err := pty.Setsize(tty, &pty.Winsize{
Rows: ttySize.Rows,
Cols: ttySize.Cols,
}); err != nil {
l.Warn().Err(err).Msg("failed to resize tty")
}
continue
}
}
// write to tty
bytesWritten, err := tty.Write(dataBuffer)
if err != nil {
l.Warn().Err(err).Msgf("failed to write %v bytes to tty", len(dataBuffer))
continue
}
l.Debug().Msgf("%d bytes written to tty...", bytesWritten)
}
}()
<-waiter
close(waiter)
l.Info().Msg("closing connection...")
connectionClosed = true
}
var WebsocketMessageType = map[int]string{
websocket.BinaryMessage: "binary",
websocket.TextMessage: "text",
websocket.CloseMessage: "close",
websocket.PingMessage: "ping",
websocket.PongMessage: "pong",
}
// TTYSize represents a JSON structure to be sent by the frontend
// xterm.js implementation to the xterm.js websocket handler
type TTYSize struct {
Cols uint16 `json:"cols"`
Rows uint16 `json:"rows"`
X uint16 `json:"x"`
Y uint16 `json:"y"`
}

View File

@ -1,47 +1 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.AppName}}</title>
<style>
html::-webkit-scrollbar,
body::-webkit-scrollbar,
div::-webkit-scrollbar {
display: none;
width: 0;
}
html,
body {
margin: 0;
overflow: hidden;
padding: 0;
}
div#terminal {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
div#terminal div {
height: 100%;
}
.xterm-viewport,
.xterm-screen {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="terminal"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>{{.AppName}}</title><link href="css/app.0e433876.css" rel="preload" as="style"><link href="css/chunk-vendors.78de0e90.css" rel="preload" as="style"><link href="js/app.5bd01a68.js" rel="preload" as="script"><link href="js/chunk-vendors.3d58276e.js" rel="preload" as="script"><link href="css/chunk-vendors.78de0e90.css" rel="stylesheet"><link href="css/app.0e433876.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but {{.AppName}} doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.3d58276e.js"></script><script src="js/app.5bd01a68.js"></script></body></html>

95
main.go Normal file
View File

@ -0,0 +1,95 @@
package main
import (
_ "embed"
"flag"
"net/http"
"os"
"os/exec"
"strings"
"github.com/creack/pty"
"github.com/gorilla/websocket"
"kumoly.io/lib/klog"
"kumoly.io/lib/ksrv"
"kumoly.io/lib/ksrv/engine"
"kumoly.io/tools/goshell/public"
)
//go:embed index.html
var index string
var tmpl *engine.Engine
var servePublic = http.FileServer(http.FS(public.FS))
var (
flagAppName string
flagAddr string
flagShell string
)
func init() {
flag.StringVar(&flagAddr, "addr", ":8000", "address to bind")
flag.StringVar(&flagShell, "shell", "bash", "the shell behind")
}
func main() {
flag.Parse()
server := ksrv.New()
mux := http.NewServeMux()
tmpl = engine.Must(engine.New("").Parse(index))
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
if r.URL.String() == "/" {
tmpl.Execute(rw, App{flagAppName})
return
}
file, err := public.FS.Open(strings.TrimPrefix(r.URL.String(), "/"))
if err != nil {
klog.Debug(err)
tmpl.Execute(rw, App{flagAppName})
return
}
stat, err := file.Stat()
if err != nil || stat.IsDir() {
klog.Debug(err)
tmpl.Execute(rw, App{flagAppName})
return
}
servePublic.ServeHTTP(rw, r)
})
server.Handle(mux).Listen(flagAddr).Serve()
}
type App struct {
AppName string
}
type windowSize struct {
Rows uint16 `json:"rows"`
Cols uint16 `json:"cols"`
X uint16
Y uint16
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func handleWebsocket(w http.ResponseWriter, r *http.Request) {
l := klog.Sub(ksrv.GetIP(r))
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
l.Error("Unable to upgrade connection, err: ", err)
return
}
cmd := exec.Command("/bin/bash", "-l")
cmd.Env = append(os.Environ(), "TERM=xterm")
pty.Winsize
}

60
main.js
View File

@ -1,60 +0,0 @@
import { Terminal } from 'xterm'
import { AttachAddon } from 'xterm-addon-attach';
import { FitAddon } from 'xterm-addon-fit';
import { SerializeAddon } from "xterm-addon-serialize";
import { Unicode11Addon } from 'xterm-addon-unicode11';
import { WebLinksAddon } from 'xterm-addon-web-links';
import 'xterm/css/xterm.css'
(function() {
const terminal = new Terminal({
// screenKeys: true,
// useStyle: true,
// cursorBlink: true,
// fullscreenWin: true,
// maximizeWin: true,
// screenReaderMode: true,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
var protocol = (location.protocol === "https:") ? "wss://" : "ws://";
var url = protocol + location.host + location.pathname + "ws" + location.search
const ws = new WebSocket(url);
const attachAddon = new AttachAddon(ws);
const webLinksAddon = new WebLinksAddon();
terminal.loadAddon(webLinksAddon);
const unicode11Addon = new Unicode11Addon();
terminal.loadAddon(unicode11Addon);
const serializeAddon = new SerializeAddon();
terminal.loadAddon(serializeAddon);
terminal.open(document.getElementById("terminal"));
ws.onclose = function(event) {
console.log(event);
terminal.write('\r\n\nconnection has been terminated from the server-side (hit refresh to restart)\n')
};
ws.onopen = function() {
terminal.loadAddon(attachAddon);
terminal._initialized = true;
terminal.focus();
setTimeout(function() {fitAddon.fit()});
document.addEventListener('keypress',(e)=>{
e.preventDefault();
})
terminal.onResize(function(event) {
var rows = event.rows;
var cols = event.cols;
var size = JSON.stringify({cols: cols, rows: rows + 1});
var send = new TextEncoder().encode("\x01" + size);
console.log('resizing to', size);
ws.send(send);
});
terminal.onTitleChange(function(event) {
console.log(event);
});
window.onresize = function() {
console.log("resize")
fitAddon.fit();
};
fitAddon.fit();
};
})();

31
make.sh
View File

@ -1,31 +0,0 @@
VERSION=$(git describe --tags --abbrev=0)
if [ $? -ne 0 ]; then VERSION=$DRONE_TAG; fi
BUILD=$(git rev-parse --short HEAD)
if [ $? -ne 0 ]; then BUILD=${DRONE_COMMIT:0:7}; fi
PROJ=gterm
DIST=dist
LDFLAGS="-ldflags \"-X main.Version=${VERSION} -X main.Build=${BUILD} -w -s\""
FAILURES=""
PLATFORMS="darwin/amd64 darwin/arm64"
PLATFORMS="$PLATFORMS linux/amd64"
PLATFORMS="$PLATFORMS linux/s390x"
for PLATFORM in $PLATFORMS; do
GOOS=${PLATFORM%/*}
GOARCH=${PLATFORM#*/}
BIN_FILENAME="${PROJ}"
CMD="CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build ${LDFLAGS} -o ${DIST}/${BIN_FILENAME} cmd/gterm/main.go"
echo "${CMD}"
eval $CMD || FAILURES="${FAILURES} ${PLATFORM}"
sh -c "cd ${DIST} && tar -czf ${PROJ}-${VERSION}-${GOOS}-${GOARCH}.tar.gz ${BIN_FILENAME} && rm ${BIN_FILENAME}"
done
if [[ "${FAILURES}" != "" ]]; then
echo ""
echo "${SCRIPT_NAME} failed on: ${FAILURES}"
exit 1
fi

View File

@ -1,26 +0,0 @@
{
"name": "gterm",
"version": "1.0.0",
"repository": "git@kumoly.io:tools/gterm.git",
"author": "Evan Chen <evanchen@kumoly.io>",
"license": "MIT",
"scripts": {
"build":"rm public/*.js public/*.css && yarn parcel build index.html"
},
"dependencies": {
"parcel": "^2.0.1",
"xterm": "^4.15.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-serialize": "^0.6.1",
"xterm-addon-unicode11": "^0.3.0",
"xterm-addon-web-links": "^0.4.0"
},
"targets":{
"default":{
"distDir":"public",
"publicUrl":"./",
"sourceMap": false
}
}
}

31
profile
View File

@ -1,31 +0,0 @@
export LSCOLORS="gxfxcxdxbxegedabagacad"
export CLICOLOR=1
export TERM="xterm-256color"
PS1='\[\e[0;33m\]\u\[\e[0m\]@\[\e[0;32m\]\h\[\e[0m\]:\[\033[01;34m\]\w\[\033[00m\]$(git_info)\[\033[00m\]\n\[\033[1;31m\]\$ \[\033[00m\]'
# functions
function git_info {
ref=$(git symbolic-ref HEAD 2> /dev/null) || return;
# Check for uncommitted changes in the index
if ! $(git diff --quiet --ignore-submodules --cached); then
uc=" $(tput setaf 64)+"
fi
# Check for unstaged changes
if ! $(git diff-files --quiet --ignore-submodules --); then
us=" $(tput setaf 124)!"
fi
# Check for untracked files
if [ -n "$(git ls-files --others --exclude-standard)" ]; then
ut=" $(tput setaf 166)?"
fi
# Check for stashed files
if $(git rev-parse --verify refs/stash &>/dev/null); then
st=" $(tput setaf 136)$"
fi
echo " ($(tput bold)${ref#refs/heads/}$uc$us$ut$st$(tput sgr0)$(tput setaf 254))";
# echo "(${ref#refs/heads/})";
}

View File

View File

@ -0,0 +1 @@
.xterm{position:relative;-moz-user-select:none;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm{cursor:text}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:.5}.xterm-underline{text-decoration:underline}.xterm-strikethrough{text-decoration:line-through}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -2,5 +2,5 @@ package public
import "embed"
//go:embed *
//go:embed js favicon.ico
var FS embed.FS

View File

@ -1 +0,0 @@
.xterm{position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{border:0;height:0;left:-9999em;margin:0;opacity:0;overflow:hidden;padding:0;position:absolute;resize:none;top:0;white-space:nowrap;width:0;z-index:-5}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;bottom:0;cursor:default;left:0;overflow-y:scroll;position:absolute;right:0;top:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{left:0;position:absolute;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;left:-9999em;line-height:normal;position:absolute;top:0;visibility:hidden}.xterm{cursor:text}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{bottom:0;color:transparent;left:0;position:absolute;right:0;top:0;z-index:10}.xterm .live-region{height:1px;left:-9999px;overflow:hidden;position:absolute;width:1px}.xterm-dim{opacity:.5}.xterm-underline{text-decoration:underline}.xterm-strikethrough{text-decoration:line-through}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
<!DOCTYPE html><html><head><link rel="stylesheet" href="index.6999253a.css"><title>{{.AppName}}</title><style>body::-webkit-scrollbar,div::-webkit-scrollbar,html::-webkit-scrollbar{display:none;width:0}body,html{margin:0;overflow:hidden;padding:0}div#terminal{height:100%;left:0;position:absolute;top:0;width:100%}div#terminal div{height:100%}.xterm-screen,.xterm-viewport{height:100%;margin:0;padding:0}</style></head><body> <div id="terminal"></div> <script type="module" src="index.a2a226ad.js"></script> </body></html>

View File

@ -0,0 +1,2 @@
(function(e){function t(t){for(var r,c,a=t[0],i=t[1],p=t[2],l=0,s=[];l<a.length;l++)c=a[l],Object.prototype.hasOwnProperty.call(o,c)&&o[c]&&s.push(o[c][0]),o[c]=0;for(r in i)Object.prototype.hasOwnProperty.call(i,r)&&(e[r]=i[r]);f&&f(t);while(s.length)s.shift()();return u.push.apply(u,p||[]),n()}function n(){for(var e,t=0;t<u.length;t++){for(var n=u[t],r=!0,a=1;a<n.length;a++){var i=n[a];0!==o[i]&&(r=!1)}r&&(u.splice(t--,1),e=c(c.s=n[0]))}return e}var r={},o={app:0},u=[];function c(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,c),n.l=!0,n.exports}c.m=e,c.c=r,c.d=function(e,t,n){c.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},c.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.t=function(e,t){if(1&t&&(e=c(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(c.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)c.d(n,r,function(t){return e[t]}.bind(null,r));return n},c.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return c.d(t,"a",t),t},c.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},c.p="";var a=window["webpackJsonp"]=window["webpackJsonp"]||[],i=a.push.bind(a);a.push=t,a=a.slice();for(var p=0;p<a.length;p++)t(a[p]);var f=i;u.push([0,"chunk-vendors"]),n()})({0:function(e,t,n){e.exports=n("56d7")},"0ae4":function(e,t,n){},"56d7":function(e,t,n){"use strict";n.r(t);n("e260"),n("e6cf"),n("cca6"),n("a79d");var r=n("7a23");function o(e,t,n,o,u,c){var a=Object(r["e"])("XTerm");return Object(r["d"])(),Object(r["b"])(a)}function u(e,t,n,o,u,c){return Object(r["d"])(),Object(r["c"])("h1",null,"TEST")}var c={},a=(n("9a49"),n("6b0d")),i=n.n(a);const p=i()(c,[["render",u]]);var f=p,l={name:"App",components:{XTerm:f}};const s=i()(l,[["render",o]]);var d=s;Object(r["a"])(d).mount("#app")},"9a49":function(e,t,n){"use strict";n("0ae4")}});
//# sourceMappingURL=app.5bd01a68.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
src/.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

17
src/.eslintrc.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

23
src/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
src/README.md Normal file
View File

@ -0,0 +1,24 @@
# src
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
src/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

29
src/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "src",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0",
"xterm": "^4.15.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-search": "^0.8.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"vue-cli-plugin-pug": "~2.0.0"
}
}

BIN
src/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

6
src/public/fs.go Normal file
View File

@ -0,0 +1,6 @@
package public
import "embed"
//go:embed css js favicon.ico
var FS embed.FS

17
src/public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>{{.AppName}}</title>
</head>
<body>
<noscript>
<strong>We're sorry but {{.AppName}} doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

17
src/src/App.vue Normal file
View File

@ -0,0 +1,17 @@
<template lang="pug">
XTerm
</template>
<script>
import XTerm from './components/XTerm.vue'
export default {
name: 'App',
components: {
XTerm
}
}
</script>
<style lang="scss">
</style>

BIN
src/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,46 @@
<template lang="pug">
.console#terminal
</template>
<script>
import {Terminal} from 'xterm'
import {FitAddon} from 'xterm-addon-fit'
export default {
data () {
return {
term: null,
terminalSocket: null,
fitaddon: null
}
},
mounted(){
console.log("mounted")
let terminalContainer = document.getElementById('terminal')
this.term = new Terminal()
this.fitaddon = new FitAddon()
this.term.loadAddon(this.fitaddon)
this.term.open(terminalContainer)
this.fitaddon.fit()
this.term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
console.log(this.term)
},
Unmounted () {
this.term.dispose()
},
methods:{
}
}
</script>
<style lang="scss">
@import '~xterm/css/xterm.css';
.console {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>

4
src/src/main.js Normal file
View File

@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

8
src/vue.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
publicPath: "",
outputDir: "../public",
indexPath: "../index.html",
devServer: {
proxy: 'http://localhost:8000'
}
}

8951
src/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

16
util.go
View File

@ -1,16 +0,0 @@
package gterm
import "sync"
var curCtr = 0
var ctrLck sync.Mutex
func ctr() int {
ctrLck.Lock()
defer ctrLck.Unlock()
if curCtr >= 99 {
curCtr = 0
}
curCtr++
return curCtr
}

4561
yarn.lock

File diff suppressed because it is too large Load Diff