Compare commits
22 Commits
37eafbe7d8
...
master
Author | SHA1 | Date |
---|---|---|
Evan Chen | ddf5e86cbc | |
Evan Chen | faf8300d8f | |
Evan Chen | f309009a76 | |
Evan Chen | 7aec30d473 | |
Evan Chen | 9728bfab58 | |
Evan Chen | 6828e6397a | |
Evan Chen | b6b0774531 | |
Evan Chen | 6a75a1e27f | |
Evan Chen | af0fc1b352 | |
Evan Chen | 2fecf640de | |
Evan Chen | edff8721cf | |
Evan Chen | 65f9eac352 | |
Evan | 4c5fe03d2d | |
Evan | 1da1d4c1f7 | |
Evan | deb22f8df1 | |
Evan Chen | 95bca77f37 | |
Evan Chen | 96209b847a | |
Evan Chen | a5e826e284 | |
Evan Chen | a3b2e0026d | |
Evan Chen | 9bb3c18df9 | |
Evan Chen | d379ffff9b | |
Evan Chen | 12084a659c |
|
@ -0,0 +1,20 @@
|
|||
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
|
|
@ -1 +1,3 @@
|
|||
node_modules
|
||||
.parcel-cache
|
||||
dist
|
41
README.md
41
README.md
|
@ -1 +1,40 @@
|
|||
# goshell
|
||||
# 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"
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
#!/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
|
|
@ -0,0 +1,122 @@
|
|||
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
7
go.mod
|
@ -1,15 +1,18 @@
|
|||
module kumoly.io/tools/goshell
|
||||
module kumoly.io/tools/gterm
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
kumoly.io/lib/klog v0.0.8
|
||||
github.com/rs/zerolog v1.26.0
|
||||
kumoly.io/lib/guard v0.1.1
|
||||
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
35
go.sum
|
@ -1,13 +1,48 @@
|
|||
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=
|
||||
|
|
|
@ -0,0 +1,362 @@
|
|||
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 >erm{
|
||||
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"`
|
||||
}
|
48
index.html
48
index.html
|
@ -1 +1,47 @@
|
|||
<!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>
|
||||
<!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>
|
95
main.go
95
main.go
|
@ -1,95 +0,0 @@
|
|||
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
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
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();
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,31 @@
|
|||
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
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
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/})";
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
.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}
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -2,5 +2,5 @@ package public
|
|||
|
||||
import "embed"
|
||||
|
||||
//go:embed js favicon.ico
|
||||
//go:embed *
|
||||
var FS embed.FS
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
.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
|
@ -0,0 +1 @@
|
|||
<!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>
|
|
@ -1,2 +0,0 @@
|
|||
(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
|
@ -1,3 +0,0 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
|
@ -1,17 +0,0 @@
|
|||
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'
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
.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?
|
|
@ -1,24 +0,0 @@
|
|||
# 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/).
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
|
@ -1,6 +0,0 @@
|
|||
package public
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed css js favicon.ico
|
||||
var FS embed.FS
|
|
@ -1,17 +0,0 @@
|
|||
<!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>
|
|
@ -1,17 +0,0 @@
|
|||
<template lang="pug">
|
||||
XTerm
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import XTerm from './components/XTerm.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
XTerm
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,46 +0,0 @@
|
|||
<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>
|
|
@ -1,4 +0,0 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
|
@ -1,8 +0,0 @@
|
|||
module.exports = {
|
||||
publicPath: "",
|
||||
outputDir: "../public",
|
||||
indexPath: "../index.html",
|
||||
devServer: {
|
||||
proxy: 'http://localhost:8000'
|
||||
}
|
||||
}
|
8951
src/yarn.lock
8951
src/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,16 @@
|
|||
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
|
||||
}
|
Loading…
Reference in New Issue