telnet

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 27, 2024 License: MIT Imports: 19 Imported by: 5

README

Moodclient/Telnet

Go Version GoDoc GoReportCard

This library provides a wrapper that can fit around any net.Conn in order to provide Telnet services for any arbitrary connection. In addition to basic line-level read and write that is compatible with RFC854/RFC5198, this library also provides an extensible base for Telnet Options (telopts), handles telopt negotiation and subnegotiation routing, and provides implementations for 10 heavily-used telopts:

  • CHARSET
  • ECHO
  • EOR
  • NAWS
  • NEW-ENVIRON
  • SEND-LOCATION
  • SUPPRESS-GO-AHEAD
  • TRANSMIT-BINARY
  • TTYPE
  • LINEMODE

In the examples folder, an example for a dead-simple terminal-based MUD client can be found.

How To Use?

Initialize a new terminal with your connection and configuration:

	terminal, err := telnet.NewTerminal(context.Background(), conn, telnet.TerminalConfig{
		Side:               telnet.SideClient,
		DefaultCharsetName: "US-ASCII",
	})
	if err != nil {
		log.Fatalln(err)
	}

The terminal will immediately begin communicating on the connection and negotiating options. It will continue to do so until the connection is closed or the provided context is cancelled (or if the context times out, but that would be weird).

You can call terminal.WaitForExit() to block the current goroutine until the terminal shuts down.

You can write text to the terminal using terminal.Keyboard().WriteString.

Hooks

Get data from the telnet connection using the Terminal's many, many event hooks:

func encounteredError(t *telnet.Terminal, err error) {
	fmt.Println(err)
}

func printerOutput(t *telnet.Terminal, output telnet.TerminalData) {
	fmt.Print(output.String())
}

...
	terminal, err := telnet.NewTerminal(ctx, conn, telnet.TerminalConfig{
		Side:               telnet.SideClient,
		DefaultCharsetName: "US-ASCII",
		EventHooks: telnet.EventHooks{
			PrinterOutput:    []telnet.TerminalDataHandler{printerOutput},
			EncounteredError: []telnet.ErrorHandler{encounteredError},
		},
	})

You can also register a hook with the terminal after creation:

terminal.RegisterIncomingTextHook(incomingText)
TelOpt Support

By default, the terminal will reject all attempts at telopt negotiation by the remote party. You can register telopts with the terminal on creation. The first argument of a registration event is whether and how the telopt is permitted. Other parameters are telopt-specific.

	terminal, err := telnet.NewTerminal(ctx, conn, telnet.TerminalConfig{
		Side:               telnet.SideClient,
		DefaultCharsetName: "US-ASCII",
		TelOpts: []telnet.TelnetOption{
			telopts.RegisterCHARSET(telnet.TelOptAllowLocal|telnet.TelOptAllowRemote, telopts.CHARSETConfig{
				AllowAnyCharset:   true,
				PreferredCharsets: []string{"UTF-8", "US-ASCII"},
			}),
			telopts.RegisterEOR(telnet.TelOptRequestRemote | telnet.TelOptAllowLocal),
			telopts.RegisterECHO(telnet.TelOptAllowRemote),
			telopts.RegisterSUPPRESSGOAHEAD(telnet.TelOptAllowLocal | telnet.TelOptAllowRemote),
			telopts.RegisterNAWS(telnet.TelOptAllowLocal),
		},
		EventHooks: telnet.EventHooks{
			PrinterOutput:    []telnet.TerminalDataHandler{printerOutput},
			EncounteredError: []telnet.ErrorEvent{encounteredError},
		},
	})

Why Another Telnet Library In Go?

There are a great many telnet libraries written in go. However, telopt support in these libraries is usually spotty, and never extensible. If one wants to write a mud client (check the org name) in go, strong support for many boutique telopts is required. Concepts that are not part of the telnet RFC but are central to modern use of the telnet protocol, such as the weird rules around IAC GA/IAC EOR, are important and not represented in these libraries.

The ultimate goal of this library is for it to not just implement the basics of the telnet protocol, but be a useful core for real-world uses of telnet, such as MUD and BBS clients and servers, strange online games that use vt100 for TUIs, and other oddities. Making this work will take long-term, dedicated labor on a telnet protocol library.

What Is Missing?

We're getting there! Currently, this library is working well with advanced MUDs as well as advanced BBS's such as retrocampus and 20forbeers. The remaining focus will be adding support for MUD-focused telopts, such as MCCP and GMCP.

Additionally, this has not been used in an environment where one server is tracking several different terminals for different connected users. The library may grow difficult to work with in that situation.

Documentation

Index

Constants

View Source
const (
	// EOR - End Of Record. The real meaning is implementation-specific, but these
	// days IAC EOR is primarily used as an alternative to IAC GA that can indicate
	// where a prompt is without all the historical baggage of GA
	EOR byte = 239
	// SE - Subnegotiation End. IAC SE is used to mark the end of a subnegotiation command
	SE byte = 240
	// NOP - No-Op. IAC NOP doesn't indicate anything at all, and this library ignores it.
	NOP byte = 241
	// DATAMARK - not in use by this library
	DATAMARK byte = 242
	// BRK - not in use by this library
	BRK byte = 243
	// IP - Not in use by this library
	IP byte = 244
	// AO - Not in use by this library
	AO byte = 245
	// AYT - If received, an IAC NOP will be sent in response
	AYT byte = 246
	// EC - Not in use by this library
	EC byte = 247
	// EL - Not in use by this library
	EL byte = 248
	// GA - Go Ahead. IAC GA is often used to indicate the end of a prompt line, so
	// that clients know where to place a cursor. However, it was originally used for
	// half-duplex terminals to indicate that the user could start typing and there is
	// a lot of weird baggage around "kludge line mode", so it is usually preferable
	// not to use this if the remote supports the EOR telopt.
	GA byte = 249
	// SB - Subnegotiation Begin. IAC SB is used to indicate the beginning of a subnegotiation
	// command. These are telopt-specific commands that have telopt-specific meanings.
	SB byte = 250
	// WILL - IAC WILL is used to indicate that this terminal intends to activate a telopt
	WILL byte = 251
	// WONT - IAC WONT is used to indicate that this terminal refuses to activate a telopt
	WONT byte = 252
	// DO - IAC DO is used to request that the remote terminal activates a telopt
	DO byte = 253
	// DONT - IAC DONT is used to demand that the remote terminal do not activate a telopt
	DONT byte = 254
	// IAC - This opcode indicates the beginning of a new command
	IAC byte = 255
)

