diff --git a/CHANGELOG.md b/CHANGELOG.md index c9356f1..deebb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## master (unreleased) -* No entry +* The default created user now has the same username as the user starting sshportal (was hardcoded "admin") +* Add Telnet support ## v1.7.1 (2018-01-03) diff --git a/README.md b/README.md index 49ae9cc..c558878 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,18 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion * Audit log (logging every user action) * Host Keys verifications shared across users * Healthcheck user (replying OK to any user) -* ipv4 and ipv6 support -* [`scp`](https://linux.die.net/man/1/scp) support -* [`rsync`](https://linux.die.net/man/1/rsync) support -* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support -* [`sftp`](https://www.ssh.com/ssh/sftp/) support -* [`ssh-agent`](https://www.ssh.com/ssh/agent) support -* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support -* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server) -* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH +* SSH compatibility + * ipv4 and ipv6 support + * [`scp`](https://linux.die.net/man/1/scp) support + * [`rsync`](https://linux.die.net/man/1/rsync) support + * [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support + * [`sftp`](https://www.ssh.com/ssh/sftp/) support + * [`ssh-agent`](https://www.ssh.com/ssh/agent) support + * [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support + * Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server) + * Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients +* SSH to non-SSH proxy + * [Telnet](https://www.ssh.com/ssh/telnet) support ## (Known) limitations diff --git a/db.go b/db.go index 68c6c9b..1a0bd2a 100644 --- a/db.go +++ b/db.go @@ -6,6 +6,7 @@ import ( "log" "net/url" "regexp" + "strconv" "strings" "time" @@ -53,9 +54,10 @@ type Host struct { // FIXME: use uuid for ID gorm.Model Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"` - Addr string `valid:"required"` - User string `valid:"optional"` - Password string `valid:"optional"` + Addr string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL + User string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL + Password string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL + URL string `valid:"optional"` SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client SSHKeyID uint `gorm:"index"` HostKey []byte `sql:"size:10000" valid:"optional"` @@ -156,6 +158,13 @@ const ( ACLActionDeny = "deny" ) +type BastionScheme string + +const ( + BastionSchemeSSH BastionScheme = "ssh" + BastionSchemeTelnet = "telnet" +) + func init() { unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*") @@ -168,37 +177,113 @@ func init() { })) } -func (host *Host) URL() string { - return fmt.Sprintf("%s@%s", host.User, host.Addr) -} +// Host helpers -func NewHostFromURL(rawurl string) (*Host, error) { - if !strings.Contains(rawurl, "://") { - rawurl = "ssh://" + rawurl +func ParseInputURL(input string) (*url.URL, error) { + if !strings.Contains(input, "://") { + input = "ssh://" + input } - u, err := url.Parse(rawurl) + u, err := url.Parse(input) if err != nil { return nil, err } - host := Host{Addr: u.Host} - if !strings.Contains(host.Addr, ":") { - host.Addr += ":22" // add port if not present - } - host.User = "root" // default username - if u.User != nil { - password, _ := u.User.Password() - host.Password = password - host.User = u.User.Username() - } - return &host, nil + return u, nil +} +func (host *Host) DialAddr() string { + return fmt.Sprintf("%s:%d", host.Hostname(), host.Port()) +} +func (host *Host) String() string { + if host.URL != "" { + return host.URL + } else if host.Addr != "" { // to be removed in a future version in favor of URL + if host.Password != "" { + return fmt.Sprintf("ssh://%s:%s@%s", host.User, strings.Repeat("*", 4), host.Addr) + } + return fmt.Sprintf("ssh://%s@%s", host.User, host.Addr) + } + return "" +} +func (host *Host) Scheme() BastionScheme { + if host.URL != "" { + u, err := url.Parse(host.URL) + if err != nil { + return BastionSchemeSSH + } + return BastionScheme(u.Scheme) + } else if host.Addr != "" { + return BastionSchemeSSH + } + return "" } - func (host *Host) Hostname() string { - return strings.Split(host.Addr, ":")[0] + if host.URL != "" { + u, err := url.Parse(host.URL) + if err != nil { + return "" + } + return u.Hostname() + } else if host.Addr != "" { // to be removed in a future version in favor of URL + return strings.Split(host.Addr, ":")[0] + } + return "" +} +func (host *Host) Username() string { + if host.URL != "" { + u, err := url.Parse(host.URL) + if err != nil { + return "root" + } + if u.User != nil { + return u.User.Username() + } + } else if host.User != "" { // to be removed in a future version in favor of URL + return host.User + } + return "root" +} +func (host *Host) Passwd() string { + if host.URL != "" { + u, err := url.Parse(host.URL) + if err != nil { + return "" + } + if u.User != nil { + password, _ := u.User.Password() + return password + } + } else if host.Password != "" { // to be removed in a future version in favor of URL + return host.Password + } + return "" +} +func (host *Host) Port() uint64 { + var portString string + if host.URL != "" { + u, err := url.Parse(host.URL) + if err != nil { + goto defaultPort + } + portString = u.Port() + } else if host.Addr != "" { // to be removed in a future version in favor of URL + portString = strings.Split(host.Addr, ":")[1] + } + if portString != "" { + port, err := strconv.ParseUint(portString, 10, 64) + if err != nil { + goto defaultPort + } + return port + } +defaultPort: + switch host.Scheme() { + case BastionSchemeSSH: + return 22 + case BastionSchemeTelnet: + return 23 + default: + return 0 + } } - -// Host helpers - func HostsPreload(db *gorm.DB) *gorm.DB { return db.Preload("Groups").Preload("SSHKey") } @@ -217,7 +302,7 @@ func HostByName(db *gorm.DB, name string) (*Host, error) { func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) { config := gossh.ClientConfig{ - User: host.User, + User: host.Username(), HostKeyCallback: hk, Auth: []gossh.AuthMethod{}, } @@ -228,8 +313,8 @@ func (host *Host) clientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, e } config.Auth = append(config.Auth, gossh.PublicKeys(signer)) } - if host.Password != "" { - config.Auth = append(config.Auth, gossh.Password(host.Password)) + if host.Passwd() != "" { + config.Auth = append(config.Auth, gossh.Password(host.Passwd())) } if len(config.Auth) == 0 { return nil, fmt.Errorf("no valid authentication method for host %q", host.Name) diff --git a/dbinit.go b/dbinit.go index 9e10c02..a216425 100644 --- a/dbinit.go +++ b/dbinit.go @@ -5,6 +5,8 @@ import ( "io/ioutil" "log" "os" + "os/user" + "strings" "time" "github.com/go-gormigrate/gormigrate" @@ -434,6 +436,28 @@ func dbInit(db *gorm.DB) error { Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") }, + }, { + ID: "28", + Migrate: func(tx *gorm.DB) error { + type Host struct { + // FIXME: use uuid for ID + gorm.Model + Name string `gorm:"size:32"` + Addr string + User string + Password string + URL string + SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` + SSHKeyID uint `gorm:"index"` + HostKey []byte `sql:"size:10000"` + Groups []*HostGroup `gorm:"many2many:host_host_groups;"` + Comment string + } + return tx.AutoMigrate(&Host{}).Error + }, + Rollback: func(tx *gorm.DB) error { + return fmt.Errorf("not implemented") + }, }, }) if err := m.Migrate(); err != nil { @@ -522,9 +546,20 @@ func dbInit(db *gorm.DB) error { if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil { return err } + var username string + if currentUser, err := user.Current(); err == nil { + username = currentUser.Username + } + if username == "" { + username = os.Getenv("USER") + } + username = strings.ToLower(username) + if username == "" { + username = "admin" // fallback username + } user := User{ - Name: "admin", - Email: "admin@sshportal", + Name: username, + Email: fmt.Sprintf("%s@localhost", username), Comment: "created by sshportal", Roles: []*UserRole{&adminRole}, InviteToken: inviteToken, diff --git a/examples/homebrew/sshportal.rb b/examples/homebrew/sshportal.rb new file mode 100644 index 0000000..fd1a529 --- /dev/null +++ b/examples/homebrew/sshportal.rb @@ -0,0 +1,24 @@ +require "language/go" + +class Sshportal < Formula + desc "sshportal: simple, fun and transparent SSH bastion" + homepage "https://github.com/moul/sshportal" + url "https://github.com/moul/sshportal/archive/v1.7.1.tar.gz" + sha256 "4611ae2f30cc595b2fb789bd0c92550533db6d4b63c638dd78cf85517b6aeaf0" + head "https://github.com/moul/sshportal.git" + + depends_on "go" => :build + + def install + ENV["GOPATH"] = buildpath + ENV["GOBIN"] = buildpath + (buildpath/"src/github.com/moul/sshportal").install Dir["*"] + + system "go", "build", "-o", "#{bin}/sshportal", "-v", "github.com/moul/sshportal" + end + + test do + output = shell_output(bin/"sshportal --version") + assert output.include? "sshportal version " + end +end diff --git a/shell.go b/shell.go index 753c390..32f31ce 100644 --- a/shell.go +++ b/shell.go @@ -635,7 +635,7 @@ GLOBAL OPTIONS: { Name: "create", Usage: "Creates a new host", - ArgsUsage: "[:]@[:]", + ArgsUsage: "[scheme://][:]@[:]", Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222", Flags: []cli.Flag{ cli.StringFlag{Name: "name, n", Usage: "Assigns a name to the host"}, @@ -653,10 +653,14 @@ GLOBAL OPTIONS: return err } - host, err := NewHostFromURL(c.Args().First()) + u, err := ParseInputURL(c.Args().First()) if err != nil { return err } + host := &Host{ + URL: u.String(), + Comment: c.String("comment"), + } if c.String("password") != "" { host.Password = c.String("password") } @@ -666,7 +670,6 @@ GLOBAL OPTIONS: host.Name = c.String("name") } // FIXME: check if name already exists - host.Comment = c.String("comment") if _, err := govalidator.ValidateStruct(host); err != nil { return err @@ -773,14 +776,11 @@ GLOBAL OPTIONS: } table := tablewriter.NewWriter(s) - table.SetHeader([]string{"ID", "Name", "URL", "Key", "Pass", "Groups", "Updated", "Created", "Comment"}) + table.SetHeader([]string{"ID", "Name", "URL", "Key", "Groups", "Updated", "Created", "Comment"}) table.SetBorder(false) table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts))) for _, host := range hosts { - authKey, authPass := "", "" - if host.Password != "" { - authPass = "yes" - } + authKey := "" if host.SSHKeyID > 0 { var key SSHKey db.Model(&host).Related(&key) @@ -793,9 +793,8 @@ GLOBAL OPTIONS: table.Append([]string{ fmt.Sprintf("%d", host.ID), host.Name, - host.URL(), + host.String(), authKey, - authPass, strings.Join(groupNames, ", "), humanize.Time(host.UpdatedAt), humanize.Time(host.CreatedAt), @@ -827,7 +826,7 @@ GLOBAL OPTIONS: ArgsUsage: "HOST...", Flags: []cli.Flag{ cli.StringFlag{Name: "name, n", Usage: "Rename the host"}, - cli.StringFlag{Name: "password, p", Usage: "Update/set a password, use \"none\" to unset"}, + cli.StringFlag{Name: "url, u", Usage: "Update connection URL"}, cli.StringFlag{Name: "comment, c", Usage: "Update/set a host comment"}, cli.StringFlag{Name: "key, k", Usage: "Link a `KEY` to use for authentication"}, cli.StringSliceFlag{Name: "assign-group, g", Usage: "Assign the host to a new `HOSTGROUPS`"}, @@ -855,7 +854,7 @@ GLOBAL OPTIONS: for _, host := range hosts { model := tx.Model(&host) // simple fields - for _, fieldname := range []string{"name", "comment", "password"} { + for _, fieldname := range []string{"name", "comment"} { if c.String(fieldname) != "" { if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil { tx.Rollback() @@ -864,6 +863,19 @@ GLOBAL OPTIONS: } } + // url + if c.String("url") != "" { + u, err := ParseInputURL(c.String("url")) + if err != nil { + tx.Rollback() + return err + } + if err := model.Update("url", u.String()).Error; err != nil { + tx.Rollback() + return err + } + } + // associations if c.String("key") != "" { var key SSHKey diff --git a/ssh.go b/ssh.go index 2d86740..1fd0622 100644 --- a/ssh.go +++ b/ssh.go @@ -88,7 +88,7 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh switch newChan.ChannelType() { case "session": default: - // TODO: handle direct-tcp + // TODO: handle direct-tcp (only for ssh scheme) if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil { log.Printf("error: failed to reject channel: %v", err) } @@ -101,7 +101,7 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh switch actx.userType() { case UserTypeBastion: log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%q,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email) - host, clientConfig, err := bastionConfig(ctx) + host, err := HostByName(actx.db, actx.inputUsername) if err != nil { ch, _, err2 := newChan.Accept() if err2 != nil { @@ -113,67 +113,91 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh return } - sess := Session{ - UserID: actx.user.ID, - HostID: host.ID, - Status: SessionStatusActive, - } - if err = actx.db.Create(&sess).Error; err != nil { + switch host.Scheme() { + case BastionSchemeSSH: + clientConfig, err := bastionClientConfig(ctx, host) + if err != nil { + ch, _, err2 := newChan.Accept() + if err2 != nil { + return + } + fmt.Fprintf(ch, "error: %v\n", err) + // FIXME: force close all channels + _ = ch.Close() + return + } + + sess := Session{ + UserID: actx.user.ID, + HostID: host.ID, + Status: SessionStatusActive, + } + if err = actx.db.Create(&sess).Error; err != nil { + ch, _, err2 := newChan.Accept() + if err2 != nil { + return + } + fmt.Fprintf(ch, "error: %v\n", err) + _ = ch.Close() + return + } + + err = bastionsession.ChannelHandler(srv, conn, newChan, ctx, bastionsession.Config{ + Addr: host.DialAddr(), + ClientConfig: clientConfig, + Logs: logsLocation, + }) + + now := time.Now() + sessUpdate := Session{ + Status: SessionStatusClosed, + ErrMsg: fmt.Sprintf("%v", err), + StoppedAt: &now, + } + switch sessUpdate.ErrMsg { + case "lch closed the connection", "rch closed the connection": + sessUpdate.ErrMsg = "" + } + actx.db.Model(&sess).Updates(&sessUpdate) + case BastionSchemeTelnet: + tmpSrv := ssh.Server{ + // PtyCallback: srv.PtyCallback, + Handler: telnetHandler(host), + } + ssh.DefaultChannelHandler(&tmpSrv, conn, newChan, ctx) + default: ch, _, err2 := newChan.Accept() if err2 != nil { return } - fmt.Fprintf(ch, "error: %v\n", err) + fmt.Fprintf(ch, "error: unknown bastion scheme: %q\n", host.Scheme()) + // FIXME: force close all channels _ = ch.Close() - return } - - err = bastionsession.ChannelHandler(srv, conn, newChan, ctx, bastionsession.Config{ - Addr: host.Addr, - Logs: logsLocation, - ClientConfig: clientConfig, - }) - - now := time.Now() - sessUpdate := Session{ - Status: SessionStatusClosed, - ErrMsg: fmt.Sprintf("%v", err), - StoppedAt: &now, - } - switch sessUpdate.ErrMsg { - case "lch closed the connection", "rch closed the connection": - sessUpdate.ErrMsg = "" - } - actx.db.Model(&sess).Updates(&sessUpdate) default: // shell ssh.DefaultChannelHandler(srv, conn, newChan, ctx) } } -func bastionConfig(ctx ssh.Context) (*Host, *gossh.ClientConfig, error) { +func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, error) { actx := ctx.Value(authContextKey).(*authContext) - host, err := HostByName(actx.db, actx.inputUsername) - if err != nil { - return nil, nil, err - } - clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host)) if err != nil { - return nil, nil, err + return nil, err } var tmpUser User if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", actx.user.ID).First(&tmpUser).Error; err != nil { - return nil, nil, err + return nil, err } var tmpHost Host if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil { - return nil, nil, err + return nil, err } action, err2 := CheckACLs(tmpUser, tmpHost) if err2 != nil { - return nil, nil, err2 + return nil, err2 } HostDecrypt(actx.globalContext.String("aes-key"), host) @@ -182,11 +206,11 @@ func bastionConfig(ctx ssh.Context) (*Host, *gossh.ClientConfig, error) { switch action { case ACLActionAllow: case ACLActionDeny: - return nil, nil, fmt.Errorf("you don't have permission to that host") + return nil, fmt.Errorf("you don't have permission to that host") default: - return nil, nil, fmt.Errorf("invalid ACL action: %q", action) + return nil, fmt.Errorf("invalid ACL action: %q", action) } - return host, clientConfig, nil + return clientConfig, nil } func shellHandler(s ssh.Session) { diff --git a/telnet.go b/telnet.go new file mode 100644 index 0000000..c154dde --- /dev/null +++ b/telnet.go @@ -0,0 +1,87 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "time" + + "github.com/gliderlabs/ssh" + oi "github.com/reiver/go-oi" + telnet "github.com/reiver/go-telnet" +) + +type bastionTelnetCaller struct { + ssh ssh.Session +} + +func (caller bastionTelnetCaller) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) { + go func(writer io.Writer, reader io.Reader) { + var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up. + p := buffer[:] + + for { + // Read 1 byte. + n, err := reader.Read(p) + if n <= 0 && nil == err { + continue + } else if n <= 0 && nil != err { + break + } + + if _, err = oi.LongWrite(writer, p); err != nil { + log.Printf("telnet longwrite failed: %v", err) + } + } + }(caller.ssh, r) + + var buffer bytes.Buffer + var p []byte + + var crlfBuffer = [2]byte{'\r', '\n'} + crlf := crlfBuffer[:] + + scanner := bufio.NewScanner(caller.ssh) + scanner.Split(scannerSplitFunc) + + for scanner.Scan() { + buffer.Write(scanner.Bytes()) + buffer.Write(crlf) + + p = buffer.Bytes() + + n, err := oi.LongWrite(w, p) + if nil != err { + break + } + if expected, actual := int64(len(p)), n; expected != actual { + err := fmt.Errorf("transmission problem: tried sending %d bytes, but actually only sent %d bytes", expected, actual) + fmt.Fprint(caller.ssh, err.Error()) + return + } + buffer.Reset() + } + + // Wait a bit to receive data from the server (that we would send to io.Stdout). + time.Sleep(3 * time.Millisecond) +} + +func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF { + return 0, nil, nil + } + return bufio.ScanLines(data, atEOF) +} + +func telnetHandler(host *Host) ssh.Handler { + return func(s ssh.Session) { + // FIXME: log session in db + //actx := s.Context().Value(authContextKey).(*authContext) + caller := bastionTelnetCaller{ssh: s} + if err := telnet.DialToAndCall(host.DialAddr(), caller); err != nil { + fmt.Fprintf(s, "error: %v", err) + } + } +} diff --git a/vendor/github.com/reiver/go-oi/LICENSE b/vendor/github.com/reiver/go-oi/LICENSE new file mode 100644 index 0000000..aeef12f --- /dev/null +++ b/vendor/github.com/reiver/go-oi/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 Charles Iliya Krempeaux :: http://changelog.ca/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/reiver/go-oi/README.md b/vendor/github.com/reiver/go-oi/README.md new file mode 100644 index 0000000..94fa42e --- /dev/null +++ b/vendor/github.com/reiver/go-oi/README.md @@ -0,0 +1,64 @@ +# go-oi + +Package **oi** provides useful tools to be used with the Go programming language's standard "io" package. + +For example, did you know that when you call the `Write` method on something that fits the `io.Writer` +interface, that it is possible that not everything was be written?! + +I.e., that a _**short write**_ happened. + +That just doing the following is (in general) **not** enough: +``` +n, err := writer.Write(p) +``` + +That, for example, you should be checking if `err == io.ErrShortWrite`, and then maybe calling the `Write` +method again but only with what didn't get written. + +For a simple example of this (that actually is **not** sufficient to solve this problem, but illustrates +the direction you would need to go to solve this problem is): +``` +n, err := w.Write(p) + +if io.ErrShortWrite == err { + n2, err2 := w.Write(p[n:]) +} +``` + +Note that the second call to the `Write` method passed `p[n:]` (instead of just `p`), to account for the `n` bytes +already being written (with the first call to the `Write` method). + +A more "production quality" version of this would likely be in a loop, but such that that the loop had "guards" +against looping forever, and also possibly looping for "too long". + +Well package **oi** provides tools that helps you deal with this and other problems. For example, you +can handle a _**short write**_ with the following **oi** func: +``` +n, err := oi.LongWrite(writer, p) +``` + + +## Documention + +Online documentation, which includes examples, can be found at: http://godoc.org/github.com/reiver/go-oi + +[![GoDoc](https://godoc.org/github.com/reiver/go-oi?status.svg)](https://godoc.org/github.com/reiver/go-oi) + + +## Example +``` +import ( + "github.com/reiver/go-oi" +) + +// ... + +p := []byte("It is important that this message be written!!!") + +n, err := oi.LongWrite(writer, p) +if nil != err { + //@TODO: Handle error. + return +} + +``` diff --git a/vendor/github.com/reiver/go-oi/doc.go b/vendor/github.com/reiver/go-oi/doc.go new file mode 100644 index 0000000..1b3a555 --- /dev/null +++ b/vendor/github.com/reiver/go-oi/doc.go @@ -0,0 +1,39 @@ +/* +Package oi provides useful tools to be used with Go's standard "io" package. + +For example, did you know that when you call the Write method on something that fits the io.Writer +interface, that it is possible that not everything was be written?! + +I.e., that a 'short write' happened. + +That just doing the following is (in general) not enough: + + n, err := writer.Write(p) + +That, for example, you should be checking if "err == io.ErrShortWrite", and then maybe calling the Write +method again but only with what didn't get written. + +For a simple example of this (that actually is not sufficient to solve this problem, but illustrates +the direction you would need to go to solve this problem is): + + n, err := w.Write(p) + + if io.ErrShortWrite == err { + n2, err2 := w.Write(p[n:]) + } + +Note that the second call to the Write method passed "p[n:]" (instead of just "p"), to account for the "n" bytes +already being written (with the first call to the `Write` method). + +A more "production quality" version of this would likely be in a loop, but such that that the loop had "guards" +against looping forever, and also possibly looping for "too long". + + +Well package oi provides tools that helps you deal with this and other problems. For example, you +can handle a 'short write' with the following oi func: +``` +n, err := oi.LongWrite(writer, p) +``` + +*/ +package oi diff --git a/vendor/github.com/reiver/go-oi/longwrite.go b/vendor/github.com/reiver/go-oi/longwrite.go new file mode 100644 index 0000000..27e7234 --- /dev/null +++ b/vendor/github.com/reiver/go-oi/longwrite.go @@ -0,0 +1,39 @@ +package oi + + +import ( + "io" +) + + +// LongWrite tries to write the bytes from 'p' to the writer 'w', such that it deals +// with "short writes" where w.Write would return an error of io.ErrShortWrite and +// n < len(p). +// +// Note that LongWrite still could return the error io.ErrShortWrite; but this +// would only be after trying to handle the io.ErrShortWrite a number of times, and +// then eventually giving up. +func LongWrite(w io.Writer, p []byte) (int64, error) { + + numWritten := int64(0) + for { +//@TODO: Should check to make sure this doesn't get stuck in an infinite loop writting nothing! + n, err := w.Write(p) + numWritten += int64(n) + if nil != err && io.ErrShortWrite != err { + return numWritten, err + } + + if !(n < len(p)) { + break + } + + p = p[n:] + + if len(p) < 1 { + break + } + } + + return numWritten, nil +} diff --git a/vendor/github.com/reiver/go-oi/longwritebyte.go b/vendor/github.com/reiver/go-oi/longwritebyte.go new file mode 100644 index 0000000..022e418 --- /dev/null +++ b/vendor/github.com/reiver/go-oi/longwritebyte.go @@ -0,0 +1,28 @@ +package oi + + +import ( + "io" +) + + +// LongWriteByte trys to write the byte from 'b' to the writer 'w', such that it deals +// with "short writes" where w.Write would return an error of io.ErrShortWrite and +// n < 1. +// +// Note that LongWriteByte still could return the error io.ErrShortWrite; but this +// would only be after trying to handle the io.ErrShortWrite a number of times, and +// then eventually giving up. +func LongWriteByte(w io.Writer, b byte) error { + var buffer [1]byte + p := buffer[:] + + buffer[0] = b + + numWritten, err := LongWrite(w, p) + if 1 != numWritten { + return io.ErrShortWrite + } + + return err +} diff --git a/vendor/github.com/reiver/go-oi/longwritestring.go b/vendor/github.com/reiver/go-oi/longwritestring.go new file mode 100644 index 0000000..504097a --- /dev/null +++ b/vendor/github.com/reiver/go-oi/longwritestring.go @@ -0,0 +1,39 @@ +package oi + + +import ( + "io" +) + + +// LongWriteString tries to write the bytes from 's' to the writer 'w', such that it deals +// with "short writes" where w.Write (or w.WriteString) would return an error of io.ErrShortWrite +// and n < len(s). +// +// Note that LongWriteString still could return the error io.ErrShortWrite; but this +// would only be after trying to handle the io.ErrShortWrite a number of times, and +// then eventually giving up. +func LongWriteString(w io.Writer, s string) (int64, error) { + + numWritten := int64(0) + for { +//@TODO: Should check to make sure this doesn't get stuck in an infinite loop writting nothing! + n, err := io.WriteString(w, s) + numWritten += int64(n) + if nil != err && io.ErrShortWrite != err { + return numWritten, err + } + + if !(n < len(s)) { + break + } + + s = s[n:] + + if len(s) < 1 { + break + } + } + + return numWritten, nil +} diff --git a/vendor/github.com/reiver/go-oi/writenopcloser.go b/vendor/github.com/reiver/go-oi/writenopcloser.go new file mode 100644 index 0000000..20a1eef --- /dev/null +++ b/vendor/github.com/reiver/go-oi/writenopcloser.go @@ -0,0 +1,38 @@ +package oi + + +import ( + "io" +) + + +// WriteNopCloser takes an io.Writer and returns an io.WriteCloser where +// calling the Write method on the returned io.WriterCloser calls the +// Write method on the io.Writer it received, but whre calling the Close +// method on the returned io.WriterCloser does "nothing" (i.e., is a "nop"). +// +// This is useful in cases where an io.WriteCloser is expected, but you +// only have an io.Writer (where closing doesn't make sense) and you +// need to make your io.Writer fit. (I.e., you need an adaptor.) +func WriteNopCloser(w io.Writer) io.WriteCloser { + wc := internalWriteNopCloser{ + writer:w, + } + + return &wc +} + + +type internalWriteNopCloser struct { + writer io.Writer +} + + +func (wc * internalWriteNopCloser) Write(p []byte) (n int, err error) { + return wc.writer.Write(p) +} + + +func (wc * internalWriteNopCloser) Close() error { + return nil +} diff --git a/vendor/github.com/reiver/go-telnet/LICENSE b/vendor/github.com/reiver/go-telnet/LICENSE new file mode 100644 index 0000000..aeef12f --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 Charles Iliya Krempeaux :: http://changelog.ca/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/reiver/go-telnet/README.md b/vendor/github.com/reiver/go-telnet/README.md new file mode 100644 index 0000000..86b0bee --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/README.md @@ -0,0 +1,252 @@ +# go-telnet + +Package **telnet** provides TELNET and TELNETS client and server implementations, for the Go programming language. + + +The **telnet** package provides an API in a style similar to the "net/http" library that is part of the Go standard library, including support for "middleware". + + +(TELNETS is *secure TELNET*, with the TELNET protocol over a secured TLS (or SSL) connection.) + + +## Documention + +Online documentation, which includes examples, can be found at: http://godoc.org/github.com/reiver/go-telnet + +[![GoDoc](https://godoc.org/github.com/reiver/go-telnet?status.svg)](https://godoc.org/github.com/reiver/go-telnet) + + +## Very Simple TELNET Server Example + +A very very simple TELNET server is shown in the following code. + +This particular TELNET server just echos back to the user anything they "submit" to the server. + +(By default, a TELNET client does *not* send anything to the server until the [Enter] key is pressed. +"Submit" means typing something and then pressing the [Enter] key.) + +``` +package main + +import ( + "github.com/reiver/go-telnet" +) + +func main() { + + var handler telnet.Handler = telnet.EchoHandler + + err := telnet.ListenAndServe(":5555", handler) + if nil != err { + //@TODO: Handle this error better. + panic(err) + } +} + +``` + +If you wanted to test out this very very simple TELNET server, if you were on the same computer it was +running, you could connect to it using the bash command: +``` +telnet localhost 5555 +``` +(Note that we use the same TCP port number -- "5555" -- as we had in our code. That is important, as the +value used by your TELNET server and the value used by your TELNET client **must** match.) + + +## Very Simple (Secure) TELNETS Server Example + +TELNETS is the secure version of TELNET. + +The code to make a TELNETS server is very similar to the code to make a TELNET server. +(The difference between we use the `telnet.ListenAndServeTLS` func instead of the +`telnet.ListenAndServe` func.) + +``` +package main + +import ( + "github.com/reiver/go-telnet" +) + +func main() { + + var handler telnet.Handler = telnet.EchoHandler + + err := telnet.ListenAndServeTLS(":5555", "cert.pem", "key.pem", handler) + if nil != err { + //@TODO: Handle this error better. + panic(err) + } +} + +``` + +If you wanted to test out this very very simple TELNETS server, get the `telnets` client program from here: +https://github.com/reiver/telnets + + +## TELNET Client Example: +``` +package main + +import ( + "github.com/reiver/go-telnet" +) + +func main() { + var caller Caller = telnet.StandardCaller + + //@TOOD: replace "example.net:5555" with address you want to connect to. + telnet.DialToAndCall("example.net:5555", caller) +} +``` + + +## TELNETS Client Example: +``` +package main + +import ( + "github.com/reiver/go-telnet" + + "crypto/tls" +) + +func main() { + //@TODO: Configure the TLS connection here, if you need to. + tlsConfig := &tls.Config{} + + var caller Caller = telnet.StandardCaller + + //@TOOD: replace "example.net:5555" with address you want to connect to. + telnet.DialToAndCallTLS("example.net:5555", caller, tlsConfig) +} +``` + + +## TELNET Shell Server Example + +A more useful TELNET servers can be made using the `"github.com/reiver/go-telnet/telsh"` sub-package. + +For example: +``` +package main + + +import ( + "github.com/reiver/go-oi" + "github.com/reiver/go-telnet" + "github.com/reiver/go-telnet/telsh" + + "fmt" + "io" + "time" +) + + +func fiveHandler(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser)error { + oi.LongWriteString(stdout, "The number FIVE looks like this: 5\r\n") + + return nil +} + +func fiveProducer(ctx telsh.Context, name string, args ...string) telsh.Handler{ + return telsh.PromoteHandlerFunc(fiveHandler) +} + + + +func danceHandler(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser)error { + for i:=0; i<20; i++ { + oi.LongWriteString(stdout, "\r⠋") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠙") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠹") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠸") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠼") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠴") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠦") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠧") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠇") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠏") + time.Sleep(50*time.Millisecond) + } + oi.LongWriteString(stdout, "\r \r\n") + + return nil +} + +func danceProducer(ctx telsh.Context, name string, args ...string) telsh.Handler{ + + return telsh.PromoteHandlerFunc(danceHandler) +} + + +func main() { + + shellHandler := telsh.NewShellHandler() + + shellHandler.WelcomeMessage = ` + __ __ ______ _ _____ ____ __ __ ______ + \ \ / /| ____|| | / ____| / __ \ | \/ || ____| + \ \ /\ / / | |__ | | | | | | | || \ / || |__ + \ \/ \/ / | __| | | | | | | | || |\/| || __| + \ /\ / | |____ | |____ | |____ | |__| || | | || |____ + \/ \/ |______||______| \_____| \____/ |_| |_||______| + +` + + + // Register the "five" command. + commandName := "five" + commandProducer := telsh.ProducerFunc(fiveProducer) + + shellHandler.Register(commandName, commandProducer) + + + + // Register the "dance" command. + commandName = "dance" + commandProducer = telsh.ProducerFunc(danceProducer) + + shellHandler.Register(commandName, commandProducer) + + + + shellHandler.Register("dance", telsh.ProducerFunc(danceProducer)) + + addr := ":5555" + if err := telnet.ListenAndServe(addr, shellHandler); nil != err { + panic(err) + } +} +``` + +TELNET servers made using the `"github.com/reiver/go-telnet/telsh"` sub-package will often be more useful +as it makes it easier for you to create a *shell* interface. + + +# More Information + +There is a lot more information about documentation on all this here: http://godoc.org/github.com/reiver/go-telnet + +(You should really read those.) + diff --git a/vendor/github.com/reiver/go-telnet/caller.go b/vendor/github.com/reiver/go-telnet/caller.go new file mode 100644 index 0000000..7895514 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/caller.go @@ -0,0 +1,18 @@ +package telnet + + +// A Caller represents the client end of a TELNET (or TELNETS) connection. +// +// Writing data to the Writer passed as an argument to the CallTELNET method +// will send data to the TELNET (or TELNETS) server. +// +// Reading data from the Reader passed as an argument to the CallTELNET method +// will receive data from the TELNET server. +// +// The Writer's Write method sends "escaped" TELNET (and TELNETS) data. +// +// The Reader's Read method "un-escapes" TELNET (and TELNETS) data, and filters +// out TELNET (and TELNETS) command sequences. +type Caller interface { + CallTELNET(Context, Writer, Reader) +} diff --git a/vendor/github.com/reiver/go-telnet/client.go b/vendor/github.com/reiver/go-telnet/client.go new file mode 100644 index 0000000..39b4d89 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/client.go @@ -0,0 +1,100 @@ +package telnet + + +import ( + "crypto/tls" +) + + +func DialAndCall(caller Caller) error { + conn, err := Dial() + if nil != err { + return err + } + + client := &Client{Caller:caller} + + return client.Call(conn) +} + + +func DialToAndCall(srvAddr string, caller Caller) error { + conn, err := DialTo(srvAddr) + if nil != err { + return err + } + + client := &Client{Caller:caller} + + return client.Call(conn) +} + + +func DialAndCallTLS(caller Caller, tlsConfig *tls.Config) error { + conn, err := DialTLS(tlsConfig) + if nil != err { + return err + } + + client := &Client{Caller:caller} + + return client.Call(conn) +} + +func DialToAndCallTLS(srvAddr string, caller Caller, tlsConfig *tls.Config) error { + conn, err := DialToTLS(srvAddr, tlsConfig) + if nil != err { + return err + } + + client := &Client{Caller:caller} + + return client.Call(conn) +} + + +type Client struct { + Caller Caller + + Logger Logger +} + + +func (client *Client) Call(conn *Conn) error { + + logger := client.logger() + + + caller := client.Caller + if nil == caller { + logger.Debug("Defaulted caller to StandardCaller.") + caller = StandardCaller + } + + + var ctx Context = NewContext().InjectLogger(logger) + + var w Writer = conn + var r Reader = conn + + caller.CallTELNET(ctx, w, r) + conn.Close() + + + return nil +} + + +func (client *Client) logger() Logger { + logger := client.Logger + if nil == logger { + logger = internalDiscardLogger{} + } + + return logger +} + + +func (client *Client) SetAuth(username string) { +//@TODO: ################################################# +} diff --git a/vendor/github.com/reiver/go-telnet/conn.go b/vendor/github.com/reiver/go-telnet/conn.go new file mode 100644 index 0000000..31a9659 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/conn.go @@ -0,0 +1,148 @@ +package telnet + + +import ( + "crypto/tls" + "net" +) + + +type Conn struct { + conn interface { + Read(b []byte) (n int, err error) + Write(b []byte) (n int, err error) + Close() error + LocalAddr() net.Addr + RemoteAddr() net.Addr + } + dataReader *internalDataReader + dataWriter *internalDataWriter +} + + +// Dial makes a (un-secure) TELNET client connection to the system's 'loopback address' +// (also known as "localhost" or 127.0.0.1). +// +// If a secure connection is desired, use `DialTLS` instead. +func Dial() (*Conn, error) { + return DialTo("") +} + +// DialTo makes a (un-secure) TELNET client connection to the the address specified by +// 'addr'. +// +// If a secure connection is desired, use `DialToTLS` instead. +func DialTo(addr string) (*Conn, error) { + + const network = "tcp" + + if "" == addr { + addr = "127.0.0.1:telnet" + } + + conn, err := net.Dial(network, addr) + if nil != err { + return nil, err + } + + dataReader := newDataReader(conn) + dataWriter := newDataWriter(conn) + + clientConn := Conn{ + conn:conn, + dataReader:dataReader, + dataWriter:dataWriter, + } + + return &clientConn, nil +} + + +// DialTLS makes a (secure) TELNETS client connection to the system's 'loopback address' +// (also known as "localhost" or 127.0.0.1). +func DialTLS(tlsConfig *tls.Config) (*Conn, error) { + return DialToTLS("", tlsConfig) +} + +// DialToTLS makes a (secure) TELNETS client connection to the the address specified by +// 'addr'. +func DialToTLS(addr string, tlsConfig *tls.Config) (*Conn, error) { + + const network = "tcp" + + if "" == addr { + addr = "127.0.0.1:telnets" + } + + conn, err := tls.Dial(network, addr, tlsConfig) + if nil != err { + return nil, err + } + + dataReader := newDataReader(conn) + dataWriter := newDataWriter(conn) + + clientConn := Conn{ + conn:conn, + dataReader:dataReader, + dataWriter:dataWriter, + } + + return &clientConn, nil +} + + + +// Close closes the client connection. +// +// Typical usage might look like: +// +// telnetsClient, err = telnet.DialToTLS(addr, tlsConfig) +// if nil != err { +// //@TODO: Handle error. +// return err +// } +// defer telnetsClient.Close() +func (clientConn *Conn) Close() error { + return clientConn.conn.Close() +} + + +// Read receives `n` bytes sent from the server to the client, +// and "returns" into `p`. +// +// Note that Read can only be used for receiving TELNET (and TELNETS) data from the server. +// +// TELNET (and TELNETS) command codes cannot be received using this method, as Read deals +// with TELNET (and TELNETS) "unescaping", and (when appropriate) filters out TELNET (and TELNETS) +// command codes. +// +// Read makes Client fit the io.Reader interface. +func (clientConn *Conn) Read(p []byte) (n int, err error) { + return clientConn.dataReader.Read(p) +} + + +// Write sends `n` bytes from 'p' to the server. +// +// Note that Write can only be used for sending TELNET (and TELNETS) data to the server. +// +// TELNET (and TELNETS) command codes cannot be sent using this method, as Write deals with +// TELNET (and TELNETS) "escaping", and will properly "escape" anything written with it. +// +// Write makes Conn fit the io.Writer interface. +func (clientConn *Conn) Write(p []byte) (n int, err error) { + return clientConn.dataWriter.Write(p) +} + + +// LocalAddr returns the local network address. +func (clientConn *Conn) LocalAddr() net.Addr { + return clientConn.conn.LocalAddr() +} + + +// RemoteAddr returns the remote network address. +func (clientConn *Conn) RemoteAddr() net.Addr { + return clientConn.conn.RemoteAddr() +} diff --git a/vendor/github.com/reiver/go-telnet/context.go b/vendor/github.com/reiver/go-telnet/context.go new file mode 100644 index 0000000..0f8ba94 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/context.go @@ -0,0 +1,31 @@ +package telnet + + +type Context interface { + Logger() Logger + + InjectLogger(Logger) Context +} + + +type internalContext struct { + logger Logger +} + + +func NewContext() Context { + ctx := internalContext{} + + return &ctx +} + + +func (ctx *internalContext) Logger() Logger { + return ctx.logger +} + +func (ctx *internalContext) InjectLogger(logger Logger) Context { + ctx.logger = logger + + return ctx +} diff --git a/vendor/github.com/reiver/go-telnet/data_reader.go b/vendor/github.com/reiver/go-telnet/data_reader.go new file mode 100644 index 0000000..6367df1 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/data_reader.go @@ -0,0 +1,173 @@ +package telnet + + +import ( + "bufio" + "errors" + "io" +) + + +var ( + errCorrupted = errors.New("Corrupted") +) + + +// An internalDataReader deals with "un-escaping" according to the TELNET protocol. +// +// In the TELNET protocol byte value 255 is special. +// +// The TELNET protocol calls byte value 255: "IAC". Which is short for "interpret as command". +// +// The TELNET protocol also has a distinction between 'data' and 'commands'. +// +//(DataReader is targetted toward TELNET 'data', not TELNET 'commands'.) +// +// If a byte with value 255 (=IAC) appears in the data, then it must be escaped. +// +// Escaping byte value 255 (=IAC) in the data is done by putting 2 of them in a row. +// +// So, for example: +// +// []byte{255} -> []byte{255, 255} +// +// Or, for a more complete example, if we started with the following: +// +// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20} +// +// ... TELNET escaping would produce the following: +// +// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20} +// +// (Notice that each "255" in the original byte array became 2 "255"s in a row.) +// +// DataReader deals with "un-escaping". In other words, it un-does what was shown +// in the examples. +// +// So, for example, it does this: +// +// []byte{255, 255} -> []byte{255} +// +// And, for example, goes from this: +// +// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20} +// +// ... to this: +// +// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20} +type internalDataReader struct { + wrapped io.Reader + buffered *bufio.Reader +} + + +// newDataReader creates a new DataReader reading from 'r'. +func newDataReader(r io.Reader) *internalDataReader { + buffered := bufio.NewReader(r) + + reader := internalDataReader{ + wrapped:r, + buffered:buffered, + } + + return &reader +} + + +// Read reads the TELNET escaped data from the wrapped io.Reader, and "un-escapes" it into 'data'. +func (r *internalDataReader) Read(data []byte) (n int, err error) { + + const IAC = 255 + + const SB = 250 + const SE = 240 + + const WILL = 251 + const WONT = 252 + const DO = 253 + const DONT = 254 + + p := data + + for len(p) > 0 { + var b byte + + b, err = r.buffered.ReadByte() + if nil != err { + return n, err + } + + if IAC == b { + var peeked []byte + + peeked, err = r.buffered.Peek(1) + if nil != err { + return n, err + } + + switch peeked[0] { + case WILL, WONT, DO, DONT: + _, err = r.buffered.Discard(2) + if nil != err { + return n, err + } + case IAC: + p[0] = IAC + n++ + p = p[1:] + + _, err = r.buffered.Discard(1) + if nil != err { + return n, err + } + case SB: + for { + var b2 byte + b2, err = r.buffered.ReadByte() + if nil != err { + return n, err + } + + if IAC == b2 { + peeked, err = r.buffered.Peek(1) + if nil != err { + return n, err + } + + if IAC == peeked[0] { + _, err = r.buffered.Discard(1) + if nil != err { + return n, err + } + } + + if SE == peeked[0] { + _, err = r.buffered.Discard(1) + if nil != err { + return n, err + } + break + } + } + } + case SE: + _, err = r.buffered.Discard(1) + if nil != err { + return n, err + } + default: + // If we get in here, this is not following the TELNET protocol. +//@TODO: Make a better error. + err = errCorrupted + return n, err + } + } else { + + p[0] = b + n++ + p = p[1:] + } + } + + return n, nil +} diff --git a/vendor/github.com/reiver/go-telnet/data_writer.go b/vendor/github.com/reiver/go-telnet/data_writer.go new file mode 100644 index 0000000..af99557 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/data_writer.go @@ -0,0 +1,142 @@ +package telnet + + +import ( + "github.com/reiver/go-oi" + + "bytes" + "errors" + "io" +) + + +var iaciac []byte = []byte{255, 255} + +var errOverflow = errors.New("Overflow") +var errPartialIACIACWrite = errors.New("Partial IAC IAC write.") + + +// An internalDataWriter deals with "escaping" according to the TELNET (and TELNETS) protocol. +// +// In the TELNET (and TELNETS) protocol byte value 255 is special. +// +// The TELNET (and TELNETS) protocol calls byte value 255: "IAC". Which is short for "interpret as command". +// +// The TELNET (and TELNETS) protocol also has a distinction between 'data' and 'commands'. +// +//(DataWriter is targetted toward TELNET (and TELNETS) 'data', not TELNET (and TELNETS) 'commands'.) +// +// If a byte with value 255 (=IAC) appears in the data, then it must be escaped. +// +// Escaping byte value 255 (=IAC) in the data is done by putting 2 of them in a row. +// +// So, for example: +// +// []byte{255} -> []byte{255, 255} +// +// Or, for a more complete example, if we started with the following: +// +// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20} +// +// ... TELNET escaping would produce the following: +// +// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20} +// +// (Notice that each "255" in the original byte array became 2 "255"s in a row.) +// +// internalDataWriter takes care of all this for you, so you do not have to do it. +type internalDataWriter struct { + wrapped io.Writer +} + + +// newDataWriter creates a new internalDataWriter writing to 'w'. +// +// 'w' receives what is written to the *internalDataWriter but escaped according to +// the TELNET (and TELNETS) protocol. +// +// I.e., byte 255 (= IAC) gets encoded as 255, 255. +// +// For example, if the following it written to the *internalDataWriter's Write method: +// +// []byte{1, 55, 2, 155, 3, 255, 4, 40, 255, 30, 20} +// +// ... then (conceptually) the following is written to 'w's Write method: +// +// []byte{1, 55, 2, 155, 3, 255, 255, 4, 40, 255, 255, 30, 20} +// +// (Notice that each "255" in the original byte array became 2 "255"s in a row.) +// +// *internalDataWriter takes care of all this for you, so you do not have to do it. +func newDataWriter(w io.Writer) *internalDataWriter { + writer := internalDataWriter{ + wrapped:w, + } + + return &writer +} + + +// Write writes the TELNET (and TELNETS) escaped data for of the data in 'data' to the wrapped io.Writer. +func (w *internalDataWriter) Write(data []byte) (n int, err error) { + var n64 int64 + + n64, err = w.write64(data) + n = int(n64) + if int64(n) != n64 { + panic(errOverflow) + } + + return n, err +} + + +func (w *internalDataWriter) write64(data []byte) (n int64, err error) { + + if len(data) <= 0 { + return 0, nil + } + + const IAC = 255 + + var buffer bytes.Buffer + for _, datum := range data { + + if IAC == datum { + + if buffer.Len() > 0 { + var numWritten int64 + + numWritten, err = oi.LongWrite(w.wrapped, buffer.Bytes()) + n += numWritten + if nil != err { + return n, err + } + buffer.Reset() + } + + + var numWritten int64 + //@TODO: Should we worry about "iaciac" potentially being modified by the .Write()? + numWritten, err = oi.LongWrite(w.wrapped, iaciac) + if int64(len(iaciac)) != numWritten { + //@TODO: Do we really want to panic() here? + panic(errPartialIACIACWrite) + } + n += 1 + if nil != err { + return n, err + } + } else { + buffer.WriteByte(datum) // The returned error is always nil, so we ignore it. + } + } + + if buffer.Len() > 0 { + var numWritten int64 + numWritten, err = oi.LongWrite(w.wrapped, buffer.Bytes()) + n += numWritten + } + + return n, err +} diff --git a/vendor/github.com/reiver/go-telnet/discard_logger.go b/vendor/github.com/reiver/go-telnet/discard_logger.go new file mode 100644 index 0000000..c334926 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/discard_logger.go @@ -0,0 +1,21 @@ +package telnet + + +type internalDiscardLogger struct{} + +func (internalDiscardLogger) Debug(...interface{}) {} +func (internalDiscardLogger) Debugf(string, ...interface{}) {} +func (internalDiscardLogger) Debugln(...interface{}) {} + +func (internalDiscardLogger) Error(...interface{}) {} +func (internalDiscardLogger) Errorf(string, ...interface{}) {} +func (internalDiscardLogger) Errorln(...interface{}) {} + +func (internalDiscardLogger) Trace(...interface{}) {} +func (internalDiscardLogger) Tracef(string, ...interface{}) {} +func (internalDiscardLogger) Traceln(...interface{}) {} + +func (internalDiscardLogger) Warn(...interface{}) {} +func (internalDiscardLogger) Warnf(string, ...interface{}) {} +func (internalDiscardLogger) Warnln(...interface{}) {} + diff --git a/vendor/github.com/reiver/go-telnet/doc.go b/vendor/github.com/reiver/go-telnet/doc.go new file mode 100644 index 0000000..5761691 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/doc.go @@ -0,0 +1,450 @@ +/* +Package telnet provides TELNET and TELNETS client and server implementations +in a style similar to the "net/http" library that is part of the Go standard library, +including support for "middleware"; TELNETS is secure TELNET, with the TELNET protocol +over a secured TLS (or SSL) connection. + + +Example TELNET Server + +ListenAndServe starts a (un-secure) TELNET server with a given address and handler. + + handler := telnet.EchoHandler + + err := telnet.ListenAndServe(":23", handler) + if nil != err { + panic(err) + } + + +Example TELNETS Server + +ListenAndServeTLS starts a (secure) TELNETS server with a given address and handler, +using the specified "cert.pem" and "key.pem" files. + + handler := telnet.EchoHandler + + err := telnet.ListenAndServeTLS(":992", "cert.pem", "key.pem", handler) + if nil != err { + panic(err) + } + + +Example TELNET Client: + +DialToAndCall creates a (un-secure) TELNET client, which connects to a given address using the specified caller. + + package main + + import ( + "github.com/reiver/go-telnet" + ) + + func main() { + var caller Caller = telnet.StandardCaller + + //@TOOD: replace "example.net:23" with address you want to connect to. + telnet.DialToAndCall("example.net:23", caller) + } + + +Example TELNETS Client: + +DialToAndCallTLS creates a (secure) TELNETS client, which connects to a given address using the specified caller. + + package main + + import ( + "github.com/reiver/go-telnet" + + "crypto/tls" + ) + + func main() { + //@TODO: Configure the TLS connection here, if you need to. + tlsConfig := &tls.Config{} + + var caller Caller = telnet.StandardCaller + + //@TOOD: replace "example.net:992" with address you want to connect to. + telnet.DialToAndCallTLS("example.net:992", caller, tlsConfig) + } + + +TELNET vs TELNETS + +If you are communicating over the open Internet, you should be using (the secure) TELNETS protocol and ListenAndServeTLS. + +If you are communicating just on localhost, then using just (the un-secure) TELNET protocol and telnet.ListenAndServe may be OK. + +If you are not sure which to use, use TELNETS and ListenAndServeTLS. + + +Example TELNET Shell Server + +The previous 2 exaple servers were very very simple. Specifically, they just echoed back whatever +you submitted to it. + +If you typed: + + Apple Banana Cherry\r\n + +... it would send back: + + Apple Banana Cherry\r\n + +(Exactly the same data you sent it.) + +A more useful TELNET server can be made using the "github.com/reiver/go-telnet/telsh" sub-package. + +The `telsh` sub-package provides "middleware" that enables you to create a "shell" interface (also +called a "command line interface" or "CLI") which most people would expect when using TELNET OR TELNETS. + +For example: + + + package main + + + import ( + "github.com/reiver/go-oi" + "github.com/reiver/go-telnet" + "github.com/reiver/go-telnet/telsh" + + "time" + ) + + + func main() { + + shellHandler := telsh.NewShellHandler() + + commandName := "date" + shellHandler.Register(commandName, danceProducer) + + commandName = "animate" + shellHandler.Register(commandName, animateProducer) + + addr := ":23" + if err := telnet.ListenAndServe(addr, shellHandler); nil != err { + panic(err) + } + } + +Note that in the example, so far, we have registered 2 commands: `date` and `animate`. + +For this to actually work, we need to have code for the `date` and `animate` commands. + +The actual implemenation for the `date` command could be done like the following: + + func dateHandlerFunc(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error { + const layout = "Mon Jan 2 15:04:05 -0700 MST 2006" + s := time.Now().Format(layout) + + if _, err := oi.LongWriteString(stdout, s); nil != err { + return err + } + + return nil + } + + + func dateProducerFunc(ctx telsh.Context, name string, args ...string) telsh.Handler{ + return telsh.PromoteHandlerFunc(dateHandler) + } + + + var dateProducer = ProducerFunc(dateProducerFunc) + +Note that your "real" work is in the `dateHandlerFunc` func. + +And the actual implementation for the `animate` command could be done as follows: + + func animateHandlerFunc(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error { + + for i:=0; i<20; i++ { + oi.LongWriteString(stdout, "\r⠋") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠙") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠹") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠸") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠼") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠴") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠦") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠧") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠇") + time.Sleep(50*time.Millisecond) + + oi.LongWriteString(stdout, "\r⠏") + time.Sleep(50*time.Millisecond) + } + oi.LongWriteString(stdout, "\r \r\n") + + return nil + } + + + func animateProducerFunc(ctx telsh.Context, name string, args ...string) telsh.Handler{ + return telsh.PromoteHandlerFunc(animateHandler) + } + + + var animateProducer = ProducerFunc(animateProducerFunc) + +Again, note that your "real" work is in the `animateHandlerFunc` func. + +Generating PEM Files + +If you are using the telnet.ListenAndServeTLS func or the telnet.Server.ListenAndServeTLS method, you will need to +supply "cert.pem" and "key.pem" files. + +If you do not already have these files, the Go soure code contains a tool for generating these files for you. + +It can be found at: + + $GOROOT/src/crypto/tls/generate_cert.go + +So, for example, if your `$GOROOT` is the "/usr/local/go" directory, then it would be at: + + /usr/local/go/src/crypto/tls/generate_cert.go + +If you run the command: + + go run $GOROOT/src/crypto/tls/generate_cert.go --help + +... then you get the help information for "generate_cert.go". + +Of course, you would replace or set `$GOROOT` with whatever your path actually is. Again, for example, +if your `$GOROOT` is the "/usr/local/go" directory, then it would be: + + go run /usr/local/go/src/crypto/tls/generate_cert.go --help + +To demonstrate the usage of "generate_cert.go", you might run the following to generate certificates +that were bound to the hosts `127.0.0.1` and `localhost`: + + go run /usr/local/go/src/crypto/tls/generate_cert.go --ca --host='127.0.0.1,localhost' + + +If you are not sure where "generate_cert.go" is on your computer, on Linux and Unix based systems, you might +be able to find the file with the command: + + locate /src/crypto/tls/generate_cert.go + +(If it finds it, it should output the full path to this file.) + + +Example TELNET Client + +You can make a simple (un-secure) TELNET client with code like the following: + + package main + + + import ( + "github.com/reiver/go-telnet" + ) + + + func main() { + var caller Caller = telnet.StandardCaller + + //@TOOD: replace "example.net:5555" with address you want to connect to. + telnet.DialToAndCall("example.net:5555", caller) + } + + +Example TELNETS Client + +You can make a simple (secure) TELNETS client with code like the following: + + package main + + + import ( + "github.com/reiver/go-telnet" + ) + + + func main() { + var caller Caller = telnet.StandardCaller + + //@TOOD: replace "example.net:5555" with address you want to connect to. + telnet.DialToAndCallTLS("example.net:5555", caller) + } + + +TELNET Story + +The TELNET protocol is best known for providing a means of connecting to a remote computer, using a (text-based) shell interface, and being able to interact with it, (more or less) as if you were sitting at that computer. + +(Shells are also known as command-line interfaces or CLIs.) + +Although this was the original usage of the TELNET protocol, it can be (and is) used for other purposes as well. + + +The Era + +The TELNET protocol came from an era in computing when text-based shell interface where the common way of interacting with computers. + +The common interface for computers during this era was a keyboard and a monochromatic (i.e., single color) text-based monitors called "video terminals". + +(The word "video" in that era of computing did not refer to things such as movies. But instead was meant to contrast it with paper. In particular, the teletype machines, which were typewriter like devices that had a keyboard, but instead of having a monitor had paper that was printed onto.) + + +Early Office Computers + +In that era, in the early days of office computers, it was rare that an individual would have a computer at their desk. (A single computer was much too expensive.) + +Instead, there would be a single central computer that everyone would share. The style of computer used (for the single central shared computer) was called a "mainframe". + +What individuals would have at their desks, instead of their own compuer, would be some type of video terminal. + +The different types of video terminals had named such as: + +• VT52 + +• VT100 + +• VT220 + +• VT240 + +("VT" in those named was short for "video terminal".) + + +Teletype + +To understand this era, we need to go back a bit in time to what came before it: teletypes. + + +Terminal Codes + +Terminal codes (also sometimes called 'terminal control codes') are used to issue various kinds of commands +to the terminal. + +(Note that 'terminal control codes' are a completely separate concept for 'TELNET commands', +and the two should NOT be conflated or confused.) + +The most common types of 'terminal codes' are the 'ANSI escape codes'. (Although there are other types too.) + + +ANSI Escape Codes + +ANSI escape codes (also sometimes called 'ANSI escape sequences') are a common type of 'terminal code' used +to do things such as: + +• moving the cursor, + +• erasing the display, + +• erasing the line, + +• setting the graphics mode, + +• setting the foregroup color, + +• setting the background color, + +• setting the screen resolution, and + +• setting keyboard strings. + + +Setting The Foreground Color With ANSI Escape Codes + +One of the abilities of ANSI escape codes is to set the foreground color. + +Here is a table showing codes for this: + + | ANSI Color | Go string | Go []byte | + | ------------ | ---------- | ----------------------------- | + | Black | "\x1b[30m" | []byte{27, '[', '3','0', 'm'} | + | Red | "\x1b[31m" | []byte{27, '[', '3','1', 'm'} | + | Green | "\x1b[32m" | []byte{27, '[', '3','2', 'm'} | + | Brown/Yellow | "\x1b[33m" | []byte{27, '[', '3','3', 'm'} | + | Blue | "\x1b[34m" | []byte{27, '[', '3','4', 'm'} | + | Magenta | "\x1b[35m" | []byte{27, '[', '3','5', 'm'} | + | Cyan | "\x1b[36m" | []byte{27, '[', '3','6', 'm'} | + | Gray/White | "\x1b[37m" | []byte{27, '[', '3','7', 'm'} | + +(Note that in the `[]byte` that the first `byte` is the number `27` (which +is the "escape" character) where the third and fouth characters are the +**not** number literals, but instead character literals `'3'` and whatever.) + + +Setting The Background Color With ANSI Escape Codes + +Another of the abilities of ANSI escape codes is to set the background color. + + | ANSI Color | Go string | Go []byte | + | ------------ | ---------- | ----------------------------- | + | Black | "\x1b[40m" | []byte{27, '[', '4','0', 'm'} | + | Red | "\x1b[41m" | []byte{27, '[', '4','1', 'm'} | + | Green | "\x1b[42m" | []byte{27, '[', '4','2', 'm'} | + | Brown/Yellow | "\x1b[43m" | []byte{27, '[', '4','3', 'm'} | + | Blue | "\x1b[44m" | []byte{27, '[', '4','4', 'm'} | + | Magenta | "\x1b[45m" | []byte{27, '[', '4','5', 'm'} | + | Cyan | "\x1b[46m" | []byte{27, '[', '4','6', 'm'} | + | Gray/White | "\x1b[47m" | []byte{27, '[', '4','7', 'm'} | + +(Note that in the `[]byte` that the first `byte` is the number `27` (which +is the "escape" character) where the third and fouth characters are the +**not** number literals, but instead character literals `'4'` and whatever.) + +Using ANSI Escape Codes + +In Go code, if I wanted to use an ANSI escape code to use a blue background, +a white foreground, and bold, I could do that with the ANSI escape code: + + "\x1b[44;37;1m" + +Note that that start with byte value 27, which we have encoded as hexadecimal +as \x1b. Followed by the '[' character. + +Coming after that is the sub-string "44", which is the code that sets our background color to blue. + +We follow that with the ';' character (which separates codes). + +And the after that comes the sub-string "37", which is the code that set our foreground color to white. + +After that, we follow with another ";" character (which, again, separates codes). + +And then we follow it the sub-string "1", which is the code that makes things bold. + +And finally, the ANSI escape sequence is finished off with the 'm' character. + +To show this in a more complete example, our `dateHandlerFunc` from before could incorporate ANSI escape sequences as follows: + + func dateHandlerFunc(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) error { + const layout = "Mon Jan 2 15:04:05 -0700 MST 2006" + s := "\x1b[44;37;1m" + time.Now().Format(layout) + "\x1b[0m" + + if _, err := oi.LongWriteString(stdout, s); nil != err { + return err + } + + return nil + } + +Note that in that example, in addition to using the ANSI escape sequence "\x1b[44;37;1m" +to set the background color to blue, set the foreground color to white, and make it bold, +we also used the ANSI escape sequence "\x1b[0m" to reset the background and foreground colors +and boldness back to "normal". + +*/ +package telnet diff --git a/vendor/github.com/reiver/go-telnet/echo_handler.go b/vendor/github.com/reiver/go-telnet/echo_handler.go new file mode 100644 index 0000000..0401009 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/echo_handler.go @@ -0,0 +1,33 @@ +package telnet + + +import ( + "github.com/reiver/go-oi" +) + + +// EchoHandler is a simple TELNET server which "echos" back to the client any (non-command) +// data back to the TELNET client, it received from the TELNET client. +var EchoHandler Handler = internalEchoHandler{} + + +type internalEchoHandler struct{} + + +func (handler internalEchoHandler) ServeTELNET(ctx Context, w Writer, r Reader) { + + var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up. + p := buffer[:] + + for { + n, err := r.Read(p) + + if n > 0 { + oi.LongWrite(w, p[:n]) + } + + if nil != err { + break + } + } +} diff --git a/vendor/github.com/reiver/go-telnet/handler.go b/vendor/github.com/reiver/go-telnet/handler.go new file mode 100644 index 0000000..96f4420 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/handler.go @@ -0,0 +1,18 @@ +package telnet + + +// A Handler serves a TELNET (or TELNETS) connection. +// +// Writing data to the Writer passed as an argument to the ServeTELNET method +// will send data to the TELNET (or TELNETS) client. +// +// Reading data from the Reader passed as an argument to the ServeTELNET method +// will receive data from the TELNET client. +// +// The Writer's Write method sends "escaped" TELNET (and TELNETS) data. +// +// The Reader's Read method "un-escapes" TELNET (and TELNETS) data, and filters +// out TELNET (and TELNETS) command sequences. +type Handler interface { + ServeTELNET(Context, Writer, Reader) +} diff --git a/vendor/github.com/reiver/go-telnet/logger.go b/vendor/github.com/reiver/go-telnet/logger.go new file mode 100644 index 0000000..979c1f4 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/logger.go @@ -0,0 +1,16 @@ +package telnet + + +type Logger interface{ + Debug(...interface{}) + Debugf(string, ...interface{}) + + Error(...interface{}) + Errorf(string, ...interface{}) + + Trace(...interface{}) + Tracef(string, ...interface{}) + + Warn(...interface{}) + Warnf(string, ...interface{}) +} diff --git a/vendor/github.com/reiver/go-telnet/reader.go b/vendor/github.com/reiver/go-telnet/reader.go new file mode 100644 index 0000000..2e56176 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/reader.go @@ -0,0 +1,6 @@ +package telnet + + +type Reader interface { + Read([]byte) (int, error) +} diff --git a/vendor/github.com/reiver/go-telnet/server.go b/vendor/github.com/reiver/go-telnet/server.go new file mode 100644 index 0000000..a63ff35 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/server.go @@ -0,0 +1,191 @@ +package telnet + + +import ( + "crypto/tls" + "net" +) + + +// ListenAndServe listens on the TCP network address `addr` and then spawns a call to the ServeTELNET +// method on the `handler` to serve each incoming connection. +// +// For a very simple example: +// +// package main +// +// import ( +// "github.com/reiver/go-telnet" +// ) +// +// func main() { +// +// //@TODO: In your code, you would probably want to use a different handler. +// var handler telnet.Handler = telnet.EchoHandler +// +// err := telnet.ListenAndServe(":5555", handler) +// if nil != err { +// //@TODO: Handle this error better. +// panic(err) +// } +// } +func ListenAndServe(addr string, handler Handler) error { + server := &Server{Addr: addr, Handler: handler} + return server.ListenAndServe() +} + + +// Serve accepts an incoming TELNET or TELNETS client connection on the net.Listener `listener`. +func Serve(listener net.Listener, handler Handler) error { + + server := &Server{Handler: handler} + return server.Serve(listener) +} + + +// A Server defines parameters of a running TELNET server. +// +// For a simple example: +// +// package main +// +// import ( +// "github.com/reiver/go-telnet" +// ) +// +// func main() { +// +// var handler telnet.Handler = telnet.EchoHandler +// +// server := &telnet.Server{ +// Addr:":5555", +// Handler:handler, +// } +// +// err := server.ListenAndServe() +// if nil != err { +// //@TODO: Handle this error better. +// panic(err) +// } +// } +type Server struct { + Addr string // TCP address to listen on; ":telnet" or ":telnets" if empty (when used with ListenAndServe or ListenAndServeTLS respectively). + Handler Handler // handler to invoke; telnet.EchoServer if nil + + TLSConfig *tls.Config // optional TLS configuration; used by ListenAndServeTLS. + + Logger Logger +} + +// ListenAndServe listens on the TCP network address 'server.Addr' and then spawns a call to the ServeTELNET +// method on the 'server.Handler' to serve each incoming connection. +// +// For a simple example: +// +// package main +// +// import ( +// "github.com/reiver/go-telnet" +// ) +// +// func main() { +// +// var handler telnet.Handler = telnet.EchoHandler +// +// server := &telnet.Server{ +// Addr:":5555", +// Handler:handler, +// } +// +// err := server.ListenAndServe() +// if nil != err { +// //@TODO: Handle this error better. +// panic(err) +// } +// } +func (server *Server) ListenAndServe() error { + + addr := server.Addr + if "" == addr { + addr = ":telnet" + } + + + listener, err := net.Listen("tcp", addr) + if nil != err { + return err + } + + + return server.Serve(listener) +} + + +// Serve accepts an incoming TELNET client connection on the net.Listener `listener`. +func (server *Server) Serve(listener net.Listener) error { + + defer listener.Close() + + + logger := server.logger() + + + handler := server.Handler + if nil == handler { +//@TODO: Should this be a "ShellHandler" instead, that gives a shell-like experience by default +// If this is changd, then need to change the comment in the "type Server struct" definition. + logger.Debug("Defaulted handler to EchoHandler.") + handler = EchoHandler + } + + + for { + // Wait for a new TELNET client connection. + logger.Debugf("Listening at %q.", listener.Addr()) + conn, err := listener.Accept() + if err != nil { +//@TODO: Could try to recover from certain kinds of errors. Maybe waiting a while before trying again. + return err + } + logger.Debugf("Received new connection from %q.", conn.RemoteAddr()) + + // Handle the new TELNET client connection by spawning + // a new goroutine. + go server.handle(conn, handler) + logger.Debugf("Spawned handler to handle connection from %q.", conn.RemoteAddr()) + } +} + +func (server *Server) handle(c net.Conn, handler Handler) { + defer c.Close() + + logger := server.logger() + + + defer func(){ + if r := recover(); nil != r { + if nil != logger { + logger.Errorf("Recovered from: (%T) %v", r, r) + } + } + }() + + var ctx Context = NewContext().InjectLogger(logger) + + var w Writer = newDataWriter(c) + var r Reader = newDataReader(c) + + handler.ServeTELNET(ctx, w, r) + c.Close() +} + + + +func (server *Server) logger() Logger { + logger := server.Logger + if nil == logger { + logger = internalDiscardLogger{} + } + + return logger +} diff --git a/vendor/github.com/reiver/go-telnet/standard_caller.go b/vendor/github.com/reiver/go-telnet/standard_caller.go new file mode 100644 index 0000000..17a9408 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/standard_caller.go @@ -0,0 +1,93 @@ +package telnet + + +import ( + "github.com/reiver/go-oi" + + "bufio" + "bytes" + "fmt" + "io" + "os" + + "time" +) + + +// StandardCaller is a simple TELNET client which sends to the server any data it gets from os.Stdin +// as TELNET (and TELNETS) data, and writes any TELNET (or TELNETS) data it receives from +// the server to os.Stdout, and writes any error it has to os.Stderr. +var StandardCaller Caller = internalStandardCaller{} + + +type internalStandardCaller struct{} + + +func (caller internalStandardCaller) CallTELNET(ctx Context, w Writer, r Reader) { + standardCallerCallTELNET(os.Stdin, os.Stdout, os.Stderr, ctx, w, r) +} + + +func standardCallerCallTELNET(stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser, ctx Context, w Writer, r Reader) { + + go func(writer io.Writer, reader io.Reader) { + + var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up. + p := buffer[:] + + for { + // Read 1 byte. + n, err := reader.Read(p) + if n <= 0 && nil == err { + continue + } else if n <= 0 && nil != err { + break + } + + oi.LongWrite(writer, p) + } + }(stdout, r) + + + + var buffer bytes.Buffer + var p []byte + + var crlfBuffer [2]byte = [2]byte{'\r','\n'} + crlf := crlfBuffer[:] + + scanner := bufio.NewScanner(stdin) + scanner.Split(scannerSplitFunc) + + for scanner.Scan() { + buffer.Write(scanner.Bytes()) + buffer.Write(crlf) + + p = buffer.Bytes() + + n, err := oi.LongWrite(w, p) + if nil != err { + break + } + if expected, actual := int64(len(p)), n; expected != actual { + err := fmt.Errorf("Transmission problem: tried sending %d bytes, but actually only sent %d bytes.", expected, actual) + fmt.Fprint(stderr, err.Error()) + return + } + + + buffer.Reset() + } + + // Wait a bit to receive data from the server (that we would send to io.Stdout). + time.Sleep(3 * time.Millisecond) +} + + +func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF { + return 0, nil, nil + } + + return bufio.ScanLines(data, atEOF) +} diff --git a/vendor/github.com/reiver/go-telnet/tls.go b/vendor/github.com/reiver/go-telnet/tls.go new file mode 100644 index 0000000..c5606a3 --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/tls.go @@ -0,0 +1,113 @@ +package telnet + + +import ( + "crypto/tls" + "net" +) + + +// ListenAndServeTLS acts identically to ListenAndServe, except that it +// uses the TELNET protocol over TLS. +// +// From a TELNET protocol point-of-view, it allows for 'secured telnet', also known as TELNETS, +// which by default listens to port 992. +// +// Of course, this port can be overridden using the 'addr' argument. +// +// For a very simple example: +// +// package main +// +// import ( +// "github.com/reiver/go-telnet" +// ) +// +// func main() { +// +// //@TODO: In your code, you would probably want to use a different handler. +// var handler telnet.Handler = telnet.EchoHandler +// +// err := telnet.ListenAndServeTLS(":5555", "cert.pem", "key.pem", handler) +// if nil != err { +// //@TODO: Handle this error better. +// panic(err) +// } +// } +func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error { + server := &Server{Addr: addr, Handler: handler} + return server.ListenAndServeTLS(certFile, keyFile) +} + + + +// ListenAndServeTLS acts identically to ListenAndServe, except that it +// uses the TELNET protocol over TLS. +// +// From a TELNET protocol point-of-view, it allows for 'secured telnet', also known as TELNETS, +// which by default listens to port 992. +func (server *Server) ListenAndServeTLS(certFile string, keyFile string) error { + + addr := server.Addr + if "" == addr { + addr = ":telnets" + } + + + listener, err := net.Listen("tcp", addr) + if nil != err { + return err + } + + + // Apparently have to make a copy of the TLS config this way, rather than by + // simple assignment, to prevent some unexported fields from being copied over. + // + // It would be nice if tls.Config had a method that would do this "safely". + // (I.e., what happens if in the future more exported fields are added to + // tls.Config?) + var tlsConfig *tls.Config = nil + if nil == server.TLSConfig { + tlsConfig = &tls.Config{} + } else { + tlsConfig = &tls.Config{ + Rand: server.TLSConfig.Rand, + Time: server.TLSConfig.Time, + Certificates: server.TLSConfig.Certificates, + NameToCertificate: server.TLSConfig.NameToCertificate, + GetCertificate: server.TLSConfig.GetCertificate, + RootCAs: server.TLSConfig.RootCAs, + NextProtos: server.TLSConfig.NextProtos, + ServerName: server.TLSConfig.ServerName, + ClientAuth: server.TLSConfig.ClientAuth, + ClientCAs: server.TLSConfig.ClientCAs, + InsecureSkipVerify: server.TLSConfig.InsecureSkipVerify, + CipherSuites: server.TLSConfig.CipherSuites, + PreferServerCipherSuites: server.TLSConfig.PreferServerCipherSuites, + SessionTicketsDisabled: server.TLSConfig.SessionTicketsDisabled, + SessionTicketKey: server.TLSConfig.SessionTicketKey, + ClientSessionCache: server.TLSConfig.ClientSessionCache, + MinVersion: server.TLSConfig.MinVersion, + MaxVersion: server.TLSConfig.MaxVersion, + CurvePreferences: server.TLSConfig.CurvePreferences, + } + } + + + tlsConfigHasCertificate := len(tlsConfig.Certificates) > 0 || nil != tlsConfig.GetCertificate + if "" == certFile || "" == keyFile || !tlsConfigHasCertificate { + tlsConfig.Certificates = make([]tls.Certificate, 1) + + var err error + tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + if nil != err { + return err + } + } + + + tlsListener := tls.NewListener(listener, tlsConfig) + + + return server.Serve(tlsListener) +} diff --git a/vendor/github.com/reiver/go-telnet/writer.go b/vendor/github.com/reiver/go-telnet/writer.go new file mode 100644 index 0000000..a42d04f --- /dev/null +++ b/vendor/github.com/reiver/go-telnet/writer.go @@ -0,0 +1,6 @@ +package telnet + + +type Writer interface { + Write([]byte) (int, error) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 3638043..282ee09 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -20,12 +20,6 @@ "revision": "7a41df006ff9af79a29f0ffa9c5f21fbe6314a2d", "revisionTime": "2017-01-10T07:11:07Z" }, - { - "checksumSHA1": "XIIHyP5QkRbX/zBNqq+hAQ2SmzE=", - "path": "github.com/gliderlabs/ssh", - "revision": "ce31f3cc47feee0c38db7ecfaa154026929ffbda", - "revisionTime": "2017-12-06T20:46:25Z" - }, { "checksumSHA1": "fI9spYCCgBl19GMD/JsW+znBHkw=", "path": "github.com/go-gormigrate/gormigrate", @@ -110,6 +104,18 @@ "revision": "a7a4c189eb47ed33ce7b35f2880070a0c82a67d4", "revisionTime": "2017-09-25T23:40:30Z" }, + { + "checksumSHA1": "7BsTyiYZMTvIGT/KsF9a3tAoN0g=", + "path": "github.com/reiver/go-oi", + "revision": "431c83978379297f04f85f6eb94f129f25ab741d", + "revisionTime": "2016-03-25T06:16:15Z" + }, + { + "checksumSHA1": "sb1kOSRpAu22ovV0dXHkPlG06i4=", + "path": "github.com/reiver/go-telnet", + "revision": "6b696f32801a8f8dd07947f1e1fdb1a7dc4766ff", + "revisionTime": "2016-03-30T05:09:16Z" + }, { "checksumSHA1": "zRAcxJW8WNDMryOBS4VE03V6ivw=", "path": "github.com/urfave/cli",