├── .github └── workflows │ └── go-cross-build.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_cn.md ├── cmd └── web_console │ ├── README.md │ └── main.go ├── docker ├── init.go └── session_docker.go ├── driver.go ├── go.mod ├── go.sum ├── router └── router.go ├── session.go ├── shell ├── init.go ├── session_shell_notwin.go └── session_shell_windows.go ├── ssh ├── init.go └── session_ssh.go └── static ├── Makefile ├── css └── xterm.min.css ├── index.html ├── js ├── attach.min.js ├── fit.min.js ├── webLinks.min.js ├── winptyCompat.min.js ├── xterm.min.js └── zepto.min.js ├── robot.txt └── web.go /.github/workflows/go-cross-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: 1.22 16 | - name: Build Cross Platform 17 | uses: wzshiming/action-go-build-cross-plantform@v1 18 | - name: Upload Release Assets 19 | uses: wzshiming/action-upload-release-assets@v1 20 | env: 21 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Log into registry 23 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin 24 | - name: Upload Release Images 25 | uses: wzshiming/action-upload-release-images@v1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | notifications: 4 | email: false 5 | jobs: 6 | include: 7 | - go: 1.13.x 8 | stage: deploy 9 | go_import_path: github.com/wzshiming/console 10 | install: skip 11 | script: skip 12 | before_deploy: 13 | - BASENAME=web_console bash -c "$(curl -fsSL https://github.com/wzshiming/my-shell/raw/master/build_all.bash)" 14 | deploy: 15 | provider: releases 16 | api_key: $CI_TOKEN 17 | file_glob: true 18 | file: release/* 19 | skip_cleanup: true 20 | on: 21 | repo: wzshiming/console 22 | branch: master 23 | tags: true 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | WORKDIR /tmp/gopath/src/github.com/wzshiming/console 3 | COPY . . 4 | ENV GOPATH=/tmp/gopath/ 5 | ENV GOBIN=$GOPATH/bin/ 6 | RUN CGO_ENABLED=0 go install github.com/wzshiming/console/cmd/web_console 7 | 8 | FROM alpine 9 | LABEL maintainer="wzshiming@foxmail.com" 10 | COPY --from=builder /tmp/gopath/bin/web_console /usr/local/bin/ 11 | VOLUME /var/run/docker.sock 12 | EXPOSE 8888 13 | ENTRYPOINT [ "web_console" ] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-NOW wzshiming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | run: 3 | go run ./cmd/web_console 4 | 5 | generate: install_tools 6 | go generate ./... 7 | 8 | fmt: 9 | go fmt ./... 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Console 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/wzshiming/console)](https://goreportcard.com/report/github.com/wzshiming/console) 4 | [![GoDoc](https://godoc.org/github.com/wzshiming/console?status.svg)](https://godoc.org/github.com/wzshiming/console) 5 | [![GitHub license](https://img.shields.io/github/license/wzshiming/console.svg)](https://github.com/wzshiming/console/blob/master/LICENSE) 6 | 7 | - [English](https://github.com/wzshiming/console/blob/master/README.md) 8 | - [简体中文](https://github.com/wzshiming/console/blob/master/README_cn.md) 9 | 10 | ## Driver 11 | - [x] Docker - Docker client 12 | - [x] SSH - SSH client 13 | - [x] Shell - Local shell 14 | 15 | ## GUI 16 | - [x] [Web console](https://github.com/wzshiming/console/blob/master/cmd/web_console/) - Web Shell 17 | - [ ] PC console - Build with [electron](https://github.com/electron/electron) 18 | - [ ] Config Management - Connection configuration management 19 | - [ ] Session Management - Multi session management 20 | 21 | ## Misc 22 | - [ ] Proxy - Network Proxy 23 | - [ ] Auth - Authentication 24 | 25 | ## License 26 | 27 | Licensed under the MIT License. See [LICENSE](https://github.com/wzshiming/console/blob/master/LICENSE) for the full license text. 28 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # Console 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/wzshiming/console)](https://goreportcard.com/report/github.com/wzshiming/console) 4 | [![GoDoc](https://godoc.org/github.com/wzshiming/console?status.svg)](https://godoc.org/github.com/wzshiming/console) 5 | [![GitHub license](https://img.shields.io/github/license/wzshiming/console.svg)](https://github.com/wzshiming/console/blob/master/LICENSE) 6 | 7 | - [English](https://github.com/wzshiming/console/blob/master/README.md) 8 | - [简体中文](https://github.com/wzshiming/console/blob/master/README_cn.md) 9 | 10 | ## 驱动 11 | - [x] Docker - Docker 客户端 12 | - [x] SSH - SSH 客户端 13 | - [x] Shell - 本地 shell 14 | 15 | ## GUI 16 | - [x] [Web console](https://github.com/wzshiming/console/blob/master/cmd/web_console/) - Web Shell 17 | - [ ] PC console - 使用[electron](https://github.com/electron/electron)构建 18 | - [ ] Config Management - 连接配置管理 19 | - [ ] Session Management - 多会话管理 20 | 21 | ## 其他 22 | - [ ] Proxy - 网络代理 23 | - [ ] Auth - 权限 24 | 25 | ## 许可证 26 | 27 | 软包根据MIT License。有关完整的许可证文本,请参阅[LICENSE](https://github.com/wzshiming/console/blob/master/LICENSE)。 28 | -------------------------------------------------------------------------------- /cmd/web_console/README.md: -------------------------------------------------------------------------------- 1 | # web console 2 | 3 | ## Download & Install 4 | 5 | ``` bash 6 | go get -u -v github.com/wzshiming/console/cmd/web_console 7 | ``` 8 | or 9 | ``` 10 | docker pull wzshiming/web_console 11 | ``` 12 | 13 | ## Start 14 | 15 | ``` bash 16 | $(go env GOBIN)/web_console 17 | ``` 18 | or 19 | ``` bash 20 | docker run -it --rm -p 8888:8888 wzshiming/web_console 21 | ``` 22 | 23 | ### Shell 24 | 25 | > http://localhost:8888/?name=shell&cmd={cmd} 26 | 27 | ### SSH 28 | 29 | > http://localhost:8888/?name=ssh&host=ssh://{username}:{password}@{domain}:{port} 30 | 31 | ### Docker 32 | 33 | > http://localhost:8888/?name=docker&host=tcp://localhost:2375&cid={Container}&cmd={cmd} 34 | or 35 | > http://localhost:8888/?name=docker&host=unix:///var/run/docker.sock&cid={Container}&cmd={cmd} 36 | 37 | ## License 38 | 39 | Pouch is licensed under the MIT License. See [LICENSE](https://github.com/wzshiming/console/blob/master/LICENSE) for the full license text. -------------------------------------------------------------------------------- /cmd/web_console/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | 10 | "github.com/gorilla/handlers" 11 | "github.com/gorilla/mux" 12 | "github.com/wzshiming/console" 13 | "github.com/wzshiming/console/router" 14 | "github.com/wzshiming/console/static" 15 | 16 | _ "github.com/wzshiming/console/docker" 17 | _ "github.com/wzshiming/console/shell" 18 | _ "github.com/wzshiming/console/ssh" 19 | ) 20 | 21 | var port = flag.Int("p", 8888, "Listen port") 22 | var ip = flag.String("ip", "0.0.0.0", "Listen ip") 23 | var name = flag.String("name", "shell", "Driver name, shell docker and ssh") 24 | var host = flag.String("host", "", "Connect to the host address, It has to be docker or SSH Driver") 25 | var cid = flag.String("cid", "", "Docker Container id, It has to be docker Driver") 26 | var cmd = flag.String("cmd", 27 | func() string { 28 | if runtime.GOOS != "windows" { 29 | return "sh" 30 | } 31 | return "cmd" 32 | }(), 33 | "command to execute") 34 | var disable = flag.Bool("d", false, "Disable url parameters") 35 | 36 | func init() { 37 | flag.Parse() 38 | } 39 | 40 | func main() { 41 | // web 42 | mux0 := mux.NewRouter() 43 | 44 | mux0.PathPrefix("/api").Handler(http.StripPrefix("/api", router.ExecRouter(*disable, &console.ReqCreateExec{ 45 | Name: *name, 46 | Host: *host, 47 | CId: *cid, 48 | Cmd: *cmd, 49 | }))) 50 | 51 | mux0.PathPrefix("/").Handler(http.FileServerFS(static.Web)) 52 | 53 | var mux http.Handler = mux0 54 | 55 | mux = handlers.RecoveryHandler()(mux) 56 | mux = handlers.CombinedLoggingHandler(os.Stderr, mux) 57 | p := fmt.Sprintf("%v:%v", *ip, *port) 58 | fmt.Printf("Open http://%s/ with your browser.\n", p) 59 | err := http.ListenAndServe(p, mux) 60 | if err != nil { 61 | fmt.Println(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docker/init.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "github.com/wzshiming/console" 5 | ) 6 | 7 | func init() { 8 | console.Register("docker", NewDockerSessions) 9 | } 10 | -------------------------------------------------------------------------------- /docker/session_docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/docker/docker/api/types" 8 | "github.com/docker/docker/api/types/container" 9 | "github.com/docker/docker/client" 10 | "github.com/wzshiming/console" 11 | ) 12 | 13 | type SessionsDocker struct { 14 | cli *client.Client 15 | } 16 | 17 | var _ console.Sessions = (*SessionsDocker)(nil) 18 | 19 | func NewDockerSessions(host string) (console.Sessions, error) { 20 | cli, err := client.NewClientWithOpts( 21 | client.WithHost(host), 22 | ) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return &SessionsDocker{ 28 | cli: cli, 29 | }, nil 30 | } 31 | 32 | func (d *SessionsDocker) CreateExec(req *console.ReqCreateExec) (*console.RespCreateExec, error) { 33 | // 创建连接 34 | 35 | exec, err := d.cli.ContainerExecCreate(context.Background(), req.CId, types.ExecConfig{ 36 | AttachStdin: true, 37 | AttachStdout: true, 38 | AttachStderr: true, 39 | Tty: true, 40 | Cmd: []string{req.Cmd}, 41 | }) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return &console.RespCreateExec{ 46 | EId: exec.ID, 47 | }, nil 48 | } 49 | 50 | func (d *SessionsDocker) StartExec(id string, ws io.ReadWriter) error { 51 | hr, err := d.cli.ContainerExecAttach(context.Background(), id, container.ExecAttachOptions{ 52 | Detach: false, 53 | Tty: true, 54 | }) 55 | if err != nil { 56 | return err 57 | } 58 | defer hr.Close() 59 | go io.Copy(ws, hr.Conn) 60 | io.Copy(hr.Conn, ws) 61 | 62 | return nil 63 | } 64 | 65 | func (d *SessionsDocker) ResizeExecTTY(req *console.ReqResizeExecTTY) error { 66 | return d.cli.ContainerExecResize(context.Background(), req.EId, container.ResizeOptions{ 67 | Height: uint(req.Height), 68 | Width: uint(req.Width), 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | type Driver func(host string) (Sessions, error) 9 | 10 | var ( 11 | driversMu sync.RWMutex 12 | drivers = map[string]Driver{} 13 | ) 14 | 15 | func Register(name string, driver Driver) { 16 | driversMu.Lock() 17 | defer driversMu.Unlock() 18 | if driver == nil { 19 | panic("console: Register driver is nil") 20 | } 21 | if _, dup := drivers[name]; dup { 22 | panic("console: Register called twice for driver " + name) 23 | } 24 | drivers[name] = driver 25 | } 26 | 27 | func GetDrivers(name, host string) (Sessions, error) { 28 | driversMu.RLock() 29 | defer driversMu.RUnlock() 30 | dri, ok := drivers[name] 31 | if !ok { 32 | return nil, fmt.Errorf("console: Unknown driver %q (forgotten import?)", name) 33 | } 34 | return dri(host) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wzshiming/console 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/creack/pty v1.1.21 7 | github.com/docker/docker v27.0.3+incompatible 8 | github.com/gorilla/handlers v1.5.2 9 | github.com/gorilla/mux v1.8.1 10 | github.com/gorilla/websocket v1.5.3 11 | github.com/wzshiming/winseq v0.0.0-20200720163736-7fa652d2b50e 12 | golang.org/x/crypto v0.25.0 13 | ) 14 | 15 | require ( 16 | github.com/Microsoft/go-winio v0.6.2 // indirect 17 | github.com/containerd/log v0.1.0 // indirect 18 | github.com/distribution/reference v0.6.0 // indirect 19 | github.com/docker/go-connections v0.5.0 // indirect 20 | github.com/docker/go-units v0.5.0 // indirect 21 | github.com/felixge/httpsnoop v1.0.4 // indirect 22 | github.com/go-logr/logr v1.4.2 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/gogo/protobuf v1.3.2 // indirect 25 | github.com/moby/docker-image-spec v1.3.1 // indirect 26 | github.com/moby/term v0.5.0 // indirect 27 | github.com/morikuni/aec v1.0.0 // indirect 28 | github.com/opencontainers/go-digest v1.0.0 // indirect 29 | github.com/opencontainers/image-spec v1.1.0 // indirect 30 | github.com/pkg/errors v0.9.1 // indirect 31 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 32 | go.opentelemetry.io/otel v1.28.0 // indirect 33 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect 34 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 35 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect 36 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 37 | golang.org/x/net v0.27.0 // indirect 38 | golang.org/x/sys v0.22.0 // indirect 39 | golang.org/x/time v0.5.0 // indirect 40 | gotest.tools/v3 v3.5.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 2 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 4 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 5 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 6 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 7 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 8 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 9 | github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 10 | github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 14 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 15 | github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= 16 | github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 17 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 18 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 19 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 20 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 21 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 22 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 23 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 24 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 25 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 26 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 27 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 28 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 29 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 30 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 31 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 32 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 33 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 34 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 35 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 36 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 37 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 38 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 39 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 40 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 41 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 42 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 43 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 44 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 45 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 46 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 47 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 48 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 49 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 50 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 51 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 52 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 53 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 54 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 55 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 59 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 60 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 61 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/wzshiming/winseq v0.0.0-20200720163736-7fa652d2b50e h1:lp2XFXaf81Y9yhE4rIt66qe6ss0jSQsBpIYWz8D/5N0= 63 | github.com/wzshiming/winseq v0.0.0-20200720163736-7fa652d2b50e/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= 64 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 65 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 66 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= 67 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= 68 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 69 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 70 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= 71 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 72 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= 73 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= 74 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 75 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 76 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= 77 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= 78 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 79 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 80 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 81 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 84 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 85 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 86 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 87 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 88 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 89 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 90 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 92 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 93 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 94 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 95 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 99 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 102 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 103 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 104 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 105 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 106 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 107 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 108 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 109 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 110 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 111 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 112 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 113 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 114 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 115 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 116 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 117 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 119 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= 120 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= 121 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= 122 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 123 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= 124 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= 125 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 126 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 127 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 128 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 129 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 130 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 131 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "sync" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/gorilla/websocket" 13 | "github.com/wzshiming/console" 14 | ) 15 | 16 | var ( 17 | sessionsMu sync.RWMutex 18 | session = map[string]console.Sessions{} 19 | ) 20 | 21 | func addSession(id string, s console.Sessions) { 22 | sessionsMu.Lock() 23 | defer sessionsMu.Unlock() 24 | session[id] = s 25 | } 26 | 27 | func getSession(id string) console.Sessions { 28 | sessionsMu.RLock() 29 | defer sessionsMu.RUnlock() 30 | return session[id] 31 | } 32 | 33 | func delSession(id string) { 34 | sessionsMu.Lock() 35 | defer sessionsMu.Unlock() 36 | delete(session, id) 37 | } 38 | 39 | func requests(rc io.ReadCloser, i interface{}) error { 40 | data, err := ioutil.ReadAll(rc) 41 | if err != nil { 42 | return err 43 | } 44 | defer rc.Close() 45 | return json.Unmarshal(data, i) 46 | } 47 | 48 | type errMsg struct { 49 | Msg string `json:"msg,omitempty"` 50 | } 51 | 52 | type wsConn struct { 53 | conn *websocket.Conn 54 | } 55 | 56 | func newWsConn(conn *websocket.Conn) *wsConn { 57 | return &wsConn{ 58 | conn: conn, 59 | } 60 | } 61 | 62 | func (c *wsConn) Read(p []byte) (n int, err error) { 63 | _, rc, err := c.conn.NextReader() 64 | if err != nil { 65 | return 0, err 66 | } 67 | return rc.Read(p) 68 | } 69 | 70 | func (c *wsConn) Write(p []byte) (n int, err error) { 71 | wc, err := c.conn.NextWriter(websocket.BinaryMessage) 72 | if err != nil { 73 | return 0, err 74 | } 75 | defer wc.Close() 76 | return wc.Write(p) 77 | } 78 | 79 | func ResponseJSON(w http.ResponseWriter, status int, v interface{}) error { 80 | w.Header().Set("Content-Type", "application/json") 81 | 82 | w.WriteHeader(status) 83 | err := json.NewEncoder(w).Encode(v) 84 | if err != nil { 85 | return err 86 | } 87 | return nil 88 | } 89 | 90 | func ExecRouter(disable bool, con *console.ReqCreateExec) *mux.Router { 91 | // 路由 92 | mux0 := mux.NewRouter() 93 | upgrader := websocket.Upgrader{ 94 | EnableCompression: true, 95 | } 96 | 97 | // 创建连接 98 | mux0.HandleFunc("/create_exec", func(w http.ResponseWriter, r *http.Request) { 99 | req := &console.ReqCreateExec{} 100 | *req = *con 101 | if !disable { 102 | err := requests(r.Body, &req) 103 | if err != nil { 104 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 105 | return 106 | } 107 | } 108 | 109 | // 设置默认连接驱动 110 | if req.Name == "" { 111 | u, err := url.Parse(req.Host) 112 | if err != nil { 113 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 114 | return 115 | } 116 | req.Name = u.Scheme 117 | } 118 | 119 | // 获取驱动 120 | sesss, err := console.GetDrivers(req.Name, req.Host) 121 | if err != nil { 122 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 123 | return 124 | } 125 | 126 | // 创建连接 127 | exec, err := sesss.CreateExec(req) 128 | if err != nil { 129 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 130 | return 131 | } 132 | 133 | addSession(exec.EId, sesss) 134 | 135 | ResponseJSON(w, http.StatusOK, exec) 136 | return 137 | }) 138 | 139 | // 开始连接 140 | mux0.HandleFunc("/start_exec", func(w http.ResponseWriter, r *http.Request) { 141 | 142 | eid := r.FormValue("eid") 143 | 144 | client := getSession(eid) 145 | if client == nil { 146 | ResponseJSON(w, http.StatusBadRequest, nil) 147 | return 148 | } 149 | defer delSession(eid) 150 | 151 | if !websocket.IsWebSocketUpgrade(r) { 152 | ResponseJSON(w, http.StatusBadRequest, nil) 153 | return 154 | } 155 | 156 | ws, err := upgrader.Upgrade(w, r, nil) 157 | if err != nil { 158 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 159 | return 160 | } 161 | defer ws.Close() 162 | 163 | ws.SetCloseHandler(nil) 164 | ws.SetPingHandler(nil) 165 | 166 | // 执行连接 167 | err = client.StartExec(eid, newWsConn(ws)) 168 | if err != nil { 169 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 170 | return 171 | } 172 | return 173 | }) 174 | 175 | // 窗口大小调整 176 | mux0.HandleFunc("/resize_exec_tty", func(w http.ResponseWriter, r *http.Request) { 177 | req := &console.ReqResizeExecTTY{} 178 | err := requests(r.Body, &req) 179 | if err != nil { 180 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 181 | return 182 | } 183 | 184 | client := getSession(req.EId) 185 | if client == nil { 186 | ResponseJSON(w, http.StatusBadRequest, nil) 187 | return 188 | } 189 | 190 | err = client.ResizeExecTTY(req) 191 | if err != nil { 192 | ResponseJSON(w, http.StatusBadRequest, errMsg{err.Error()}) 193 | return 194 | } 195 | 196 | ResponseJSON(w, http.StatusOK, nil) 197 | return 198 | }) 199 | 200 | return mux0 201 | } 202 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package console 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Sessions interface { 8 | CreateExec(req *ReqCreateExec) (*RespCreateExec, error) 9 | StartExec(id string, ws io.ReadWriter) error 10 | ResizeExecTTY(req *ReqResizeExecTTY) error 11 | } 12 | 13 | type ReqCreateExec struct { 14 | Name string `json:"name,omitempty"` 15 | Host string `json:"host,omitempty"` 16 | CId string `json:"cid,omitempty"` 17 | Cmd string `json:"cmd,omitempty"` 18 | } 19 | 20 | type RespCreateExec struct { 21 | EId string `json:"eid,omitempty"` 22 | } 23 | 24 | type ReqResizeExecTTY struct { 25 | EId string `json:"eid,omitempty"` 26 | Height int `json:"height,omitempty"` 27 | Width int `json:"width,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /shell/init.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "github.com/wzshiming/console" 5 | _ "github.com/wzshiming/winseq" 6 | ) 7 | 8 | func init() { 9 | console.Register("shell", NewShellSessions) 10 | } 11 | -------------------------------------------------------------------------------- /shell/session_shell_notwin.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package shell 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "strconv" 12 | "unsafe" 13 | 14 | "github.com/creack/pty" 15 | "github.com/wzshiming/console" 16 | ) 17 | 18 | type SessionsShell struct { 19 | sessions map[string]*os.File 20 | } 21 | 22 | var _ console.Sessions = (*SessionsShell)(nil) 23 | 24 | func NewShellSessions(host string) (console.Sessions, error) { 25 | return &SessionsShell{ 26 | sessions: map[string]*os.File{}, 27 | }, nil 28 | } 29 | 30 | func (d *SessionsShell) CreateExec(req *console.ReqCreateExec) (*console.RespCreateExec, error) { 31 | sh := exec.Command(req.Cmd) 32 | id := "0x" + strconv.FormatUint(uint64(uintptr(unsafe.Pointer(sh))), 16) 33 | // Start the command with a pty. 34 | ptmx, err := pty.Start(sh) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | d.sessions[id] = ptmx 40 | return &console.RespCreateExec{ 41 | EId: id, 42 | }, nil 43 | } 44 | 45 | func (d *SessionsShell) StartExec(eid string, ws io.ReadWriter) error { 46 | cli, ok := d.sessions[eid] 47 | if !ok { 48 | return fmt.Errorf("Can not find eid " + eid) 49 | } 50 | defer func() { 51 | delete(d.sessions, eid) 52 | cli.Close() 53 | }() 54 | 55 | go io.Copy(cli, ws) 56 | io.Copy(ws, cli) 57 | return nil 58 | } 59 | 60 | func (d *SessionsShell) ResizeExecTTY(req *console.ReqResizeExecTTY) error { 61 | cli, ok := d.sessions[req.EId] 62 | if !ok { 63 | return fmt.Errorf("Can not find eid " + req.EId) 64 | } 65 | return pty.Setsize(cli, &pty.Winsize{ 66 | Rows: uint16(req.Height), 67 | Cols: uint16(req.Width), 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /shell/session_shell_windows.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | "strconv" 9 | "unsafe" 10 | 11 | "github.com/wzshiming/console" 12 | ) 13 | 14 | type SessionsShell struct { 15 | sessions map[string]*exec.Cmd 16 | } 17 | 18 | var _ console.Sessions = (*SessionsShell)(nil) 19 | 20 | func NewShellSessions(host string) (console.Sessions, error) { 21 | return &SessionsShell{ 22 | sessions: map[string]*exec.Cmd{}, 23 | }, nil 24 | } 25 | 26 | func (d *SessionsShell) CreateExec(req *console.ReqCreateExec) (*console.RespCreateExec, error) { 27 | cli := exec.Command(req.Cmd) 28 | id := "0x" + strconv.FormatUint(uint64(uintptr(unsafe.Pointer(cli))), 16) 29 | d.sessions[id] = cli 30 | return &console.RespCreateExec{ 31 | EId: id, 32 | }, nil 33 | } 34 | 35 | func (d *SessionsShell) StartExec(eid string, ws io.ReadWriter) error { 36 | cli, ok := d.sessions[eid] 37 | if !ok { 38 | return fmt.Errorf("Can not find eid " + eid) 39 | } 40 | defer func() { 41 | delete(d.sessions, eid) 42 | }() 43 | 44 | cli.Stdin = io.TeeReader(NewReader(ws), ws) 45 | cli.Stdout = ws 46 | cli.Stderr = ws 47 | 48 | return cli.Run() 49 | } 50 | 51 | func (d *SessionsShell) ResizeExecTTY(req *console.ReqResizeExecTTY) error { 52 | return nil 53 | } 54 | 55 | type Reader struct { 56 | buf io.Reader 57 | } 58 | 59 | func NewReader(read io.Reader) *Reader { 60 | return &Reader{read} 61 | } 62 | 63 | func (r *Reader) Read(p []byte) (n int, err error) { 64 | n, err = r.buf.Read(p) 65 | if err != nil { 66 | return 0, err 67 | } 68 | buf := p[:n] 69 | buf = bytes.Replace(buf, []byte{13}, []byte{'\r', '\n'}, -1) 70 | copy(p[:len(buf)], buf) 71 | return len(buf), nil 72 | } 73 | -------------------------------------------------------------------------------- /ssh/init.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "github.com/wzshiming/console" 5 | ) 6 | 7 | func init() { 8 | console.Register("ssh", NewSshSessions) 9 | } 10 | -------------------------------------------------------------------------------- /ssh/session_ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "strconv" 8 | "unsafe" 9 | 10 | "github.com/wzshiming/console" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | type SessionsSsh struct { 15 | cli *ssh.Client 16 | sessions map[string]*ssh.Session 17 | } 18 | 19 | var _ console.Sessions = (*SessionsSsh)(nil) 20 | 21 | func NewSshSessions(host string) (console.Sessions, error) { 22 | u, err := url.Parse(host) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | pwd := "" 28 | user := "" 29 | if u.User != nil { 30 | user = u.User.Username() 31 | pwd, _ = u.User.Password() 32 | } 33 | cli, err := ssh.Dial("tcp", u.Host, &ssh.ClientConfig{ 34 | User: user, 35 | Auth: []ssh.AuthMethod{ssh.Password(pwd)}, 36 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 37 | }) 38 | 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return &SessionsSsh{ 44 | cli: cli, 45 | sessions: map[string]*ssh.Session{}, 46 | }, nil 47 | } 48 | 49 | func (d *SessionsSsh) CreateExec(req *console.ReqCreateExec) (*console.RespCreateExec, error) { 50 | cli, err := d.cli.NewSession() 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | id := "0x" + strconv.FormatUint(uint64(uintptr(unsafe.Pointer(cli))), 16) 56 | d.sessions[id] = cli 57 | return &console.RespCreateExec{ 58 | EId: id, 59 | }, nil 60 | } 61 | 62 | func (d *SessionsSsh) StartExec(eid string, ws io.ReadWriter) error { 63 | cli, ok := d.sessions[eid] 64 | if !ok { 65 | return fmt.Errorf("Can not find eid " + eid) 66 | } 67 | defer func() { 68 | delete(d.sessions, eid) 69 | }() 70 | 71 | cli.Stdin = ws 72 | cli.Stdout = ws 73 | cli.Stderr = ws 74 | 75 | // Request pseudo terminal 76 | err := cli.RequestPty("xterm", 40, 80, ssh.TerminalModes{ 77 | ssh.ECHO: 1, // disable echoing 78 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 79 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 80 | }) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = cli.Shell() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | return cli.Wait() 91 | } 92 | 93 | func (d *SessionsSsh) ResizeExecTTY(req *console.ReqResizeExecTTY) error { 94 | cli, ok := d.sessions[req.EId] 95 | if !ok { 96 | return fmt.Errorf("Can not find eid " + req.EId) 97 | } 98 | return cli.WindowChange(req.Height, req.Width) 99 | } 100 | -------------------------------------------------------------------------------- /static/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | update: 5 | wget -O js/xterm.min.js https://cdn.bootcss.com/xterm/3.12.2/xterm.min.js 6 | wget -O css/xterm.min.css https://cdn.bootcss.com/xterm/3.12.2/xterm.min.css 7 | wget -O js/fit.min.js https://cdn.bootcss.com/xterm/3.12.2/addons/fit/fit.min.js 8 | wget -O js/attach.min.js https://cdn.bootcss.com/xterm/3.12.2/addons/attach/attach.min.js 9 | wget -O js/winptyCompat.min.js https://cdn.bootcss.com/xterm/3.12.2/addons/winptyCompat/winptyCompat.min.js 10 | wget -O js/webLinks.min.js https://cdn.bootcss.com/xterm/3.12.2/addons/webLinks/webLinks.min.js -------------------------------------------------------------------------------- /static/css/xterm.min.css: -------------------------------------------------------------------------------- 1 | .xterm{font-feature-settings:"liga" 0;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:10}.xterm .xterm-helper-textarea{position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-10;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:100;color:transparent}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden} 2 | /*# sourceMappingURL=xterm.min.css.map */ -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Console 5 | 6 | 7 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /static/js/attach.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).attach=e()}}(function(){return function f(a,i,u){function s(t,e){if(!i[t]){if(!a[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var o=i[t]={exports:{}};a[t][0].call(o.exports,function(e){return s(a[t][1][e]||e)},o,o.exports,f,a,i,u)}return i[t].exports}for(var c="function"==typeof require&&require,e=0;e0&&e-1 in t)}function q(t){return a.call(t,function(t){return null!=t})}function H(t){return t.length>0?r.fn.concat.apply([],t):t}function I(t){return t.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function V(t){return t in l?l[t]:l[t]=new RegExp("(^|\\s)"+t+"(\\s|$)")}function _(t,e){return"number"!=typeof e||h[I(t)]?e:e+"px"}function B(t){var e,n;return c[t]||(e=f.createElement(t),f.body.appendChild(e),n=getComputedStyle(e,"").getPropertyValue("display"),e.parentNode.removeChild(e),"none"==n&&(n="block"),c[t]=n),c[t]}function U(t){return"children"in t?u.call(t.children):r.map(t.childNodes,function(t){return 1==t.nodeType?t:void 0})}function X(t,e){var n,r=t?t.length:0;for(n=0;r>n;n++)this[n]=t[n];this.length=r,this.selector=e||""}function J(t,r,i){for(n in r)i&&(Z(r[n])||L(r[n]))?(Z(r[n])&&!Z(t[n])&&(t[n]={}),L(r[n])&&!L(t[n])&&(t[n]=[]),J(t[n],r[n],i)):r[n]!==e&&(t[n]=r[n])}function W(t,e){return null==e?r(t):r(t).filter(e)}function Y(t,e,n,r){return F(e)?e.call(t,n,r):e}function G(t,e,n){null==n?t.removeAttribute(e):t.setAttribute(e,n)}function K(t,n){var r=t.className||"",i=r&&r.baseVal!==e;return n===e?i?r.baseVal:r:void(i?r.baseVal=n:t.className=n)}function Q(t){try{return t?"true"==t||("false"==t?!1:"null"==t?null:+t+""==t?+t:/^[\[\{]/.test(t)?r.parseJSON(t):t):t}catch(e){return t}}function tt(t,e){e(t);for(var n=0,r=t.childNodes.length;r>n;n++)tt(t.childNodes[n],e)}var e,n,r,i,O,P,o=[],s=o.concat,a=o.filter,u=o.slice,f=t.document,c={},l={},h={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},p=/^\s*<(\w+|!)[^>]*>/,d=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,m=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,g=/^(?:body|html)$/i,v=/([A-Z])/g,y=["val","css","html","text","data","width","height","offset"],x=["after","prepend","before","append"],b=f.createElement("table"),E=f.createElement("tr"),j={tr:f.createElement("tbody"),tbody:b,thead:b,tfoot:b,td:E,th:E,"*":f.createElement("div")},w=/complete|loaded|interactive/,T=/^[\w-]*$/,S={},C=S.toString,N={},A=f.createElement("div"),D={tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},L=Array.isArray||function(t){return t instanceof Array};return N.matches=function(t,e){if(!e||!t||1!==t.nodeType)return!1;var n=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.matchesSelector;if(n)return n.call(t,e);var r,i=t.parentNode,o=!i;return o&&(i=A).appendChild(t),r=~N.qsa(i,e).indexOf(t),o&&A.removeChild(t),r},O=function(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})},P=function(t){return a.call(t,function(e,n){return t.indexOf(e)==n})},N.fragment=function(t,n,i){var o,s,a;return d.test(t)&&(o=r(f.createElement(RegExp.$1))),o||(t.replace&&(t=t.replace(m,"<$1>")),n===e&&(n=p.test(t)&&RegExp.$1),n in j||(n="*"),a=j[n],a.innerHTML=""+t,o=r.each(u.call(a.childNodes),function(){a.removeChild(this)})),Z(i)&&(s=r(o),r.each(i,function(t,e){y.indexOf(t)>-1?s[t](e):s.attr(t,e)})),o},N.Z=function(t,e){return new X(t,e)},N.isZ=function(t){return t instanceof N.Z},N.init=function(t,n){var i;if(!t)return N.Z();if("string"==typeof t)if(t=t.trim(),"<"==t[0]&&p.test(t))i=N.fragment(t,RegExp.$1,n),t=null;else{if(n!==e)return r(n).find(t);i=N.qsa(f,t)}else{if(F(t))return r(f).ready(t);if(N.isZ(t))return t;if(L(t))i=q(t);else if(R(t))i=[t],t=null;else if(p.test(t))i=N.fragment(t.trim(),RegExp.$1,n),t=null;else{if(n!==e)return r(n).find(t);i=N.qsa(f,t)}}return N.Z(i,t)},r=function(t,e){return N.init(t,e)},r.extend=function(t){var e,n=u.call(arguments,1);return"boolean"==typeof t&&(e=t,t=n.shift()),n.forEach(function(n){J(t,n,e)}),t},N.qsa=function(t,e){var n,r="#"==e[0],i=!r&&"."==e[0],o=r||i?e.slice(1):e,s=T.test(o);return t.getElementById&&s&&r?(n=t.getElementById(o))?[n]:[]:1!==t.nodeType&&9!==t.nodeType&&11!==t.nodeType?[]:u.call(s&&!r&&t.getElementsByClassName?i?t.getElementsByClassName(o):t.getElementsByTagName(e):t.querySelectorAll(e))},r.contains=f.documentElement.contains?function(t,e){return t!==e&&t.contains(e)}:function(t,e){for(;e&&(e=e.parentNode);)if(e===t)return!0;return!1},r.type=$,r.isFunction=F,r.isWindow=k,r.isArray=L,r.isPlainObject=Z,r.isEmptyObject=function(t){var e;for(e in t)return!1;return!0},r.isNumeric=function(t){var e=Number(t),n=typeof t;return null!=t&&"boolean"!=n&&("string"!=n||t.length)&&!isNaN(e)&&isFinite(e)||!1},r.inArray=function(t,e,n){return o.indexOf.call(e,t,n)},r.camelCase=O,r.trim=function(t){return null==t?"":String.prototype.trim.call(t)},r.uuid=0,r.support={},r.expr={},r.noop=function(){},r.map=function(t,e){var n,i,o,r=[];if(z(t))for(i=0;i=0?t:t+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(t){return o.every.call(this,function(e,n){return t.call(e,n,e)!==!1}),this},filter:function(t){return F(t)?this.not(this.not(t)):r(a.call(this,function(e){return N.matches(e,t)}))},add:function(t,e){return r(P(this.concat(r(t,e))))},is:function(t){return this.length>0&&N.matches(this[0],t)},not:function(t){var n=[];if(F(t)&&t.call!==e)this.each(function(e){t.call(this,e)||n.push(this)});else{var i="string"==typeof t?this.filter(t):z(t)&&F(t.item)?u.call(t):r(t);this.forEach(function(t){i.indexOf(t)<0&&n.push(t)})}return r(n)},has:function(t){return this.filter(function(){return R(t)?r.contains(this,t):r(this).find(t).size()})},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},first:function(){var t=this[0];return t&&!R(t)?t:r(t)},last:function(){var t=this[this.length-1];return t&&!R(t)?t:r(t)},find:function(t){var e,n=this;return e=t?"object"==typeof t?r(t).filter(function(){var t=this;return o.some.call(n,function(e){return r.contains(e,t)})}):1==this.length?r(N.qsa(this[0],t)):this.map(function(){return N.qsa(this,t)}):r()},closest:function(t,e){var n=[],i="object"==typeof t&&r(t);return this.each(function(r,o){for(;o&&!(i?i.indexOf(o)>=0:N.matches(o,t));)o=o!==e&&!M(o)&&o.parentNode;o&&n.indexOf(o)<0&&n.push(o)}),r(n)},parents:function(t){for(var e=[],n=this;n.length>0;)n=r.map(n,function(t){return(t=t.parentNode)&&!M(t)&&e.indexOf(t)<0?(e.push(t),t):void 0});return W(e,t)},parent:function(t){return W(P(this.pluck("parentNode")),t)},children:function(t){return W(this.map(function(){return U(this)}),t)},contents:function(){return this.map(function(){return this.contentDocument||u.call(this.childNodes)})},siblings:function(t){return W(this.map(function(t,e){return a.call(U(e.parentNode),function(t){return t!==e})}),t)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(t){return r.map(this,function(e){return e[t]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=""),"none"==getComputedStyle(this,"").getPropertyValue("display")&&(this.style.display=B(this.nodeName))})},replaceWith:function(t){return this.before(t).remove()},wrap:function(t){var e=F(t);if(this[0]&&!e)var n=r(t).get(0),i=n.parentNode||this.length>1;return this.each(function(o){r(this).wrapAll(e?t.call(this,o):i?n.cloneNode(!0):n)})},wrapAll:function(t){if(this[0]){r(this[0]).before(t=r(t));for(var e;(e=t.children()).length;)t=e.first();r(t).append(this)}return this},wrapInner:function(t){var e=F(t);return this.each(function(n){var i=r(this),o=i.contents(),s=e?t.call(this,n):t;o.length?o.wrapAll(s):i.append(s)})},unwrap:function(){return this.parent().each(function(){r(this).replaceWith(r(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(t){return this.each(function(){var n=r(this);(t===e?"none"==n.css("display"):t)?n.show():n.hide()})},prev:function(t){return r(this.pluck("previousElementSibling")).filter(t||"*")},next:function(t){return r(this.pluck("nextElementSibling")).filter(t||"*")},html:function(t){return 0 in arguments?this.each(function(e){var n=this.innerHTML;r(this).empty().append(Y(this,t,e,n))}):0 in this?this[0].innerHTML:null},text:function(t){return 0 in arguments?this.each(function(e){var n=Y(this,t,e,this.textContent);this.textContent=null==n?"":""+n}):0 in this?this.pluck("textContent").join(""):null},attr:function(t,r){var i;return"string"!=typeof t||1 in arguments?this.each(function(e){if(1===this.nodeType)if(R(t))for(n in t)G(this,n,t[n]);else G(this,t,Y(this,r,e,this.getAttribute(t)))}):0 in this&&1==this[0].nodeType&&null!=(i=this[0].getAttribute(t))?i:e},removeAttr:function(t){return this.each(function(){1===this.nodeType&&t.split(" ").forEach(function(t){G(this,t)},this)})},prop:function(t,e){return t=D[t]||t,1 in arguments?this.each(function(n){this[t]=Y(this,e,n,this[t])}):this[0]&&this[0][t]},removeProp:function(t){return t=D[t]||t,this.each(function(){delete this[t]})},data:function(t,n){var r="data-"+t.replace(v,"-$1").toLowerCase(),i=1 in arguments?this.attr(r,n):this.attr(r);return null!==i?Q(i):e},val:function(t){return 0 in arguments?(null==t&&(t=""),this.each(function(e){this.value=Y(this,t,e,this.value)})):this[0]&&(this[0].multiple?r(this[0]).find("option").filter(function(){return this.selected}).pluck("value"):this[0].value)},offset:function(e){if(e)return this.each(function(t){var n=r(this),i=Y(this,e,t,n.offset()),o=n.offsetParent().offset(),s={top:i.top-o.top,left:i.left-o.left};"static"==n.css("position")&&(s.position="relative"),n.css(s)});if(!this.length)return null;if(f.documentElement!==this[0]&&!r.contains(f.documentElement,this[0]))return{top:0,left:0};var n=this[0].getBoundingClientRect();return{left:n.left+t.pageXOffset,top:n.top+t.pageYOffset,width:Math.round(n.width),height:Math.round(n.height)}},css:function(t,e){if(arguments.length<2){var i=this[0];if("string"==typeof t){if(!i)return;return i.style[O(t)]||getComputedStyle(i,"").getPropertyValue(t)}if(L(t)){if(!i)return;var o={},s=getComputedStyle(i,"");return r.each(t,function(t,e){o[e]=i.style[O(e)]||s.getPropertyValue(e)}),o}}var a="";if("string"==$(t))e||0===e?a=I(t)+":"+_(t,e):this.each(function(){this.style.removeProperty(I(t))});else for(n in t)t[n]||0===t[n]?a+=I(n)+":"+_(n,t[n])+";":this.each(function(){this.style.removeProperty(I(n))});return this.each(function(){this.style.cssText+=";"+a})},index:function(t){return t?this.indexOf(r(t)[0]):this.parent().children().indexOf(this[0])},hasClass:function(t){return t?o.some.call(this,function(t){return this.test(K(t))},V(t)):!1},addClass:function(t){return t?this.each(function(e){if("className"in this){i=[];var n=K(this),o=Y(this,t,e,n);o.split(/\s+/g).forEach(function(t){r(this).hasClass(t)||i.push(t)},this),i.length&&K(this,n+(n?" ":"")+i.join(" "))}}):this},removeClass:function(t){return this.each(function(n){if("className"in this){if(t===e)return K(this,"");i=K(this),Y(this,t,n,i).split(/\s+/g).forEach(function(t){i=i.replace(V(t)," ")}),K(this,i.trim())}})},toggleClass:function(t,n){return t?this.each(function(i){var o=r(this),s=Y(this,t,i,K(this));s.split(/\s+/g).forEach(function(t){(n===e?!o.hasClass(t):n)?o.addClass(t):o.removeClass(t)})}):this},scrollTop:function(t){if(this.length){var n="scrollTop"in this[0];return t===e?n?this[0].scrollTop:this[0].pageYOffset:this.each(n?function(){this.scrollTop=t}:function(){this.scrollTo(this.scrollX,t)})}},scrollLeft:function(t){if(this.length){var n="scrollLeft"in this[0];return t===e?n?this[0].scrollLeft:this[0].pageXOffset:this.each(n?function(){this.scrollLeft=t}:function(){this.scrollTo(t,this.scrollY)})}},position:function(){if(this.length){var t=this[0],e=this.offsetParent(),n=this.offset(),i=g.test(e[0].nodeName)?{top:0,left:0}:e.offset();return n.top-=parseFloat(r(t).css("margin-top"))||0,n.left-=parseFloat(r(t).css("margin-left"))||0,i.top+=parseFloat(r(e[0]).css("border-top-width"))||0,i.left+=parseFloat(r(e[0]).css("border-left-width"))||0,{top:n.top-i.top,left:n.left-i.left}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent||f.body;t&&!g.test(t.nodeName)&&"static"==r(t).css("position");)t=t.offsetParent;return t})}},r.fn.detach=r.fn.remove,["width","height"].forEach(function(t){var n=t.replace(/./,function(t){return t[0].toUpperCase()});r.fn[t]=function(i){var o,s=this[0];return i===e?k(s)?s["inner"+n]:M(s)?s.documentElement["scroll"+n]:(o=this.offset())&&o[t]:this.each(function(e){s=r(this),s.css(t,Y(this,i,e,s[t]()))})}}),x.forEach(function(n,i){var o=i%2;r.fn[n]=function(){var n,a,s=r.map(arguments,function(t){var i=[];return n=$(t),"array"==n?(t.forEach(function(t){return t.nodeType!==e?i.push(t):r.zepto.isZ(t)?i=i.concat(t.get()):void(i=i.concat(N.fragment(t)))}),i):"object"==n||null==t?t:N.fragment(t)}),u=this.length>1;return s.length<1?this:this.each(function(e,n){a=o?n:n.parentNode,n=0==i?n.nextSibling:1==i?n.firstChild:2==i?n:null;var c=r.contains(f.documentElement,a);s.forEach(function(e){if(u)e=e.cloneNode(!0);else if(!a)return r(e).remove();a.insertBefore(e,n),c&&tt(e,function(e){if(!(null==e.nodeName||"SCRIPT"!==e.nodeName.toUpperCase()||e.type&&"text/javascript"!==e.type||e.src)){var n=e.ownerDocument?e.ownerDocument.defaultView:t;n.eval.call(n,e.innerHTML)}})})})},r.fn[o?n+"To":"insert"+(i?"Before":"After")]=function(t){return r(t)[n](this),this}}),N.Z.prototype=X.prototype=r.fn,N.uniq=P,N.deserializeValue=Q,r.zepto=N,r}();return t.Zepto=e,void 0===t.$&&(t.$=e),function(e){function h(t){return t._zid||(t._zid=n++)}function p(t,e,n,r){if(e=d(e),e.ns)var i=m(e.ns);return(a[h(t)]||[]).filter(function(t){return t&&(!e.e||t.e==e.e)&&(!e.ns||i.test(t.ns))&&(!n||h(t.fn)===h(n))&&(!r||t.sel==r)})}function d(t){var e=(""+t).split(".");return{e:e[0],ns:e.slice(1).sort().join(" ")}}function m(t){return new RegExp("(?:^| )"+t.replace(" "," .* ?")+"(?: |$)")}function g(t,e){return t.del&&!f&&t.e in c||!!e}function v(t){return l[t]||f&&c[t]||t}function y(t,n,i,o,s,u,f){var c=h(t),p=a[c]||(a[c]=[]);n.split(/\s/).forEach(function(n){if("ready"==n)return e(document).ready(i);var a=d(n);a.fn=i,a.sel=s,a.e in l&&(i=function(t){var n=t.relatedTarget;return!n||n!==this&&!e.contains(this,n)?a.fn.apply(this,arguments):void 0}),a.del=u;var c=u||i;a.proxy=function(e){if(e=T(e),!e.isImmediatePropagationStopped()){e.data=o;var n=c.apply(t,e._args==r?[e]:[e].concat(e._args));return n===!1&&(e.preventDefault(),e.stopPropagation()),n}},a.i=p.length,p.push(a),"addEventListener"in t&&t.addEventListener(v(a.e),a.proxy,g(a,f))})}function x(t,e,n,r,i){var o=h(t);(e||"").split(/\s/).forEach(function(e){p(t,e,n,r).forEach(function(e){delete a[o][e.i],"removeEventListener"in t&&t.removeEventListener(v(e.e),e.proxy,g(e,i))})})}function T(t,n){return(n||!t.isDefaultPrevented)&&(n||(n=t),e.each(w,function(e,r){var i=n[e];t[e]=function(){return this[r]=b,i&&i.apply(n,arguments)},t[r]=E}),t.timeStamp||(t.timeStamp=Date.now()),(n.defaultPrevented!==r?n.defaultPrevented:"returnValue"in n?n.returnValue===!1:n.getPreventDefault&&n.getPreventDefault())&&(t.isDefaultPrevented=b)),t}function S(t){var e,n={originalEvent:t};for(e in t)j.test(e)||t[e]===r||(n[e]=t[e]);return T(n,t)}var r,n=1,i=Array.prototype.slice,o=e.isFunction,s=function(t){return"string"==typeof t},a={},u={},f="onfocusin"in t,c={focus:"focusin",blur:"focusout"},l={mouseenter:"mouseover",mouseleave:"mouseout"};u.click=u.mousedown=u.mouseup=u.mousemove="MouseEvents",e.event={add:y,remove:x},e.proxy=function(t,n){var r=2 in arguments&&i.call(arguments,2);if(o(t)){var a=function(){return t.apply(n,r?r.concat(i.call(arguments)):arguments)};return a._zid=h(t),a}if(s(n))return r?(r.unshift(t[n],t),e.proxy.apply(null,r)):e.proxy(t[n],t);throw new TypeError("expected function")},e.fn.bind=function(t,e,n){return this.on(t,e,n)},e.fn.unbind=function(t,e){return this.off(t,e)},e.fn.one=function(t,e,n,r){return this.on(t,e,n,r,1)};var b=function(){return!0},E=function(){return!1},j=/^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/,w={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};e.fn.delegate=function(t,e,n){return this.on(e,t,n)},e.fn.undelegate=function(t,e,n){return this.off(e,t,n)},e.fn.live=function(t,n){return e(document.body).delegate(this.selector,t,n),this},e.fn.die=function(t,n){return e(document.body).undelegate(this.selector,t,n),this},e.fn.on=function(t,n,a,u,f){var c,l,h=this;return t&&!s(t)?(e.each(t,function(t,e){h.on(t,n,a,e,f)}),h):(s(n)||o(u)||u===!1||(u=a,a=n,n=r),(u===r||a===!1)&&(u=a,a=r),u===!1&&(u=E),h.each(function(r,o){f&&(c=function(t){return x(o,t.type,u),u.apply(this,arguments)}),n&&(l=function(t){var r,s=e(t.target).closest(n,o).get(0);return s&&s!==o?(r=e.extend(S(t),{currentTarget:s,liveFired:o}),(c||u).apply(s,[r].concat(i.call(arguments,1)))):void 0}),y(o,t,u,a,n,l||c)}))},e.fn.off=function(t,n,i){var a=this;return t&&!s(t)?(e.each(t,function(t,e){a.off(t,n,e)}),a):(s(n)||o(i)||i===!1||(i=n,n=r),i===!1&&(i=E),a.each(function(){x(this,t,i,n)}))},e.fn.trigger=function(t,n){return t=s(t)||e.isPlainObject(t)?e.Event(t):T(t),t._args=n,this.each(function(){t.type in c&&"function"==typeof this[t.type]?this[t.type]():"dispatchEvent"in this?this.dispatchEvent(t):e(this).triggerHandler(t,n)})},e.fn.triggerHandler=function(t,n){var r,i;return this.each(function(o,a){r=S(s(t)?e.Event(t):t),r._args=n,r.target=a,e.each(p(a,t.type||t),function(t,e){return i=e.proxy(r),r.isImmediatePropagationStopped()?!1:void 0})}),i},"focusin focusout focus blur load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(t){e.fn[t]=function(e){return 0 in arguments?this.bind(t,e):this.trigger(t)}}),e.Event=function(t,e){s(t)||(e=t,t=e.type);var n=document.createEvent(u[t]||"Events"),r=!0;if(e)for(var i in e)"bubbles"==i?r=!!e[i]:n[i]=e[i];return n.initEvent(t,r,!0),T(n)}}(e),function(e){function p(t,n,r){var i=e.Event(n);return e(t).trigger(i,r),!i.isDefaultPrevented()}function d(t,e,n,i){return t.global?p(e||r,n,i):void 0}function m(t){t.global&&0===e.active++&&d(t,null,"ajaxStart")}function g(t){t.global&&!--e.active&&d(t,null,"ajaxStop")}function v(t,e){var n=e.context;return e.beforeSend.call(n,t,e)===!1||d(e,n,"ajaxBeforeSend",[t,e])===!1?!1:void d(e,n,"ajaxSend",[t,e])}function y(t,e,n,r){var i=n.context,o="success";n.success.call(i,t,o,e),r&&r.resolveWith(i,[t,o,e]),d(n,i,"ajaxSuccess",[e,n,t]),b(o,e,n)}function x(t,e,n,r,i){var o=r.context;r.error.call(o,n,e,t),i&&i.rejectWith(o,[n,e,t]),d(r,o,"ajaxError",[n,r,t||e]),b(e,n,r)}function b(t,e,n){var r=n.context;n.complete.call(r,e,t),d(n,r,"ajaxComplete",[e,n]),g(n)}function E(t,e,n){if(n.dataFilter==j)return t;var r=n.context;return n.dataFilter.call(r,t,e)}function j(){}function w(t){return t&&(t=t.split(";",2)[0]),t&&(t==c?"html":t==f?"json":a.test(t)?"script":u.test(t)&&"xml")||"text"}function T(t,e){return""==e?t:(t+"&"+e).replace(/[&?]{1,2}/,"?")}function S(t){t.processData&&t.data&&"string"!=e.type(t.data)&&(t.data=e.param(t.data,t.traditional)),!t.data||t.type&&"GET"!=t.type.toUpperCase()&&"jsonp"!=t.dataType||(t.url=T(t.url,t.data),t.data=void 0)}function C(t,n,r,i){return e.isFunction(n)&&(i=r,r=n,n=void 0),e.isFunction(r)||(i=r,r=void 0),{url:t,data:n,success:r,dataType:i}}function O(t,n,r,i){var o,s=e.isArray(n),a=e.isPlainObject(n);e.each(n,function(n,u){o=e.type(u),i&&(n=r?i:i+"["+(a||"object"==o||"array"==o?n:"")+"]"),!i&&s?t.add(u.name,u.value):"array"==o||!r&&"object"==o?O(t,u,r,n):t.add(n,u)})}var i,o,n=+new Date,r=t.document,s=/)<[^<]*)*<\/script>/gi,a=/^(?:text|application)\/javascript/i,u=/^(?:text|application)\/xml/i,f="application/json",c="text/html",l=/^\s*$/,h=r.createElement("a");h.href=t.location.href,e.active=0,e.ajaxJSONP=function(i,o){if(!("type"in i))return e.ajax(i);var c,p,s=i.jsonpCallback,a=(e.isFunction(s)?s():s)||"Zepto"+n++,u=r.createElement("script"),f=t[a],l=function(t){e(u).triggerHandler("error",t||"abort")},h={abort:l};return o&&o.promise(h),e(u).on("load error",function(n,r){clearTimeout(p),e(u).off().remove(),"error"!=n.type&&c?y(c[0],h,i,o):x(null,r||"error",h,i,o),t[a]=f,c&&e.isFunction(f)&&f(c[0]),f=c=void 0}),v(h,i)===!1?(l("abort"),h):(t[a]=function(){c=arguments},u.src=i.url.replace(/\?(.+)=\?/,"?$1="+a),r.head.appendChild(u),i.timeout>0&&(p=setTimeout(function(){l("timeout")},i.timeout)),h)},e.ajaxSettings={type:"GET",beforeSend:j,success:j,error:j,complete:j,context:null,global:!0,xhr:function(){return new t.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript, application/x-javascript",json:f,xml:"application/xml, text/xml",html:c,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0,dataFilter:j},e.ajax=function(n){var u,f,s=e.extend({},n||{}),a=e.Deferred&&e.Deferred();for(i in e.ajaxSettings)void 0===s[i]&&(s[i]=e.ajaxSettings[i]);m(s),s.crossDomain||(u=r.createElement("a"),u.href=s.url,u.href=u.href,s.crossDomain=h.protocol+"//"+h.host!=u.protocol+"//"+u.host),s.url||(s.url=t.location.toString()),(f=s.url.indexOf("#"))>-1&&(s.url=s.url.slice(0,f)),S(s);var c=s.dataType,p=/\?.+=\?/.test(s.url);if(p&&(c="jsonp"),s.cache!==!1&&(n&&n.cache===!0||"script"!=c&&"jsonp"!=c)||(s.url=T(s.url,"_="+Date.now())),"jsonp"==c)return p||(s.url=T(s.url,s.jsonp?s.jsonp+"=?":s.jsonp===!1?"":"callback=?")),e.ajaxJSONP(s,a);var P,d=s.accepts[c],g={},b=function(t,e){g[t.toLowerCase()]=[t,e]},C=/^([\w-]+:)\/\//.test(s.url)?RegExp.$1:t.location.protocol,N=s.xhr(),O=N.setRequestHeader;if(a&&a.promise(N),s.crossDomain||b("X-Requested-With","XMLHttpRequest"),b("Accept",d||"*/*"),(d=s.mimeType||d)&&(d.indexOf(",")>-1&&(d=d.split(",",2)[0]),N.overrideMimeType&&N.overrideMimeType(d)),(s.contentType||s.contentType!==!1&&s.data&&"GET"!=s.type.toUpperCase())&&b("Content-Type",s.contentType||"application/x-www-form-urlencoded"),s.headers)for(o in s.headers)b(o,s.headers[o]);if(N.setRequestHeader=b,N.onreadystatechange=function(){if(4==N.readyState){N.onreadystatechange=j,clearTimeout(P);var t,n=!1;if(N.status>=200&&N.status<300||304==N.status||0==N.status&&"file:"==C){if(c=c||w(s.mimeType||N.getResponseHeader("content-type")),"arraybuffer"==N.responseType||"blob"==N.responseType)t=N.response;else{t=N.responseText;try{t=E(t,c,s),"script"==c?(1,eval)(t):"xml"==c?t=N.responseXML:"json"==c&&(t=l.test(t)?null:e.parseJSON(t))}catch(r){n=r}if(n)return x(n,"parsererror",N,s,a)}y(t,N,s,a)}else x(N.statusText||null,N.status?"error":"abort",N,s,a)}},v(N,s)===!1)return N.abort(),x(null,"abort",N,s,a),N;var A="async"in s?s.async:!0;if(N.open(s.type,s.url,A,s.username,s.password),s.xhrFields)for(o in s.xhrFields)N[o]=s.xhrFields[o];for(o in g)O.apply(N,g[o]);return s.timeout>0&&(P=setTimeout(function(){N.onreadystatechange=j,N.abort(),x(null,"timeout",N,s,a)},s.timeout)),N.send(s.data?s.data:null),N},e.get=function(){return e.ajax(C.apply(null,arguments))},e.post=function(){var t=C.apply(null,arguments);return t.type="POST",e.ajax(t)},e.getJSON=function(){var t=C.apply(null,arguments);return t.dataType="json",e.ajax(t)},e.fn.load=function(t,n,r){if(!this.length)return this;var a,i=this,o=t.split(/\s/),u=C(t,n,r),f=u.success;return o.length>1&&(u.url=o[0],a=o[1]),u.success=function(t){i.html(a?e("
").html(t.replace(s,"")).find(a):t),f&&f.apply(i,arguments)},e.ajax(u),this};var N=encodeURIComponent;e.param=function(t,n){var r=[];return r.add=function(t,n){e.isFunction(n)&&(n=n()),null==n&&(n=""),this.push(N(t)+"="+N(n))},O(r,t,n),r.join("&").replace(/%20/g,"+")}}(e),function(t){t.fn.serializeArray=function(){var e,n,r=[],i=function(t){return t.forEach?t.forEach(i):void r.push({name:e,value:t})};return this[0]&&t.each(this[0].elements,function(r,o){n=o.type,e=o.name,e&&"fieldset"!=o.nodeName.toLowerCase()&&!o.disabled&&"submit"!=n&&"reset"!=n&&"button"!=n&&"file"!=n&&("radio"!=n&&"checkbox"!=n||o.checked)&&i(t(o).val())}),r},t.fn.serialize=function(){var t=[];return this.serializeArray().forEach(function(e){t.push(encodeURIComponent(e.name)+"="+encodeURIComponent(e.value))}),t.join("&")},t.fn.submit=function(e){if(0 in arguments)this.bind("submit",e);else if(this.length){var n=t.Event("submit");this.eq(0).trigger(n),n.isDefaultPrevented()||this.get(0).submit()}return this}}(e),function(){try{getComputedStyle(void 0)}catch(e){var n=getComputedStyle;t.getComputedStyle=function(t,e){try{return n(t,e)}catch(r){return null}}}}(),e}); -------------------------------------------------------------------------------- /static/robot.txt: -------------------------------------------------------------------------------- 1 | # robots.txt 2 | Disallow: / -------------------------------------------------------------------------------- /static/web.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed css 8 | //go:embed js 9 | //go:embed index.html 10 | //go:embed robot.txt 11 | var Web embed.FS 12 | --------------------------------------------------------------------------------