Telnet opcodes

View Source
const DefaultKeyboardLock = 5 * time.Second

DefaultKeyboardLock indicates the default time to apply a lock with TelnetKeyboard.SetLock and telopts should use this duration when setting a keyboard lock unless they have a good reason not to.

Variables

This section is empty.

Functions

func GetTelOpt

func GetTelOpt[OptionStruct any, T TypedTelnetOption[OptionStruct]](terminal *Terminal) (T, error)

GetTelOpt retrieves a live telopt from a terminal. It is used like this:

telnet.GetTelOpt[telopts.ECHO](terminal)

The above will return a value of type *telopts.ECHO, or nil if ECHO is not a registered telopt. If there is a telopt of a different type registered under ECHO's code, then the method will return an error.

This can be used to update the local state of a telopt, or respond to TelOptEvents by querying the newly-updated remote state of a telopt.

Types

type ApcData added in v0.5.0

type ApcData struct {
	ansi.ApcSequence
}

func (ApcData) EscapedString added in v0.5.0

func (o ApcData) EscapedString(terminal TelOptLibrary) string

type Charset

type Charset struct {
	// contains filtered or unexported fields
}

Charset represents the full encoding landscape for this terminal. Terminals have both a default charset and separate negotiated charsets for encoding and decoding. On terminal creation, the negotiated charsets are the same as the default charset. Through the CHARSET telopt, a new negotiated charset can be established with the remote. However, according to the RFC, the negotiated charset should only be used when

TRANSMIT-BINARY is active. When it is not active, the default charset should continue

to be used. Not all implementors follow that requirement, though, so CharsetUsage is used to establish when the negotiated charset should be used.

Additionally, the RFC required the default charset to be US-ASCII prior to 2008 and requires it to be UTF-8 since 2008. However, not all peers have been updated to support UTF-8, so it is useful for us to use the default charset to establish whether our peer actually supports UTF-8 and make that information available to the consumer. The consumer can use that information to decide whether to send UTF-8 text to the peer or limit itself to US-ASCII.

Finally, some non-english services written prior to 2008 broke RFC and do not use US-ASCII as their default charset. So in some cases, we will establish a default character set other than US-ASCII to support these services.

Lastly, a fallback character set can be established that will be used during decoding if the correct charset for decoding fails.

func NewCharset

func NewCharset(defaultCodePage string, fallbackCodePage string, usage CharsetUsage) (*Charset, error)

NewCharset creates a new charset with a default charset, an optional fallback charset,

& a CharsetUsage to decide how the negotiated charset will be used if one is negotiated.

func (*Charset) BinaryDecode

func (c *Charset) BinaryDecode() bool

BinaryDecode returns a bool indicating whether the printer should use binary mode

func (*Charset) BinaryEncode

func (c *Charset) BinaryEncode() bool

BinaryEncode returns a bool indicating whether the keyboard should use binary mode

func (*Charset) Decode

func (c *Charset) Decode(buffer []byte, incomingText []byte, fallback EncodingState) (consumed int, buffered int, fellback EncodingState, err error)

Decode accepts a byte slice that is encoded in the printer's current encoding as well as a destination buffer for decoded bytes. Additionally, it accepts a bool indicating whether the decode process should skip the default/negotiated charset and immediately use the fallback charset.

The method returns how many bytes were consumed from the incoming text, how many bytes were written to the buffer, whether the charset had to move to fallback mode due to decoding failure, and potentially an error.

func (*Charset) DecodingName

func (c *Charset) DecodingName() string

DecodingName returns the name of the character set currently used by the printer. This method takes into account the default & negotiated character sets, the CharsetUsage value, and whether the printer is in binary mode

func (*Charset) DefaultCharsetName

func (c *Charset) DefaultCharsetName() string

DefaultCharsetName returns the name of the default character set

func (*Charset) Encode

func (c *Charset) Encode(utf8Text string) ([]byte, error)

Encode accepts a string of UTF-8 text and returns a byte slice that is encoded in the keyboard's current encoding

func (*Charset) EncodingName

func (c *Charset) EncodingName() string

EncodingName returns the name of the character set currently used by the keyboard. This method takes into account the default & negotiated character sets, the CharsetUsage value, and whether the keyboard is in binary mode

func (*Charset) PromoteDefaultCharset

func (c *Charset) PromoteDefaultCharset(oldCodePage string, newCodePage string) (bool, error)

PromoteDefaultCharset will change the default character set to the new code page if it is currently set to the old code page. If the default character set is changed, the negotiated character set will also be changed if it's the same as the default character set.

