├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── auth.go ├── chars.go ├── cmd └── telshell │ └── main.go ├── docs ├── demo.gif └── preview.png ├── go.mod ├── go.sum ├── handler.go ├── install.sh ├── internal ├── app │ ├── array_flags.go │ └── context.go └── helpers │ └── net.go ├── iowrapper.go ├── platform_posix.go ├── platform_windows.go ├── server.go └── welcome.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | .vscode/ 4 | build/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Denis Sedchenko 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ADDR ?= :1000 2 | APP_SH ?= /bin/bash 3 | PKG ?= ./cmd/telshell 4 | BUILD_DIR ?= ./build 5 | PROJECT_NAME := telshell 6 | 7 | define go_build 8 | $(info - Building for $(1) ($(2))) 9 | $(shell GOOS=$(1) GOARCH=$(2) go build -o $(BUILD_DIR)/telshell_$(3) $(PKG)) 10 | endef 11 | 12 | .PHONY:build 13 | build: clean linux darwin windows 14 | 15 | .PHONY: run 16 | run: 17 | @go run $(PKG) -addr=$(ADDR) -shell=$(APP_SH) 18 | 19 | .PHONY: clean 20 | clean: 21 | @echo "- Clean build directory" && rm -rf $(BUILD_DIR) 22 | 23 | .PHONY:windows 24 | windows: 25 | $(call go_build,windows,amd64,windows-amd64.exe) 26 | $(call go_build,windows,386,windows-i386.exe) 27 | 28 | .PHONY:linux 29 | linux: 30 | $(call go_build,linux,amd64,linux-amd64) 31 | $(call go_build,linux,386,linux-i386) 32 | $(call go_build,linux,arm64,linux-aarch64) 33 | 34 | .PHONY: darwin 35 | darwin: 36 | $(call go_build,darwin,amd64,darwin-amd64) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TelShell 2 | 3 | Tiny Telnet shell server in Go 4 | 5 | ![alt text](./docs/demo.gif) 6 | 7 | ## Download 8 | 9 | ```bash 10 | curl https://raw.githubusercontent.com/x1unix/telshell/master/install.sh | sh 11 | ``` 12 | 13 | Also, you can grab latest release from [here](https://github.com/x1unix/telshell/releases/latest) 14 | 15 | ## Usage 16 | 17 | ```bash 18 | ./telshell -addr=:5000 19 | ``` 20 | 21 | We also recommend `-s=-i` argument to start shell in *interactive* mode 22 | 23 | Use `-help` to get help about additional params 24 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | 10 | "github.com/pkg/errors" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | const MaxLoginChars = 32 15 | 16 | var nameRegex = regexp.MustCompile(`(?m)^[A-Za-z0-9\.\-\_]+$`) 17 | 18 | // AuthShellHandler is shell handler that requires username and password 19 | type AuthShellHandler struct { 20 | IOParams 21 | shellPath string 22 | shellArgs []string 23 | log *zap.SugaredLogger 24 | } 25 | 26 | // NewAuthShellHandler creates new authorized shell handler 27 | func NewAuthShellHandler(params IOParams, shell string, args ...string) AuthShellHandler { 28 | return AuthShellHandler{ 29 | IOParams: params, 30 | shellPath: shell, 31 | shellArgs: args, 32 | log: zap.S().Named("auth_shell"), 33 | } 34 | } 35 | 36 | // Handle implements telshell.Handler 37 | func (h AuthShellHandler) Handle(ctx context.Context, rw io.ReadWriter) error { 38 | fmt.Fprint(rw, "Username:") 39 | 40 | // Read output and match string 41 | buff := make([]byte, MaxLoginChars*4) 42 | _, _ = rw.Read(buff) 43 | 44 | // Sanitize input 45 | buff = TrimCLRF(buff) 46 | if len(buff) == 0 || !nameRegex.Match(buff) { 47 | // Ignore empty prompt response or invalid username format 48 | fmt.Fprintln(rw, "Access denied") 49 | return nil 50 | } 51 | 52 | username := string(buff) 53 | return h.startUserShell(ctx, username, rw) 54 | } 55 | 56 | func (h AuthShellHandler) startUserShell(ctx context.Context, user string, rw io.ReadWriter) error { 57 | wrapCtx, cancelFn := context.WithCancel(ctx) 58 | wrapper := NewTerminalWrapper(h.log, rw, h.IOParams) 59 | cmd := runShellAs(ctx, user, h.shellPath, h.shellArgs...) 60 | cmd.Env = os.Environ() 61 | if err := wrapper.Listen(wrapCtx, cmd); err != nil { 62 | return err 63 | } 64 | 65 | h.log.Debugw("login shell start", 66 | "command", cmd.Path, 67 | "args", cmd.Args, 68 | ) 69 | if err := cmd.Start(); err != nil { 70 | return errors.Wrap(err, "failed to start shell instance") 71 | } 72 | 73 | defer cancelFn() 74 | return cmd.Wait() 75 | } 76 | -------------------------------------------------------------------------------- /chars.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import "bytes" 4 | 5 | var ( 6 | CL = byte(0xD) 7 | RF = byte(0xA) 8 | NulChar = byte(0x0) 9 | CLRF = "\r\n" 10 | ) 11 | 12 | func IsCLRF(buff []byte) bool { 13 | if len(buff) != 2 { 14 | return false 15 | } 16 | 17 | return buff[0] == CL && buff[1] == RF 18 | } 19 | 20 | // TrimCLRF trims CL, RF and nul terminator characters 21 | func TrimCLRF(buff []byte) []byte { 22 | return bytes.TrimFunc(buff, func(r rune) bool { 23 | b := byte(r) 24 | return b == CL || b == RF || b == NulChar 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/telshell/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/x1unix/telshell" 9 | "github.com/x1unix/telshell/internal/app" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | const version = "1.1.0" 14 | 15 | type startupParams struct { 16 | debug bool 17 | withAuth bool 18 | replaceLineEndings bool 19 | bufferSize int 20 | addr string 21 | shell string 22 | shellArgs app.FlagsArray 23 | } 24 | 25 | func (s startupParams) ioParams() telshell.IOParams { 26 | return telshell.IOParams{ 27 | BufferSize: uint(s.bufferSize), 28 | ReplaceLineEndings: s.replaceLineEndings, 29 | } 30 | } 31 | 32 | func (s startupParams) getLogger() (*zap.Logger, error) { 33 | if s.debug { 34 | return zap.NewDevelopment() 35 | } 36 | 37 | // Disable stacktrace and caller 38 | cfg := zap.NewDevelopmentConfig() 39 | cfg.EncoderConfig.CallerKey = "" 40 | cfg.EncoderConfig.NameKey = "" 41 | cfg.EncoderConfig.StacktraceKey = "" 42 | return cfg.Build() 43 | } 44 | 45 | func main() { 46 | params := startupParams{ 47 | shellArgs: telshell.ShellArgs[:], 48 | } 49 | 50 | flag.StringVar(¶ms.addr, "addr", ":1000", "Address to listen") 51 | flag.BoolVar(¶ms.withAuth, "auth", false, "Require authorization") 52 | flag.BoolVar(¶ms.debug, "debug", false, "Enable debug mode") 53 | flag.StringVar(¶ms.shell, "shell", telshell.DefaultShell, "Define shell argument") 54 | flag.IntVar(¶ms.bufferSize, "buffer", 64, "Buffer size") 55 | flag.Var(¶ms.shellArgs, "s", "Define shell argument") 56 | flag.BoolVar(¶ms.replaceLineEndings, "replaceLineEndings", true, 57 | "Replace UNIX (\\n) with DOS (\\r\\n) line endings") 58 | 59 | flag.Usage = func() { 60 | fmt.Println("TelShell, version", version) 61 | fmt.Println("Simple telnet shell server") 62 | fmt.Printf("\nUsage of %s:\n", os.Args[0]) 63 | flag.PrintDefaults() 64 | } 65 | flag.Parse() 66 | 67 | // Initialize logger 68 | l, err := params.getLogger() 69 | if err != nil { 70 | fmt.Println(err) 71 | os.Exit(1) 72 | } 73 | 74 | zap.ReplaceGlobals(l) 75 | defer l.Sync() 76 | if err := start(params); err != nil { 77 | zap.S().Fatal(err) 78 | } 79 | } 80 | 81 | func start(p startupParams) error { 82 | ctx, _ := app.GetApplicationContext() 83 | var h telshell.Handler 84 | if p.withAuth { 85 | zap.S().Infof("shell auth enabled, shell is %q", p.shell) 86 | h = telshell.NewAuthShellHandler(p.ioParams(), p.shell, p.shellArgs...) 87 | } else { 88 | zap.S().Infof("shell auth disabled, shell is %q", p.shell) 89 | h = telshell.NewShellHandler(p.ioParams(), p.shell, p.shellArgs...) 90 | } 91 | srv := telshell.NewServer(telshell.WelcomeHandler{}, h) 92 | return srv.Start(ctx, p.addr) 93 | } 94 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x1unix/telshell/785d4b9f80ebb8c31477c58ae66c4b754ed38901/docs/demo.gif -------------------------------------------------------------------------------- /docs/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x1unix/telshell/785d4b9f80ebb8c31477c58ae66c4b754ed38901/docs/preview.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/x1unix/telshell 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/pkg/errors v0.8.1 7 | go.uber.org/atomic v1.5.1 // indirect 8 | go.uber.org/multierr v1.4.0 // indirect 9 | go.uber.org/zap v1.13.0 10 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect 11 | golang.org/x/tools v0.0.0-20200102200121-6de373a2766c // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 6 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 11 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 16 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 17 | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= 18 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 19 | go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM= 20 | go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 21 | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= 22 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 23 | go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= 24 | go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 25 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 26 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 27 | go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= 28 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 31 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 32 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 33 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 34 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= 35 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 36 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 37 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 38 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 39 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 40 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 41 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 46 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 47 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 48 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 49 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 50 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 51 | golang.org/x/tools v0.0.0-20200102200121-6de373a2766c h1:PBxLbymhzlh6kZuAXmeh8JK2tAJR0GM5Q/W71G2QJ40= 52 | golang.org/x/tools v0.0.0-20200102200121-6de373a2766c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 53 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 54 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 58 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 60 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 61 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/pkg/errors" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | // Handler is TCP request handler 15 | type Handler interface { 16 | Handle(ctx context.Context, rw io.ReadWriter) error 17 | } 18 | 19 | // ShellHandler provides shell access via Telnet 20 | type ShellHandler struct { 21 | ioParams IOParams 22 | shellPath string 23 | shellArgs []string 24 | log *zap.SugaredLogger 25 | } 26 | 27 | // NewShellHandler creates a new shell handler 28 | func NewShellHandler(params IOParams, shell string, args ...string) ShellHandler { 29 | return ShellHandler{ 30 | shellPath: shell, 31 | shellArgs: args, 32 | ioParams: params, 33 | log: zap.S().Named("shell"), 34 | } 35 | } 36 | 37 | // Handle implements telshell.Handler 38 | func (s ShellHandler) Handle(ctx context.Context, rw io.ReadWriter) error { 39 | fmt.Fprintf(rw, "Current shell is %q\n", s.shellPath) 40 | 41 | wrapCtx, cancelFn := context.WithCancel(ctx) 42 | wrapper := NewTerminalWrapper(s.log, rw, s.ioParams) 43 | cmd := exec.CommandContext(ctx, s.shellPath, s.shellArgs...) 44 | cmd.Env = os.Environ() 45 | if err := wrapper.Listen(wrapCtx, cmd); err != nil { 46 | return err 47 | } 48 | 49 | if err := cmd.Start(); err != nil { 50 | return errors.Wrap(err, "failed to start shell instance") 51 | } 52 | 53 | defer cancelFn() 54 | return cmd.Wait() 55 | } 56 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PKG_URL="github.com/x1unix/telshell" 3 | URL_DOWNLOAD_PREFIX="https://${PKG_URL}/releases/latest/download" 4 | ISSUE_URL="https://${PKG_URL}/issues" 5 | 6 | OS=$(uname -s | awk '{print tolower($0)}') 7 | ARCH=$(uname -m) 8 | 9 | RED="\033[0;31m" 10 | GREEN='\033[0;32m' 11 | YELLOW='\033[1;33m' 12 | NC='\033[0m' 13 | 14 | warn() { 15 | printf "${YELLOW}${1}${NC}\n" 16 | } 17 | 18 | panic() { 19 | printf "${RED}ERROR: ${1}${NC}\n" >&2 20 | printf "${RED}\nIf you feel that this is an installer issue, you may submit an issue on ${ISSUE_URL}\nInstallation failed. ${NC}\n" 21 | exit 1 22 | } 23 | 24 | is_windows() { 25 | case ${OS} in 26 | cygwin*|mingw32*|msys*|mingw*) 27 | return 0 28 | ;; 29 | *) 30 | return 1 31 | ;; 32 | esac 33 | } 34 | 35 | get_download_url() { 36 | arc=$(get_arch) 37 | os="$OS" 38 | 39 | if is_windows; then 40 | file_suffix=.exe # Set .exe as executable suffix 41 | os=windows # Override os to Windows for MinGW, MSYS, etc 42 | fi 43 | echo "${URL_DOWNLOAD_PREFIX}/telshell_${os}-${arc}${file_suffix}" 44 | } 45 | 46 | get_arch() { 47 | case $ARCH in 48 | "x86_64" | "amd64" ) 49 | echo "amd64" 50 | ;; 51 | "i386" | "i486" | "i586" | "i686") 52 | echo "i386" 53 | ;; 54 | *) 55 | echo "$ARCH" 56 | ;; 57 | esac 58 | } 59 | 60 | main() { 61 | download_dir="${HOME}/bin" 62 | mkdir -p "${download_dir}" 63 | 64 | dest_file="${download_dir}/telshell" 65 | if is_windows; then 66 | dest_file="${dest_file}.exe" 67 | fi 68 | 69 | download_url=$(get_download_url) 70 | echo "-> Downloading '${download_url}'..." 71 | http_status=$(curl --fail --write-out "%{http_code}" -L --show-error --progress -o "${dest_file}" "${download_url}") 72 | case ${http_status} in 73 | "200") 74 | chmod +x "${dest_file}" 75 | echo "-> Successfully installed to '${dest_file}'" 76 | printf "${GREEN}Done!${NC}\n" 77 | exit 0 78 | ;; 79 | "404") 80 | sys_label="${OS} ${ARCH}" 81 | panic "No prebuilt binaries available for ${sys_label}, try to check out release for your platform at https://${PKG_URL}/releases" 82 | ;; 83 | *) 84 | panic "Installation failed, failed to download binary (HTTP error ${http_status})" 85 | ;; 86 | esac 87 | } 88 | 89 | main -------------------------------------------------------------------------------- /internal/app/array_flags.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "strings" 4 | 5 | type FlagsArray []string 6 | 7 | func (i FlagsArray) String() string { 8 | return strings.Join(i, ", ") 9 | } 10 | 11 | func (i *FlagsArray) Set(value string) error { 12 | *i = append(*i, value) 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /internal/app/context.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | ) 10 | 11 | var once = &sync.Once{} 12 | 13 | var ( 14 | ctx context.Context 15 | cancelFunc context.CancelFunc 16 | ) 17 | 18 | // GetApplicationContext returns application context for graceful shutdown 19 | func GetApplicationContext() (context.Context, context.CancelFunc) { 20 | once.Do(func() { 21 | ctx, cancelFunc = context.WithCancel(context.Background()) 22 | 23 | go func() { 24 | signals := []os.Signal{syscall.SIGTERM, syscall.SIGINT, os.Interrupt} 25 | sigChan := make(chan os.Signal, 1) 26 | signal.Notify(sigChan, signals...) 27 | defer signal.Reset(signals...) 28 | <-sigChan 29 | cancelFunc() 30 | }() 31 | }) 32 | 33 | return ctx, cancelFunc 34 | } 35 | -------------------------------------------------------------------------------- /internal/helpers/net.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // IsErrClosing is workaround for detecting usage of closed TCP connection. 8 | // Required, since poll.ErrClosing is private and wrapped in net.OpError. 9 | // 10 | // See: https://github.com/golang/go/issues/10176 11 | func IsErrClosing(err error) bool { 12 | if err == nil { 13 | return false 14 | } 15 | return strings.Contains(err.Error(), "use of closed network connection") 16 | } 17 | -------------------------------------------------------------------------------- /iowrapper.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "os/exec" 8 | 9 | "github.com/x1unix/telshell/internal/helpers" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type IOParams struct { 14 | BufferSize uint 15 | ReplaceLineEndings bool 16 | } 17 | 18 | type TerminalWrapper struct { 19 | IOParams 20 | client io.ReadWriter 21 | log *zap.SugaredLogger 22 | } 23 | 24 | func NewTerminalWrapper(log *zap.SugaredLogger, client io.ReadWriter, params IOParams) TerminalWrapper { 25 | return TerminalWrapper{client: client, log: log, IOParams: params} 26 | } 27 | 28 | func (w TerminalWrapper) Listen(ctx context.Context, cmd *exec.Cmd) error { 29 | return w.listenHost(ctx, cmd) 30 | } 31 | 32 | func (w TerminalWrapper) listenHost(ctx context.Context, cmd *exec.Cmd) error { 33 | stdin, err := cmd.StdinPipe() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | stdout, err := cmd.StdoutPipe() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | stderr, err := cmd.StderrPipe() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | go w.readFromHost(ctx, stdout) 49 | go w.readFromHost(ctx, stderr) 50 | go w.writeToHost(ctx, stdin) 51 | 52 | return nil 53 | } 54 | 55 | func (w TerminalWrapper) readFromHost(ctx context.Context, r io.Reader) { 56 | for { 57 | select { 58 | case <-ctx.Done(): 59 | return 60 | default: 61 | } 62 | 63 | buff := make([]byte, w.BufferSize) 64 | _, _ = r.Read(buff) 65 | 66 | if w.ReplaceLineEndings { 67 | // Replace RF with CRLF line endings 68 | buff = bytes.ReplaceAll(buff, []byte("\n"), []byte{CL, RF}) 69 | } 70 | 71 | w.client.Write(buff) 72 | } 73 | } 74 | 75 | func (w TerminalWrapper) writeToHost(ctx context.Context, dest io.Writer) { 76 | for { 77 | select { 78 | case <-ctx.Done(): 79 | return 80 | default: 81 | } 82 | arr := make([]byte, w.BufferSize) 83 | _, err := w.client.Read(arr) 84 | if err == io.EOF || helpers.IsErrClosing(err) { 85 | return 86 | } 87 | if err != nil { 88 | w.log.Error(err) 89 | continue 90 | } 91 | 92 | arr = w.filterChars(arr) 93 | dest.Write(arr) 94 | } 95 | } 96 | 97 | func (w TerminalWrapper) filterChars(msg []byte) []byte { 98 | filtered := make([]byte, 0, len(msg)) 99 | for _, b := range msg { 100 | switch b { 101 | case CL, NulChar: 102 | continue 103 | default: 104 | } 105 | 106 | filtered = append(filtered, b) 107 | } 108 | 109 | return filtered 110 | } 111 | -------------------------------------------------------------------------------- /platform_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows,!js,!nacl 2 | 3 | package telshell 4 | 5 | import ( 6 | "context" 7 | "github.com/x1unix/telshell/internal/app" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | var ( 13 | DefaultShell = "/bin/sh" 14 | ShellArgs = app.FlagsArray{} 15 | ) 16 | 17 | func runShellAs(ctx context.Context, username, shell string, shellArgs ...string) *exec.Cmd { 18 | args := append([]string{"-k", "-Su", username, shell}, shellArgs...) 19 | cmd := exec.CommandContext(ctx, "sudo", args...) 20 | cmd.Env = os.Environ() 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /platform_windows.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import ( 4 | "context" 5 | "github.com/x1unix/telshell/internal/app" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | DefaultShell = "cmd.exe" 12 | ShellArgs = app.FlagsArray{} 13 | ) 14 | 15 | func runShellAs(ctx context.Context, username, shell string, shellArgs ...string) *exec.Cmd { 16 | args := append([]string{shell}, shellArgs...) 17 | return exec.CommandContext(ctx, "runas", "/user:"+username, strings.Join(args, " ")) 18 | } 19 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/x1unix/telshell/internal/helpers" 7 | "net" 8 | "sync" 9 | 10 | "github.com/pkg/errors" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | var ( 15 | ErrServerStarted = errors.New("server is already started") 16 | ErrServerNotStarted = errors.New("server not started") 17 | ) 18 | 19 | // Server is telnet server used for serving requests 20 | type Server struct { 21 | log *zap.SugaredLogger 22 | handlers []Handler 23 | ln net.Listener 24 | 25 | running bool 26 | runLock sync.RWMutex 27 | 28 | ctx context.Context 29 | cancelFn context.CancelFunc 30 | } 31 | 32 | // NewServer creates new server with specified handlers. 33 | // 34 | // Each handler executes one by one for each new connection 35 | func NewServer(handlers ...Handler) *Server { 36 | return &Server{ 37 | handlers: handlers, 38 | log: zap.S().Named("server"), 39 | } 40 | } 41 | 42 | // SetLogger sets logger 43 | func (s *Server) SetLogger(l *zap.Logger) { 44 | s.log = l.Sugar() 45 | } 46 | 47 | // Start starts server 48 | func (s *Server) Start(ctx context.Context, addr string) (err error) { 49 | if err := s.checkRunState(true); err != nil { 50 | return err 51 | } 52 | 53 | var lc net.ListenConfig 54 | s.ctx, s.cancelFn = context.WithCancel(ctx) 55 | s.ln, err = lc.Listen(s.ctx, "tcp", addr) 56 | if err != nil { 57 | s.running = false 58 | return err 59 | } 60 | 61 | s.markRunState(true) 62 | s.log.Infof("listening on %q...", addr) 63 | 64 | return s.listen() 65 | } 66 | 67 | func (s *Server) checkRunState(isRunning bool) error { 68 | s.runLock.RLock() 69 | defer s.runLock.RUnlock() 70 | 71 | if isRunning == s.running { 72 | if isRunning { 73 | return ErrServerStarted 74 | } 75 | 76 | return ErrServerNotStarted 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (s *Server) markRunState(isRunning bool) { 83 | s.runLock.Lock() 84 | defer s.runLock.Unlock() 85 | s.running = isRunning 86 | } 87 | 88 | func (s *Server) listen() error { 89 | go func() { 90 | <-s.ctx.Done() 91 | s.log.Info("closing server...") 92 | if err := s.shutdown(); err != nil { 93 | s.log.Error(err) 94 | } 95 | }() 96 | 97 | for { 98 | select { 99 | case <-s.ctx.Done(): 100 | s.log.Debug("context is dead, listener stop") 101 | return nil 102 | default: 103 | } 104 | 105 | conn, err := s.ln.Accept() 106 | if err != nil { 107 | if helpers.IsErrClosing(err) { 108 | return nil 109 | } 110 | 111 | if e := s.shutdown(); e != nil { 112 | s.log.Warn(e) 113 | } 114 | return err 115 | } 116 | 117 | go s.handle(conn) 118 | } 119 | } 120 | 121 | func (s *Server) handle(conn net.Conn) { 122 | defer func() { 123 | if r := recover(); r != nil { 124 | s.log.Errorf("recovered from panic: %s", r) 125 | } 126 | }() 127 | 128 | s.log.Debugf("%q: received new connection", conn.RemoteAddr().String()) 129 | 130 | // Execute each handler 131 | for _, handler := range s.handlers { 132 | if err := handler.Handle(s.ctx, conn); err != nil { 133 | s.log.Errorf("handler returned an error: %s", err) 134 | fmt.Fprintf(conn, "ERROR:\t%s\r\n", err.Error()) 135 | } 136 | } 137 | 138 | // Close connection if wasn't closed 139 | _ = conn.Close() 140 | s.log.Debugf("%q: connection closed", conn.RemoteAddr().String()) 141 | } 142 | 143 | func (s *Server) shutdown() error { 144 | s.markRunState(false) 145 | if err := s.ln.Close(); err != nil { 146 | return errors.Wrap(err, "failed to close TCP server") 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // Stop stops the server 153 | func (s *Server) Stop() error { 154 | if err := s.checkRunState(false); err != nil { 155 | return err 156 | } 157 | 158 | s.cancelFn() 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /welcome.go: -------------------------------------------------------------------------------- 1 | package telshell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | type WelcomeHandler struct{} 13 | 14 | func (h WelcomeHandler) Handle(_ context.Context, rw io.ReadWriter) error { 15 | hostname, err := os.Hostname() 16 | if err != nil { 17 | fmt.Fprintln(rw, "ERROR: \t", err.Error()) 18 | return err 19 | } 20 | 21 | banner := fmt.Sprintf("# Welcome to TelShell on %s (%s) #", hostname, runtime.GOOS) 22 | decorations := strings.Repeat("#", len(banner)) 23 | tprintln(rw, CLRF+decorations) 24 | tprintln(rw, banner) 25 | tprintln(rw, decorations+CLRF) 26 | return nil 27 | } 28 | 29 | // tprintln prints message with DOS line endings 30 | func tprintln(rw io.ReadWriter, msg string) { 31 | fmt.Fprint(rw, msg+"\r\n") 32 | } 33 | --------------------------------------------------------------------------------