├── LICENSE ├── README.md ├── conn.go ├── examples ├── bat.org │ └── main.go └── unix-cisco │ └── main.go └── go.mod /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Michal Derkacz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [documentation](http://godoc.org/github.com/ziutek/telnet) 2 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // Package telnet provides simple interface for interacting with Telnet 2 | // connection. 3 | package telnet 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "net" 10 | "time" 11 | "unicode" 12 | ) 13 | 14 | const ( 15 | CR = byte('\r') 16 | LF = byte('\n') 17 | ) 18 | 19 | const ( 20 | cmdSE = 240 21 | cmdNOP = 241 22 | cmdData = 242 23 | 24 | cmdBreak = 243 25 | cmdGA = 249 26 | cmdSB = 250 27 | 28 | cmdWill = 251 29 | cmdWont = 252 30 | cmdDo = 253 31 | cmdDont = 254 32 | 33 | cmdIAC = 255 34 | ) 35 | 36 | const ( 37 | optEcho = 1 38 | optSuppressGoAhead = 3 39 | // optTerminalType = 24 40 | optNAWS = 31 41 | ) 42 | 43 | // Conn implements net.Conn interface for Telnet protocol plus some set of 44 | // Telnet specific methods. 45 | type Conn struct { 46 | net.Conn 47 | r *bufio.Reader 48 | 49 | unixWriteMode bool 50 | 51 | cliSuppressGoAhead bool 52 | cliEcho bool 53 | } 54 | 55 | func NewConn(conn net.Conn) (*Conn, error) { 56 | c := Conn{ 57 | Conn: conn, 58 | r: bufio.NewReaderSize(conn, 256), 59 | } 60 | return &c, nil 61 | } 62 | 63 | func Dial(network, addr string) (*Conn, error) { 64 | conn, err := net.Dial(network, addr) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return NewConn(conn) 69 | } 70 | 71 | func DialTimeout(network, addr string, timeout time.Duration) (*Conn, error) { 72 | conn, err := net.DialTimeout(network, addr, timeout) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return NewConn(conn) 77 | } 78 | 79 | // SetUnixWriteMode sets flag that applies only to the Write method. 80 | // If set, Write converts any '\n' (LF) to '\r\n' (CR LF). 81 | func (c *Conn) SetUnixWriteMode(uwm bool) { 82 | c.unixWriteMode = uwm 83 | } 84 | 85 | func (c *Conn) do(option byte) error { 86 | //log.Println("do:", option) 87 | _, err := c.Conn.Write([]byte{cmdIAC, cmdDo, option}) 88 | return err 89 | } 90 | 91 | func (c *Conn) dont(option byte) error { 92 | //log.Println("dont:", option) 93 | _, err := c.Conn.Write([]byte{cmdIAC, cmdDont, option}) 94 | return err 95 | } 96 | 97 | func (c *Conn) will(option byte) error { 98 | //log.Println("will:", option) 99 | _, err := c.Conn.Write([]byte{cmdIAC, cmdWill, option}) 100 | return err 101 | } 102 | 103 | func (c *Conn) wont(option byte) error { 104 | //log.Println("wont:", option) 105 | _, err := c.Conn.Write([]byte{cmdIAC, cmdWont, option}) 106 | return err 107 | } 108 | 109 | func (c *Conn) sub(opt byte, data ...byte) error { 110 | if _, err := c.Conn.Write([]byte{cmdIAC, cmdSB, opt}); err != nil { 111 | return err 112 | } 113 | if _, err := c.Conn.Write(data); err != nil { 114 | return err 115 | } 116 | _, err := c.Conn.Write([]byte{cmdIAC, cmdSE}) 117 | return err 118 | } 119 | 120 | func (c *Conn) deny(cmd, opt byte) (err error) { 121 | switch cmd { 122 | case cmdDo: 123 | err = c.wont(opt) 124 | case cmdDont: 125 | // nop 126 | case cmdWill, cmdWont: 127 | err = c.dont(opt) 128 | } 129 | return 130 | } 131 | 132 | func (c *Conn) skipSubneg() error { 133 | for { 134 | if b, err := c.r.ReadByte(); err != nil { 135 | return err 136 | } else if b == cmdIAC { 137 | if b, err = c.r.ReadByte(); err != nil { 138 | return err 139 | } else if b == cmdSE { 140 | return nil 141 | } 142 | } 143 | } 144 | } 145 | 146 | func (c *Conn) cmd(cmd byte) error { 147 | switch cmd { 148 | case cmdGA: 149 | return nil 150 | case cmdDo, cmdDont, cmdWill, cmdWont: 151 | // Process cmd after this switch. 152 | case cmdSB: 153 | return c.skipSubneg() 154 | default: 155 | return fmt.Errorf("unknown command: %d", cmd) 156 | } 157 | // Read an option 158 | o, err := c.r.ReadByte() 159 | if err != nil { 160 | return err 161 | } 162 | //log.Println("received cmd:", cmd, o) 163 | switch o { 164 | case optEcho: 165 | // Accept any echo configuration. 166 | switch cmd { 167 | case cmdDo: 168 | if !c.cliEcho { 169 | c.cliEcho = true 170 | err = c.will(o) 171 | } 172 | case cmdDont: 173 | if c.cliEcho { 174 | c.cliEcho = false 175 | err = c.wont(o) 176 | } 177 | case cmdWill: 178 | if !c.cliEcho { 179 | c.cliEcho = true 180 | err = c.do(o) 181 | } 182 | case cmdWont: 183 | if c.cliEcho { 184 | c.cliEcho = false 185 | err = c.dont(o) 186 | } 187 | } 188 | case optSuppressGoAhead: 189 | // We don't use GA so can allways accept every configuration 190 | switch cmd { 191 | case cmdDo: 192 | if !c.cliSuppressGoAhead { 193 | c.cliSuppressGoAhead = true 194 | err = c.will(o) 195 | } 196 | case cmdDont: 197 | if c.cliSuppressGoAhead { 198 | c.cliSuppressGoAhead = false 199 | err = c.wont(o) 200 | } 201 | case cmdWill: 202 | if !c.cliSuppressGoAhead { 203 | c.cliSuppressGoAhead = true 204 | err = c.do(o) 205 | } 206 | case cmdWont: 207 | if c.cliSuppressGoAhead { 208 | c.cliSuppressGoAhead = false 209 | err = c.dont(o) 210 | } 211 | } 212 | case optNAWS: 213 | if cmd != cmdDo { 214 | err = c.deny(cmd, o) 215 | break 216 | } 217 | if err = c.will(o); err != nil { 218 | break 219 | } 220 | // Reply with max window size: 65535x65535 221 | err = c.sub(o, 255, 255, 255, 255, 255, 255, 255, 255) 222 | default: 223 | // Deny any other option 224 | err = c.deny(cmd, o) 225 | } 226 | return err 227 | } 228 | 229 | func (c *Conn) tryReadByte() (b byte, retry bool, err error) { 230 | b, err = c.r.ReadByte() 231 | if err != nil || b != cmdIAC { 232 | return 233 | } 234 | b, err = c.r.ReadByte() 235 | if err != nil { 236 | return 237 | } 238 | if b != cmdIAC { 239 | err = c.cmd(b) 240 | if err != nil { 241 | return 242 | } 243 | retry = true 244 | } 245 | return 246 | } 247 | 248 | // SetEcho tries to enable/disable echo on server side. Typically telnet 249 | // servers doesn't support this. 250 | func (c *Conn) SetEcho(echo bool) error { 251 | if echo { 252 | return c.do(optEcho) 253 | } 254 | return c.dont(optEcho) 255 | } 256 | 257 | // ReadByte works like bufio.ReadByte 258 | func (c *Conn) ReadByte() (b byte, err error) { 259 | retry := true 260 | for retry && err == nil { 261 | b, retry, err = c.tryReadByte() 262 | } 263 | return 264 | } 265 | 266 | // ReadRune works like bufio.ReadRune 267 | func (c *Conn) ReadRune() (r rune, size int, err error) { 268 | loop: 269 | r, size, err = c.r.ReadRune() 270 | if err != nil { 271 | return 272 | } 273 | if r != unicode.ReplacementChar || size != 1 { 274 | // Properly readed rune 275 | return 276 | } 277 | // Bad rune 278 | err = c.r.UnreadRune() 279 | if err != nil { 280 | return 281 | } 282 | // Read telnet command or escaped IAC 283 | _, retry, err := c.tryReadByte() 284 | if err != nil { 285 | return 286 | } 287 | if retry { 288 | // This bad rune was a begining of telnet command. Try read next rune. 289 | goto loop 290 | } 291 | // Return escaped IAC as unicode.ReplacementChar 292 | return 293 | } 294 | 295 | // Read is for implement an io.Reader interface 296 | func (c *Conn) Read(buf []byte) (int, error) { 297 | var n int 298 | for n < len(buf) { 299 | b, retry, err := c.tryReadByte() 300 | if err != nil { 301 | return n, err 302 | } 303 | if !retry { 304 | buf[n] = b 305 | n++ 306 | } 307 | if n > 0 && c.r.Buffered() == 0 { 308 | // Don't block if can't return more data. 309 | return n, err 310 | } 311 | } 312 | return n, nil 313 | } 314 | 315 | // ReadBytes works like bufio.ReadBytes 316 | func (c *Conn) ReadBytes(delim byte) ([]byte, error) { 317 | var line []byte 318 | for { 319 | b, err := c.ReadByte() 320 | if err != nil { 321 | return nil, err 322 | } 323 | line = append(line, b) 324 | if b == delim { 325 | break 326 | } 327 | } 328 | return line, nil 329 | } 330 | 331 | // SkipBytes works like ReadBytes but skips all read data. 332 | func (c *Conn) SkipBytes(delim byte) error { 333 | for { 334 | b, err := c.ReadByte() 335 | if err != nil { 336 | return err 337 | } 338 | if b == delim { 339 | break 340 | } 341 | } 342 | return nil 343 | } 344 | 345 | // ReadString works like bufio.ReadString 346 | func (c *Conn) ReadString(delim byte) (string, error) { 347 | bytes, err := c.ReadBytes(delim) 348 | return string(bytes), err 349 | } 350 | 351 | func (c *Conn) readUntil(read bool, delims ...string) ([]byte, int, error) { 352 | if len(delims) == 0 { 353 | return nil, 0, nil 354 | } 355 | p := make([]string, len(delims)) 356 | for i, s := range delims { 357 | if len(s) == 0 { 358 | return nil, 0, nil 359 | } 360 | p[i] = s 361 | } 362 | var line []byte 363 | for { 364 | b, err := c.ReadByte() 365 | if err != nil { 366 | return nil, 0, err 367 | } 368 | if read { 369 | line = append(line, b) 370 | } 371 | for i, s := range p { 372 | if s[0] == b { 373 | if len(s) == 1 { 374 | return line, i, nil 375 | } 376 | p[i] = s[1:] 377 | } else { 378 | p[i] = delims[i] 379 | } 380 | } 381 | } 382 | panic(nil) 383 | } 384 | 385 | // ReadUntilIndex reads from connection until one of delimiters occurs. Returns 386 | // read data and an index of delimiter or error. 387 | func (c *Conn) ReadUntilIndex(delims ...string) ([]byte, int, error) { 388 | return c.readUntil(true, delims...) 389 | } 390 | 391 | // ReadUntil works like ReadUntilIndex but don't return a delimiter index. 392 | func (c *Conn) ReadUntil(delims ...string) ([]byte, error) { 393 | d, _, err := c.readUntil(true, delims...) 394 | return d, err 395 | } 396 | 397 | // SkipUntilIndex works like ReadUntilIndex but skips all read data. 398 | func (c *Conn) SkipUntilIndex(delims ...string) (int, error) { 399 | _, i, err := c.readUntil(false, delims...) 400 | return i, err 401 | } 402 | 403 | // SkipUntil works like ReadUntil but skips all read data. 404 | func (c *Conn) SkipUntil(delims ...string) error { 405 | _, _, err := c.readUntil(false, delims...) 406 | return err 407 | } 408 | 409 | // Write is for implement an io.Writer interface 410 | func (c *Conn) Write(buf []byte) (int, error) { 411 | search := "\xff" 412 | if c.unixWriteMode { 413 | search = "\xff\n" 414 | } 415 | var ( 416 | n int 417 | err error 418 | ) 419 | for len(buf) > 0 { 420 | var k int 421 | i := bytes.IndexAny(buf, search) 422 | if i == -1 { 423 | k, err = c.Conn.Write(buf) 424 | n += k 425 | break 426 | } 427 | k, err = c.Conn.Write(buf[:i]) 428 | n += k 429 | if err != nil { 430 | break 431 | } 432 | switch buf[i] { 433 | case LF: 434 | k, err = c.Conn.Write([]byte{CR, LF}) 435 | case cmdIAC: 436 | k, err = c.Conn.Write([]byte{cmdIAC, cmdIAC}) 437 | } 438 | n += k 439 | if err != nil { 440 | break 441 | } 442 | buf = buf[i+1:] 443 | } 444 | return n, err 445 | } 446 | -------------------------------------------------------------------------------- /examples/bat.org/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/ziutek/telnet" 8 | ) 9 | 10 | func checkErr(err error) { 11 | if err != nil { 12 | log.Fatalln("Error:", err) 13 | } 14 | } 15 | 16 | func main() { 17 | t, err := telnet.Dial("tcp", "bat.org:23") 18 | checkErr(err) 19 | 20 | buf := make([]byte, 512) 21 | for { 22 | n, err := t.Read(buf) // Use raw read to find issue #15. 23 | os.Stdout.Write(buf[:n]) 24 | checkErr(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/unix-cisco/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/ziutek/telnet" 9 | ) 10 | 11 | const timeout = 10 * time.Second 12 | 13 | func checkErr(err error) { 14 | if err != nil { 15 | log.Fatalln("Error:", err) 16 | } 17 | } 18 | 19 | func expect(t *telnet.Conn, d ...string) { 20 | checkErr(t.SetReadDeadline(time.Now().Add(timeout))) 21 | checkErr(t.SkipUntil(d...)) 22 | } 23 | 24 | func sendln(t *telnet.Conn, s string) { 25 | checkErr(t.SetWriteDeadline(time.Now().Add(timeout))) 26 | buf := make([]byte, len(s)+1) 27 | copy(buf, s) 28 | buf[len(s)] = '\n' 29 | _, err := t.Write(buf) 30 | checkErr(err) 31 | } 32 | 33 | func main() { 34 | if len(os.Args) != 5 { 35 | log.Printf("Usage: %s {unix|cisco} HOST:PORT USER PASSWD", os.Args[0]) 36 | return 37 | } 38 | typ, dst, user, passwd := os.Args[1], os.Args[2], os.Args[3], os.Args[4] 39 | 40 | t, err := telnet.Dial("tcp", dst) 41 | checkErr(err) 42 | t.SetUnixWriteMode(true) 43 | 44 | var data []byte 45 | switch typ { 46 | case "unix": 47 | expect(t, "login: ") 48 | sendln(t, user) 49 | expect(t, "ssword: ") 50 | sendln(t, passwd) 51 | expect(t, "$") 52 | sendln(t, "ls -l") 53 | data, err = t.ReadBytes('$') 54 | case "cisco": 55 | expect(t, "name: ") 56 | sendln(t, user) 57 | expect(t, "ssword: ") 58 | sendln(t, passwd) 59 | expect(t, ">") 60 | sendln(t, "sh ver") 61 | data, err = t.ReadBytes('>') 62 | default: 63 | log.Fatalln("bad host type: " + typ) 64 | } 65 | checkErr(err) 66 | os.Stdout.Write(data) 67 | os.Stdout.WriteString("\n") 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ziutek/telnet 2 | 3 | go 1.22.5 4 | --------------------------------------------------------------------------------