This is primarily used when we get some indication that the remote supports UTF-8 to promote the default charset from US-ASCII to UTF-8. The US-ASCII decoder will always decode UTF-8, but it's useful for the consumer to know whether the remote actually supports UTF-8, in order to decide whether to send things like emojis.

func (*Charset) SetBinaryDecode

func (c *Charset) SetBinaryDecode(decode bool)

SetBinaryDecode is used by the TRANSMIT-BINARY telopt to establish whether the printer should use binary mode

func (*Charset) SetBinaryEncode

func (c *Charset) SetBinaryEncode(encode bool)

SetBinaryEncode is used by the TRANSMIT-BINARY telopt to establish whether the keyboard should use binary mode

func (*Charset) SetNegotiatedDecodingCharset added in v0.2.0

func (c *Charset) SetNegotiatedDecodingCharset(codePage string) error

SetNegotiatedDecodingCHarset modifies the negotiated printer charset to the requested character set.

func (*Charset) SetNegotiatedEncodingCharset added in v0.2.0

func (c *Charset) SetNegotiatedEncodingCharset(codePage string) error

SetNegotiatedEncodingCharset modifies the negotiated keyboard charset to the requested character set

type CharsetUsage

type CharsetUsage byte

CharsetUsage indicates when charsets negotiated via the CHARSET telopt are used. According to RFC, negotiated telopts are only to be used when TRANSMIT-BINARY is active, but many implementations are incorrect. On the other hand, many implementations don't actually do anything, they just advertise that the server can handle UTF-8, so following the RFC doesn't do any harm.

const (
	// CharsetUsageBinary indicates that text communications should use a CHARSET-negotiated character set
	// if the connection is in BINARY mode, and the default character set otherwise
	CharsetUsageBinary CharsetUsage = iota
	// CharsetUsageAlways indicates that text communications should always use a CHARSET-negotiated character
	// set (if any) instead of the default character set
	CharsetUsageAlways
)

type Command

type Command struct {
	// OpCode is the code that comes after IAC in this command. Bear in mind that
	// subnegotiations, which come in the form of IAC SB <bytes> IAC SE, are represented
	// as a single command object with the OpCode of SB. IAC SE is never sent in its
	// own command.
	OpCode byte
	// Option indicates which telopt this command is referring to, if the command has one.
	// IAC WILL/WONT/DO/DONT/SB are always followed by a byte indicating a telopt.
	Option TelOptCode
	// Subnegotiation contains a byte slice containing the bytes, if any, that came
	// between IAC SB and IAC SE.  For non-SB commands, this slice is empty.
	Subnegotiation []byte
}

Command is a struct that indicates some sort of IAC command either received from or sent to the remote. Any possible command can be represented by this struct.

type CommandData added in v0.3.0

type CommandData struct {
	Command
}

CommandData is a type representing a single IAC command received from telnet

func (CommandData) EscapedString added in v0.3.0

func (o CommandData) EscapedString(terminal TelOptLibrary) string

func (CommandData) String added in v0.3.0

func (o CommandData) String() string

type ControlCodeData added in v0.5.0

type ControlCodeData ansi.ControlCode

func (ControlCodeData) EscapedString added in v0.5.0

func (o ControlCodeData) EscapedString(terminal TelOptLibrary) string

func (ControlCodeData) String added in v0.5.0

func (o ControlCodeData) String() string

type CsiData added in v0.5.0

type CsiData struct {
	ansi.CsiSequence
}

func (CsiData) EscapedString added in v0.5.0

func (o CsiData) EscapedString(terminal TelOptLibrary) string

type DcsData added in v0.5.0

type DcsData struct {
	ansi.DcsSequence
}

func (DcsData) EscapedString added in v0.5.0

func (o DcsData) EscapedString(terminal TelOptLibrary) string

type EncodingState added in v0.5.0

type EncodingState int
const (
	EncodingUnsure EncodingState = iota
	EncodingInvalid
	EncodingValid
)

type ErrorHandler added in v0.2.0

type ErrorHandler func(t *Terminal, err error)

ErrorHandler is an event hook type that receives errors

type EscData added in v0.5.0

type EscData struct {
	ansi.EscSequence
}

func (EscData) EscapedString added in v0.5.0

func (o EscData) EscapedString(terminal TelOptLibrary) string

type EventHook

type EventHook[T any] func(terminal *Terminal, data T)

EventHook is a type for function pointers that are registered to receive events

type EventHooks

type EventHooks struct {
	EncounteredError []ErrorHandler
	PrinterOutput    []TerminalDataHandler
	OutboundData     []TerminalDataHandler

	TelOptEvent []TelOptEventHandler
}

EventHooks is used to pass in a set of pre-registered event hooks to a Terminal when calling NewTerminal. See TerminalConfig for more info.

type EventPublisher

type EventPublisher[U any] struct {
	// contains filtered or unexported fields
}

EventPublisher is a type used to register and fire arbitrary events

func NewPublisher

func NewPublisher[U any, T ~func(terminal *Terminal, data U)](hooks []T) *EventPublisher[U]

NewPublisher creates a new EventPublisher for a particular EventHook. A slice of hooks can be passed in- in which case the hooks will be registered to receive events from the publisher. Otherwise, nil can be passed in.

func (*EventPublisher[U]) Fire

func (e *EventPublisher[U]) Fire(terminal *Terminal, eventData U)

Fire calls the event for all EventHook instances registered to this publisher with the provided parameters

func (*EventPublisher[U]) Register

func (e *EventPublisher[U]) Register(hook EventHook[U])

Register registers a single EventHook to receive events from this publisher.

type Middleware added in v0.8.0

type Middleware interface {
	Handle(terminal *Terminal, data TerminalData, next TerminalDataHandler)
}

type MiddlewareStack added in v0.8.0

type MiddlewareStack struct {
	// contains filtered or unexported fields
}

func NewMiddlewareStack added in v0.8.0

func NewMiddlewareStack(lineOut TerminalDataHandler, middlewares ...Middleware) *MiddlewareStack

func (*MiddlewareStack) LineIn added in v0.8.0

func (s *MiddlewareStack) LineIn(t *Terminal, data TerminalData)

func (*MiddlewareStack) PushMiddleware added in v0.8.0

func (s *MiddlewareStack) PushMiddleware(middleware Middleware)

func (*MiddlewareStack) QueueMiddleware added in v0.8.0

func (s *MiddlewareStack) QueueMiddleware(middleware Middleware)

func (*MiddlewareStack) RemoveMiddleware added in v0.8.0

func (s *MiddlewareStack) RemoveMiddleware(middleware Middleware)

type OscData added in v0.5.0

type OscData struct {
	ansi.OscSequence
}

func (OscData) EscapedString added in v0.5.0

func (o OscData) EscapedString(terminal TelOptLibrary) string

type PmData added in v0.5.0

type PmData struct {
	ansi.PmSequence
}

func (PmData) EscapedString added in v0.5.0

func (o PmData) EscapedString(terminal TelOptLibrary) string

type PromptCommands

type PromptCommands uint32

PromptCommands is a set of flags indicating which IAC opcodes indicate the end of a prompt line. MUDs like to use GA or EOR to indicate where to place the cursor at the end of a prompt. But GA can be turned off with the SUPPRESS-GO-AHEAD telopt, and EOR has to be turned on with the EOR telopt, so this helps us track where we're at.

const (
	// PromptCommandGA refers to the IAC GA command. This command was initially used as part
	// of telnet's scheme for supporting half-duplex terminals.  However, half-duplex terminals
	// were rapidly phased out after the telnet protocol was introduced and eventually came to be
	// used for a variety of hacky boutique purposes.  For MUDs and BBSs it is often deactivated
	// via the SUPPRESS-GO-AHEAD telopt in order to activate character mode. It is sometimes used
	// as a prompt indicator on MUDs
	PromptCommandGA PromptCommands = 1 << iota
	// PromptCommandEOR refers to the IAC EOR command. This was introduced as part of the EOR telopt
	// and can be used for any purpose an application would like to use it for.  It is mainly only
	// used as a prompt indicator on MUDs.
	PromptCommandEOR
)

type PromptData added in v0.3.0

type PromptData PromptCommands

PromptData is a type representing a hint received from telnet about where the user prompt should be placed in the output stream.

func (PromptData) EscapedString added in v0.3.0

func (o PromptData) EscapedString(terminal TelOptLibrary) string

func (PromptData) String added in v0.3.0

func (o PromptData) String() string

type SosData added in v0.5.0

type SosData struct {
	ansi.SosSequence
}

func (SosData) EscapedString added in v0.5.0

func (o SosData) EscapedString(terminal TelOptLibrary) string

type TelOptCode

type TelOptCode byte

TelOptCode - each telopt has a unique identification number between 0 and 255

type TelOptEvent

type TelOptEvent interface {
	// String produces human-readable text describing the event that occurred
	String() string
	// Option is the specific telopt that experienced an event
	Option() TelnetOption
}

TelOptEvent is an interface used for all TelOptEvents issued by anyone, both TelOptStateChangeEvent, which is issued by this terminal, and other events issued by telopts themselves

type TelOptEventHandler added in v0.2.0

type TelOptEventHandler func(t *Terminal, event TelOptEvent)

TelOptEventHandler is an event hook type that receives arbitrary events raised by telopts with Terminal.RaiseTelOptEvent

type TelOptLibrary added in v0.2.0

type TelOptLibrary interface {
	CommandString(c Command) string
}

TelOptLibrary is an interface used to abstract Terminal from PrinterOutput for the benefit of anyone who may be using TelnetScanner without Terminal.

Any method that accepts this type will likely want to use *Terminal

type TelOptSide

type TelOptSide byte

TelOptSide is used to distinguish the two "sides" of a telopt. Telopts can be active on either the local side, the remote side, both, or neither. As a result, the current state of a telopt needs to be requested for a particular side of the connection.

const (
	TelOptSideUnknown TelOptSide = iota
	TelOptSideLocal
	TelOptSideRemote
)

func (TelOptSide) String

func (s TelOptSide) String() string

type TelOptState

type TelOptState byte

TelOptState indicates whether the telopt is currently active, inactive, or other

const (
	// TelOptUnknown is the zero value for the telopt state value.  This is generally interchangeable with
	// TelOptInactive
	TelOptUnknown TelOptState = iota
	// TelOptInactive indicates that the option is not currently active
	TelOptInactive
	// TelOptRequested indicates that this client has sent a request to activate the telopt to the other party
	// but has not yet heard back
	TelOptRequested
	// TelOptActive indicates that the option is currently active
	TelOptActive
)

func (TelOptState) String

func (s TelOptState) String() string

type TelOptStateChangeEvent

type TelOptStateChangeEvent struct {
	TelnetOption TelnetOption
	Side         TelOptSide
	OldState     TelOptState
	NewState     TelOptState
}

TelOptStateChangeEvent is a TelOptEvent that indicates that a single telopt has changed state on one side of the connection

func (TelOptStateChangeEvent) Option added in v0.2.0

func (TelOptStateChangeEvent) String added in v0.2.0

func (e TelOptStateChangeEvent) String() string

type TelOptUsage

type TelOptUsage byte

TelOptUsage indicates how a particular TelnetOption is supposed to be used by the terminal. Whether it is permitted to be activated locally or on the remote, and whether we should request activation locally or on the remote when the Terminal launches.

const (
	// TelOptAllowRemote - if the remote requests to activate this telopt on their side,
	// we will permit it
	TelOptAllowRemote TelOptUsage = 1 << iota

	// TelOptAllowLocal - if the remote requests that we activate this telopt on our side,
	// we will comply
	TelOptAllowLocal
)
const (
	// TelOptRequestRemote - we will request that the remote activate this telopt during
	// Terminal startup
	TelOptRequestRemote TelOptUsage = TelOptAllowRemote | telOptOnlyRequestRemote
	// TelOptRequestLocal - we will request that the remote allow us to activate this
	// telopt on our side during Terminal startup
	TelOptRequestLocal TelOptUsage = TelOptAllowLocal | telOptOnlyRequestLocal
)

type TelnetKeyboard

type TelnetKeyboard struct {
	// contains filtered or unexported fields
}

TelnetKeyboard is a Terminal subsidiary that is in charge of sending outbound data to the remote peer.

func (*TelnetKeyboard) ClearLock

func (k *TelnetKeyboard) ClearLock(lockName string)

ClearLock will clear a named lock in order to end buffering (assuming there are no other active locks) and immediately write buffered text.

func (*TelnetKeyboard) ClearPromptCommand

func (k *TelnetKeyboard) ClearPromptCommand(flag PromptCommands)

ClearPromptCommand will deactivate a particular prompt command and prevent it from being sent by the keyboard. Prompt commands are IAC GA/IAC EOR, commands that indicate to the remote where to place a prompt

func (*TelnetKeyboard) HasActiveLock

func (k *TelnetKeyboard) HasActiveLock(lockName string) bool

HasActiveLock will indicate whether a named lock is currently active on the keyboard

func (*TelnetKeyboard) LineOut added in v0.6.0

func (k *TelnetKeyboard) LineOut(t *Terminal, data TerminalData)

func (*TelnetKeyboard) SendPromptHint

func (k *TelnetKeyboard) SendPromptHint()

SendPromptHint will send a IAC GA or IAC EOR if possible, indicating to the remote to place a prompt after the most-recently-sent text.

This command will send an EOR if that telopt is active. Otherwise, it will send a GA if it isn't being suppressed. If it is not valid to send either prompt hint, this method will do nothing.

If one wants to send an IAC GA or IAC EOR command, this method should be used rather than WriteCommand. Commands sent via WriteCommand will not be buffered when the keyboard is under a lock, so prompt hints sent via WriteCommand will arrive before the prompt text when a keyboard lock is active.

func (*TelnetKeyboard) SetLock

func (k *TelnetKeyboard) SetLock(lockName string, duration time.Duration)

SetLock will buffer all text output without sending until the provided lockName is cleared with ClearLock, or until the provided duration expires. This method is primarily used by telopts to handle changes in communication semantics. According to the Telnet RFC, communication semantics should change the moment a side sends a command that requests that they change. Since it is not known at that time whether the remote can receive these semantics, it is recommended that writes are buffered until the remote responds to the request.

func (*TelnetKeyboard) SetPromptCommand

func (k *TelnetKeyboard) SetPromptCommand(flag PromptCommands)

SetPromptCommand will activate a particular prompt command and permit it to be sent by the keyboard. Prompt commands are IAC GA/IAC EOR, commands that indicate to the remote where to place a prompt

func (*TelnetKeyboard) WrapWriter added in v0.8.0

func (k *TelnetKeyboard) WrapWriter(wrap func(io.Writer) (io.Writer, error)) error

func (*TelnetKeyboard) WriteCommand

func (k *TelnetKeyboard) WriteCommand(c Command, postSend func() error)

WriteCommand will queue a command to be sent to the remote. A post-send event can be provided, which is useful for cases where the provided command will signal to the remote that the communication semantic is changing in some way. If the postSend method is not nil, it will be executed immediately after writing the command to the output stream, and can be used to change the communication semantic for future writes.

func (*TelnetKeyboard) WriteString

func (k *TelnetKeyboard) WriteString(str string)

WriteString will queue some text to be sent to the remote

type TelnetOption

type TelnetOption interface {
	// Code returns the code this option should be registered under. This method is expected to run succesfully
	// before Initialize is called.
	Code() TelOptCode
	// String should return the short name used to refer to this option. This method is expected to run
	// successfully before Initialize is called.
	String() string
	// Usage indicates the way in which this TelOpt is permitted to be used. This method
	// is expected to run successfully before Initialize is called.
	Usage() TelOptUsage

	// Initialize sets the terminal used by this telopt and performs any other necessary
	// business before other methods may be called.
	Initialize(terminal *Terminal)
	// Terminal returns the current terminal. This method must successfully return nil
	// before Initialize is called.
	Terminal() *Terminal

	// LocalState returns the current state of this option locally- receiving a DO command will activate
	// it and a DONT command will deactivate it.
	LocalState() TelOptState
	// RemoteState returns the current state of this option in the remote- receiving a WILL command
	// will activate it and a WONT command will deactivate it
	RemoteState() TelOptState

	// TransitionLocalState is called when the terminal attempts to change this option to a new state
	// locally.  This is not called when the option is initialized to Inactive at the start of a new
	// terminal, and it will not be called if the terminal tries to repeatedly transition this option
	// to the same state.
	//
	// This method returns a simple callback method.  If that callback is not nil, then it will
	// be executed as soon as the command associated with this state change is written to the
	// keyboard stream (or immediately if no command is necessary).  This is vital for cases when
	// a telopt changes the semantics of outbound communications, since that semantic change needs
	// to take place immediately after we send the command indicating that we will be changing things.
	TransitionLocalState(newState TelOptState) (func() error, error)
	// TransitionRemoteState is calledw hen the terminal attempts to change this option to a new state
	// for the remote.  This is not called when the option is initialized to Inactive at the start of
	// a new terminal, and it will nto be called if the terminal tries to repeatedly transition this
	// option to the same state
	//
	// This method returns a simple callback method.  If that callback is not nil, then it will
	// be executed as soon as the command associated with this state change is written to the
	// keyboard stream (or immediately if no command is necessary).  This is vital for cases when
	// a telopt changes the semantics of outbound communications, since that semantic change needs
	// to take place immediately after we send the command indicating that we will be changing things.
	TransitionRemoteState(newState TelOptState) (func() error, error)

	// Subnegotiate is called when a subnegotiation request arrives from the remote party. This will only
	// be called when the option is active on at least one side of the connection
	Subnegotiate(subnegotiation []byte) error
	// SubnegotiationString creates a legible string for a subnegotiation request
	SubnegotiationString(subnegotiation []byte) (string, error)
}

TelnetOption is an object representing a single telopt within the currently-running terminal. Each terminal has its own telopt object for each telopt it supports.

type TelnetPrinter

type TelnetPrinter struct {
	// contains filtered or unexported fields
}

TelnetPrinter is a Terminal subsidiary that parses text sent by the remote peer. This object is largely not used by consumers. It has a few methods that are consumed by telopts, but received text is largely handled through the Terminal itself.

func (*TelnetPrinter) ClearPromptCommand

func (p *TelnetPrinter) ClearPromptCommand(flag PromptCommands)

ClearPromptCommand will deactivate a particular prompt command and cause it to be ignored by the printer. Prompt commands are IAC GA/IAC EOR, commands that indicate to the consumer where to place a prompt

func (*TelnetPrinter) Middlewares added in v0.8.0

func (p *TelnetPrinter) Middlewares() *MiddlewareStack

func (*TelnetPrinter) SetPromptCommand

func (p *TelnetPrinter) SetPromptCommand(flag PromptCommands)

SetPromptCommand will activate a particular prompt command and permit it to be received by the printer. Prompt commands are IAC GA/IAC EOR, commands that indicate to the consumer where to place a prompt

func (*TelnetPrinter) WrapReader added in v0.8.0

func (p *TelnetPrinter) WrapReader(wrap func(reader io.Reader) (io.Reader, error)) error

type TelnetScanner added in v0.2.0

type TelnetScanner struct {
	// contains filtered or unexported fields
}

TelnetScanner is used internally by TelnetPrinter to read sequences from a Reader and output units of received output. It is exported due to the object being potentially useful outside the context of this library's Terminal object. If you intend to use Terminal, there is no need to use or think about this type.

TelnetScanner's Scan method works like an io.Scanner, except that it accepts a context.Context. If the ctx is cancelled or timed out, Scan will return false with with the appropriate error. Otherwise, it will return true until it reaches the input stream's EOF. Like io.Scanner, Scan is a blocking call.

After Scan returns, even if it returns false, Err and Output may have useful return values. Output returns a PrinterOutput object, or nil. PrinterOutput may be one of the PrinterOutput implementations defined in this package (TextOutput, PromptOutput, SequenceOutput, etc.).

PrinterOutput's String method will always return the correct text to print to a VT100 compatible terminal, and EscapedString will always return the correct text to print to a default log in which you'd like to see escape sequences, commands, and control characters.

Otherwise, you can inspect the PrinterOutput objects by using a type switch.

As with Scanner, one should deal with the Output() return value, if any, before dealing with the Err() return value.

func NewTelnetScanner added in v0.2.0

func NewTelnetScanner(charset *Charset, inputStream io.Reader) *TelnetScanner

NewTelnetScanner creates a new TelnetScanner from a Charset (used to decode bytes from the stream) and an input stream

func (*TelnetScanner) Err added in v0.2.0

func (s *TelnetScanner) Err() error

Err returns the error, if any, raised by the most recent call to Scan

func (*TelnetScanner) Output added in v0.2.0

func (s *TelnetScanner) Output() TerminalData

Output returns the PrinterOutput, if any, assembled by the most recent call to Scan

func (*TelnetScanner) Scan added in v0.2.0

func (s *TelnetScanner) Scan(ctx context.Context) bool

Scan will block until either the provided context is done, or a complete block of data is received from the input stream. "Complete" is subjective, but the TelnetScanner will not output partial ANSI sequences or partial glyphs of text.

Scan returns true if the caller should continue to call Scan to receive additional data. After calling Scan, Err and Output should be called to check for useful data.

func (*TelnetScanner) ScanTelnet added in v0.2.0

func (s *TelnetScanner) ScanTelnet(data []byte, atEOF bool) (advance int, token []byte, err error)

ScanTelnet is a method used as the split method for io.Scanner. It will receive chunks of text or commands as individual tokens.

type Terminal

type Terminal struct {
	// contains filtered or unexported fields
}

Terminal is a wrapper around a connection to enable telnet communications over the connection. Telnet's base protocol doesn't distinguish between client and server, so there is only one terminal type for both sides of the connection. A few telopts have different behavior for the client and server side though, so the terminal is aware of which side it is supposed to be.

Telnet functions as a "full duplex" protocol, meaning that it does not operate in a request-response type of semantic that users may be familiar with. Instead, it's best to envision a telnet connection as two asynchronous datastreams- a printer reader that produces text from the remote peer, and a keyboard writer that sends text to the remote peer.

Text from the printer is sent to the consumer of the Terminal via the many event hooks that can be registered for. The PrinterOutput hook produces structured data that is read in from the peer, and output is provided to the printer directly by calling terminal.Keyboard().Send*

Telnet has a mechanism for sending and receiving Command objects. Most of these are related to telopt negotiation, which the Terminal handles on your behalf based on the telopt preferences provided at creation time. In order to receive commands from the other side, it's best to register for TelOptEvent hooks, which provide the results of received commands in a more legible format. Generally, the user should not write commands unless they really, really know what they're doing. If you want to signal a prompt to the remote with IAC GA, use terminal.Keyboard().SendPromptHint().

The user should bear in mind that the terminal runs three (substantive) goroutines: one for the printer, one for the keyboard, and one for the terminal. The terminal loop receives data from both of the other loops and forwards it to registered hooks, which means that blocking calls in hook methods that last long enough will block functioning of the terminal altogether. It is the responsibility of the consumer to move long-running calls to their own concurrency scheme where necessary.

func NewTerminal

func NewTerminal(ctx context.Context, conn net.Conn, config TerminalConfig) (*Terminal, error)

NewTerminal initializes a new terminal object from a net.Conn and begins reading from the printer and writing to the keyboard. Telopt negotiation begins with the remote immediately when this method is called.

The terminal will continue until either the passed context is cancelled, or until the connection is closed.

All functioning of this terminal is determined by the properties passed in the TerminalConfig object. See that type for more information.

func NewTerminalFromPipes added in v0.2.0

func NewTerminalFromPipes(ctx context.Context, reader io.Reader, writer io.Writer, config TerminalConfig) (*Terminal, error)

NewTerminalFromPipes initializes a new terminal from a Reader and Writer instead of a net.Conn. This is useful for testing, or when data is arriving via more circuitous means than a simple connection. This Terminal will continue until BOTH the reader and writer are closed (or the context is cancelled). Only closing one will cause the connection to stall but the terminal will remain active, so that should never be done.

func (*Terminal) Charset

func (t *Terminal) Charset() *Charset

Charset returns the relevant Charset object for the terminal, which stores what charset the terminal uses for encoding & decoding by default, what charset has been negotiated for use with TRANSMIT-BINARY, etc.

func (*Terminal) CommandString

func (t *Terminal) CommandString(c Command) string

CommandString converts a Command object into a legible stream. This can be useful when logging a received command object

func (*Terminal) Keyboard

func (t *Terminal) Keyboard() *TelnetKeyboard

Keyboard returns the object that is used for sending outbound communications

func (*Terminal) Printer

func (t *Terminal) Printer() *TelnetPrinter

Printer returns the object that is used for receiving inbound communciations

func (*Terminal) RaiseTelOptEvent

func (t *Terminal) RaiseTelOptEvent(event TelOptEvent)

RaiseTelOptEvent is called by telopt implementations, and the Terminal, to inject an event into the terminal event stream. Telopts can use this method to fire arbitrary events that can be interpreted by the consumer. This terminal will use this method to inject TelOptStateChangeEvent when negotiations cause a telopt to change its state. This is good for event-delivery telopts such as GCMP, but it can also be used for things like NAWS to alert the consumer that basic data has been collected from the remote.

func (*Terminal) RegisterEncounteredErrorHook

func (t *Terminal) RegisterEncounteredErrorHook(encounteredError ErrorHandler)

RegisterEncounteredErrorHook will register an event to be called when an error was encountered by the terminal or one of its subsidiaries. Not all errors will be sent via this hook: just errors that are not returned to the user immediately.

If a method call to Terminal or one of its subsidiaries immediately returns an error to the user, it will not be delivered via this hook. If an error ends terminal processing immediately, it will not be delivered via this hook, it will be delivered via WaitForExit.

func (*Terminal) RegisterOutboundDataHook added in v0.3.0

func (t *Terminal) RegisterOutboundDataHook(outboundText TerminalDataHandler)

RegisterOutboundDataHook will register an event to be called when something has been sent from the keyboard. This is primarily useful for debug logging.

func (*Terminal) RegisterPrinterOutputHook added in v0.2.0

func (t *Terminal) RegisterPrinterOutputHook(printerOutput TerminalDataHandler)

RegisterPrinterOutputHook will register an event to be called when data is received from the printer.

func (*Terminal) RegisterTelOptEventHook

func (t *Terminal) RegisterTelOptEventHook(telOptEvent TelOptEventHandler)

RegisterTelOptEventHook will register an event to be called when a telopt delivers an event via RaiseTelOptEvent.

func (*Terminal) Side

func (t *Terminal) Side() TerminalSide

Side returns a TerminalSide object indicating whether the terminal represents a client or server

func (*Terminal) WaitForExit

func (t *Terminal) WaitForExit() error

WaitForExit will block until the terminal has ceased operation, either due to the context passed to NewTerminal being cancelled, or due to the underlying data streams closing.

type TerminalConfig

type TerminalConfig struct {
	// DefaultCharsetName is the registered IANA name of the character set to use for all communications not
	// sent via a negotiated charset (via the CHARSET telopt). RFC 854 (Telnet Protocol) specifies that by
	// default, communications take place in ASCII encoding.  RFC 5198 specified that since 2008, communications
	// should by default take place in UTF-8.  However, many active telnet services and a vanishingly small
	// number of telnet clients have not been updated to use UTF-8. While UTF-8, as a superset of ASCII,
	// will generally function just fine as a communications protocol with ASCII systems, it can be useful
	// to make US-ASCII the default character set, allow the remote to negotiate to UTF-8 if they want, and
	// use the current character set to determine support for sending things like emojis.
	//
	// Lastly, in the pre-2008 period, many telnet services were established in languages that could not use
	// US-ASCII under any circumstances and used other character sets as the default rather than implementing
	// CHARSET appropriately. For these services, launching with an alternative charset such as Big5 can be
	// necessary.
	//
	// The charset specified here will be used initially for all text communications until a different character
	// set is negotiated with the CHARSET telopt.  If there are non-charset text communications (see CharsetUsage),
	// this will be used for them.  Text sent in telopt subnegotiations will always use UTF-8 regardless of this
	// setting.
	//
	// If this characters set is US-ASCII and the remote indicates support for UTF-8 via a CHARSET negotiation
	// or some other mechanism, the default character set will be promoted to UTF-8.
	DefaultCharsetName string

	// FallbackCharsetName can be left empty. If populated, it is the registered IANA name for
	// a character set that will be used when the normal character decoding fails. If decoding
	// a character from the printer results in the unicode replacement character, decoding will
	// be retried using this character set. If decoding does not result in a unicode replacement
	// character, the fallback character set will continue to be used until the next control code
	// (including line break), command, or escape sequence, even if the fallback character set
	// starts to fail during that time.
	//
	// This can be useful when connecting to BBS servers (or certain MUDs that act like them),
	// because some use CP437 without any CHARSET negotiation at all. Since all bytes are valid
	// CP437 bytes, replacing failed unicode bytes with CP437 bytes will usually detect and decode
	// these servers without difficulty, with the minor exception of the small number of sequences
	// that result in valid UTF-8 codepoints, such as \xdb\xb1.
	FallbackCharsetName string

	// CharsetUsage is only relevant if a new characters set has been negotiated via the CHARSET telopt.
	// This field indicates when the negotiated character set will be used
	// to send and receive text. According to RFC 2066, the charset is only to be used in BINARY mode
	// (RFC 856).  However, some systems will use it all the time, or only use CHARSET to advertise that the
	// server is speaking UTF-8 without actually implementing any encoding functionality. As a result, we offer
	// the option to always use the negotiated charset or only use it when BINARY mode is active.
	//
	// Text sent in telopt subnegotiations will always use UTF-8 regardless of this setting.
	CharsetUsage CharsetUsage

	// Side indicates whether this terminal is intended to be the client or server. Even though RFC 854
	// (Telnet Protocol) does not have the concept of a client or server, just local and remote, some TelOpts,
	// such as CHARSET, indicate different behaviors for clients and servers.
	Side TerminalSide

	// TelOpts indicates which TelOpts the terminal should request from the remote, and which the remote
	// should be permitted to request from us.
	TelOpts []TelnetOption

	// EventHooks is a set of callbacks that the terminal will call when the relevant
	// event occurs.  You can register additional callbacks after creation with
	// Terminal.Register* methods.
	EventHooks EventHooks

	// PrinterMiddlewares is a set of middlewares that should process output from the printer
	// before it is sent to registered hooks
	PrinterMiddlewares []Middleware

	// KeyboardMiddlewares is a set of middlewares that should process data sent
	// to the keyboard before it is sent to the network connection
	KeyboardMiddlewares []Middleware
}

type TerminalData added in v0.3.0

type TerminalData interface {
	String() string
	EscapedString(terminal TelOptLibrary) string
}

TerminalData is an interface output by Terminal and TelnetScanner to represent a single unit of output from telnet

func NextOutput added in v0.4.0

func NextOutput[T string | []byte](p *TerminalDataParser, data T) TerminalData

type TerminalDataHandler added in v0.3.0

type TerminalDataHandler func(t *Terminal, output TerminalData)

TerminalDataHandler is an event hook type that receives text, control codes, escape sequences, and commands from the printer

type TerminalDataParser added in v0.4.0

type TerminalDataParser struct {
	// contains filtered or unexported fields
}

func NewTerminalDataParser added in v0.4.0

func NewTerminalDataParser() *TerminalDataParser

func (*TerminalDataParser) FireAll added in v0.4.0

func (p *TerminalDataParser) FireAll(terminal *Terminal, data string, publisher *EventPublisher[TerminalData])

func (*TerminalDataParser) FireSingle added in v0.4.0

func (p *TerminalDataParser) FireSingle(terminal *Terminal, data string, hook TerminalDataHandler)

func (*TerminalDataParser) Flush added in v0.4.0

func (p *TerminalDataParser) Flush() TerminalData

type TerminalSide

type TerminalSide byte

TerminalSide indicates whether this terminal represents a client or server. Technically speaking, telnet is a peer-to-peer protocol, more concerned with "local and remote" than "client and server". Some RFCs (mainly CHARSET) have distinct behavior for clients and server, though.

const (
	SideUnknown TerminalSide = iota
	SideClient
	SideServer
)

type TextData added in v0.3.0

type TextData string

TextData is a type representing printable text that has been received from telnet

func (TextData) EscapedString added in v0.3.0

func (o TextData) EscapedString(terminal TelOptLibrary) string

func (TextData) String added in v0.3.0

func (o TextData) String() string

type TypedTelnetOption

type TypedTelnetOption[OptionStruct any] interface {
	*OptionStruct
	TelnetOption
}

TypedTelnetOption - this is used as a bit of a hack for GetTelOpt. It allows the generic semantic for that method to work

Directories

Path Synopsis
examples module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL