Initial commit
Some checks failed
Cargo test & clippy / build (push) Has been cancelled
Cargo test & clippy / rust-clippy-analyze (push) Has been cancelled
Cargo security audit / audit (push) Failing after 4s

This commit is contained in:
2024-03-02 16:04:35 +01:00
commit 16dd18b400
21 changed files with 3179 additions and 0 deletions

23
.github/workflows/cargo-audit.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Cargo security audit
on:
schedule:
- cron: "31 1 * * *"
push:
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
jobs:
audit:
defaults:
run:
working-directory: ./backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}

32
.github/workflows/cargo-test.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Cargo test & clippy
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
rust-clippy-analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Run rust-clippy
run: cargo clippy --all-features

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.idea

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"preLaunchTask": "rust: cargo build",
"program": "target/debug/telnet_server",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

30
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "telnet_server: Start TELNET Docker",
"type": "shell",
"command": "./telnet/run_telnet.sh",
"args": [
"${input:host}",
"${input:port}"
],
"detail": "Starts a Docker container (in interactive mode) that runs TELNET and connects to given host and port",
"problemMatcher": []
}
],
"inputs": [
{
"id": "host",
"type": "promptString",
"default": "host.docker.internal",
"description": "Which host should the TELNET client connect to?"
},
{
"id": "port",
"type": "promptString",
"default": "9000",
"description": "Which port should the TELNET client connect to?"
},
]
}

7
Cargo.lock generated Normal file
View File

@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "telnet_server"
version = "0.0.1"

21
Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "telnet_server"
version = "0.0.1"
edition = "2024"
authors = ["Laika Schmidt <laika.schmidt@magenta.de>"]
description = "A bare TELNET server provider"
readme = "README.md"
repository = "https://github.com/its-laika/telnet_server"
license = "MIT"
keywords = ["TELNET"]
exclude = [".github/", ".vscode/", "resources/", "telnet/", ".gitignore"]
[lib]
name = "telnet_server"
path = "src/lib.rs"
[[bin]]
name = "telnet_server"
path = "src/bin/main.rs"
[dependencies]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 - 2025 Laika Schmidt
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.

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# telnet_server
_...because nothing beats the charm of the seventies!_
## Goals
This lib should provide everything to build a service (based on TELNET) without
having to touch any TELNET or tcp specifics. Message communication should only
happen between this lib and the code providing the service.
See [src/bin/main.rs](src/bin/main.rs#l23) on how I think this should work.
## Status
Non functional and heavily WIP. **DO NOT USE!**
## Running TELNET
As TELNET is not anymore part of modern operating systems (thank god), I've
created a minimal Dockerfile that let's me use TELNET on CLI. TELNET starts via
this command:
```sh
./telnet/run_telnet.sh HOST PORT
```
For development, _HOST_ is "host.docker.internal" and _PORT_ is 9000.
## For f*cks sake, why TELNET???
Because it looked interesting. Honestly, even if I get it finished, the library
likely won't be used _at all_.
## License
The code is licensed under the [MIT License](LICENSE). The RFCs in the
[resources](resources) folder have a different copyright but are allowed (and
encouraged) to be copied and redistributed if unchanged.

36
resources/ascii.txt Normal file
View File

@@ -0,0 +1,36 @@
Source: https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html (downloaded: 2024-03-03 12:24 UTC)
Dec Char Dec Char Dec Char Dec Char
--------- --------- --------- ----------
0 NUL (null) 32 SPACE 64 @ 96 `
1 SOH (start of heading) 33 ! 65 A 97 a
2 STX (start of text) 34 " 66 B 98 b
3 ETX (end of text) 35 # 67 C 99 c
4 EOT (end of transmission) 36 $ 68 D 100 d
5 ENQ (enquiry) 37 % 69 E 101 e
6 ACK (acknowledge) 38 & 70 F 102 f
7 BEL (bell) 39 ' 71 G 103 g
8 BS (backspace) 40 ( 72 H 104 h
9 TAB (horizontal tab) 41 ) 73 I 105 i
10 LF (NL line feed, new line) 42 * 74 J 106 j
11 VT (vertical tab) 43 + 75 K 107 k
12 FF (NP form feed, new page) 44 , 76 L 108 l
13 CR (carriage return) 45 - 77 M 109 m
14 SO (shift out) 46 . 78 N 110 n
15 SI (shift in) 47 / 79 O 111 o
16 DLE (data link escape) 48 0 80 P 112 p
17 DC1 (device control 1) 49 1 81 Q 113 q
18 DC2 (device control 2) 50 2 82 R 114 r
19 DC3 (device control 3) 51 3 83 S 115 s
20 DC4 (device control 4) 52 4 84 T 116 t
21 NAK (negative acknowledge) 53 5 85 U 117 u
22 SYN (synchronous idle) 54 6 86 V 118 v
23 ETB (end of trans. block) 55 7 87 W 119 w
24 CAN (cancel) 56 8 88 X 120 x
25 EM (end of medium) 57 9 89 Y 121 y
26 SUB (substitute) 58 : 90 Z 122 z
27 ESC (escape) 59 ; 91 [ 123 {
28 FS (file separator) 60 < 92 \ 124 |
29 GS (group separator) 61 = 93 ] 125 }
30 RS (record separator) 62 > 94 ^ 126 ~
31 US (unit separator) 63 ? 95 _ 127 DEL

1287
resources/rfc1184.txt Normal file

File diff suppressed because it is too large Load Diff

855
resources/rfc854.txt Normal file
View File

@@ -0,0 +1,855 @@
Source: https://datatracker.ietf.org/doc/html/rfc854 (downloaded: 2024-03-03 12:20 UTC)
Network Working Group J. Postel
Request for Comments: 854 J. Reynolds
ISI
Obsoletes: NIC 18639 May 1983
TELNET PROTOCOL SPECIFICATION
This RFC specifies a standard for the ARPA Internet community. Hosts on
the ARPA Internet are expected to adopt and implement this standard.
INTRODUCTION
The purpose of the TELNET Protocol is to provide a fairly general,
bi-directional, eight-bit byte oriented communications facility. Its
primary goal is to allow a standard method of interfacing terminal
devices and terminal-oriented processes to each other. It is
envisioned that the protocol may also be used for terminal-terminal
communication ("linking") and process-process communication
(distributed computation).
GENERAL CONSIDERATIONS
A TELNET connection is a Transmission Control Protocol (TCP)
connection used to transmit data with interspersed TELNET control
information.
The TELNET Protocol is built upon three main ideas: first, the
concept of a "Network Virtual Terminal"; second, the principle of
negotiated options; and third, a symmetric view of terminals and
processes.
1. When a TELNET connection is first established, each end is
assumed to originate and terminate at a "Network Virtual Terminal",
or NVT. An NVT is an imaginary device which provides a standard,
network-wide, intermediate representation of a canonical terminal.
This eliminates the need for "server" and "user" hosts to keep
information about the characteristics of each other's terminals and
terminal handling conventions. All hosts, both user and server, map
their local device characteristics and conventions so as to appear to
be dealing with an NVT over the network, and each can assume a
similar mapping by the other party. The NVT is intended to strike a
balance between being overly restricted (not providing hosts a rich
enough vocabulary for mapping into their local character sets), and
being overly inclusive (penalizing users with modest terminals).
NOTE: The "user" host is the host to which the physical terminal
is normally attached, and the "server" host is the host which is
normally providing some service. As an alternate point of view,
Postel & Reynolds [Page 1]
RFC 854 May 1983
applicable even in terminal-to-terminal or process-to-process
communications, the "user" host is the host which initiated the
communication.
2. The principle of negotiated options takes cognizance of the fact
that many hosts will wish to provide additional services over and
above those available within an NVT, and many users will have
sophisticated terminals and would like to have elegant, rather than
minimal, services. Independent of, but structured within the TELNET
Protocol are various "options" that will be sanctioned and may be
used with the "DO, DON'T, WILL, WON'T" structure (discussed below) to
allow a user and server to agree to use a more elaborate (or perhaps
just different) set of conventions for their TELNET connection. Such
options could include changing the character set, the echo mode, etc.
The basic strategy for setting up the use of options is to have
either party (or both) initiate a request that some option take
effect. The other party may then either accept or reject the
request. If the request is accepted the option immediately takes
effect; if it is rejected the associated aspect of the connection
remains as specified for an NVT. Clearly, a party may always refuse
a request to enable, and must never refuse a request to disable some
option since all parties must be prepared to support the NVT.
The syntax of option negotiation has been set up so that if both
parties request an option simultaneously, each will see the other's
request as the positive acknowledgment of its own.
3. The symmetry of the negotiation syntax can potentially lead to
nonterminating acknowledgment loops -- each party seeing the incoming
commands not as acknowledgments but as new requests which must be
acknowledged. To prevent such loops, the following rules prevail:
a. Parties may only request a change in option status; i.e., a
party may not send out a "request" merely to announce what mode it
is in.
b. If a party receives what appears to be a request to enter some
mode it is already in, the request should not be acknowledged.
This non-response is essential to prevent endless loops in the
negotiation. It is required that a response be sent to requests
for a change of mode -- even if the mode is not changed.
c. Whenever one party sends an option command to a second party,
whether as a request or an acknowledgment, and use of the option
will have any effect on the processing of the data being sent from
the first party to the second, then the command must be inserted
in the data stream at the point where it is desired that it take
Postel & Reynolds [Page 2]
RFC 854 May 1983
effect. (It should be noted that some time will elapse between
the transmission of a request and the receipt of an
acknowledgment, which may be negative. Thus, a host may wish to
buffer data, after requesting an option, until it learns whether
the request is accepted or rejected, in order to hide the
"uncertainty period" from the user.)
Option requests are likely to flurry back and forth when a TELNET
connection is first established, as each party attempts to get the
best possible service from the other party. Beyond that, however,
options can be used to dynamically modify the characteristics of the
connection to suit changing local conditions. For example, the NVT,
as will be explained later, uses a transmission discipline well
suited to the many "line at a time" applications such as BASIC, but
poorly suited to the many "character at a time" applications such as
NLS. A server might elect to devote the extra processor overhead
required for a "character at a time" discipline when it was suitable
for the local process and would negotiate an appropriate option.
However, rather than then being permanently burdened with the extra
processing overhead, it could switch (i.e., negotiate) back to NVT
when the detailed control was no longer necessary.
It is possible for requests initiated by processes to stimulate a
nonterminating request loop if the process responds to a rejection by
merely re-requesting the option. To prevent such loops from
occurring, rejected requests should not be repeated until something
changes. Operationally, this can mean the process is running a
different program, or the user has given another command, or whatever
makes sense in the context of the given process and the given option.
A good rule of thumb is that a re-request should only occur as a
result of subsequent information from the other end of the connection
or when demanded by local human intervention.
Option designers should not feel constrained by the somewhat limited
syntax available for option negotiation. The intent of the simple
syntax is to make it easy to have options -- since it is
correspondingly easy to profess ignorance about them. If some
particular option requires a richer negotiation structure than
possible within "DO, DON'T, WILL, WON'T", the proper tack is to use
"DO, DON'T, WILL, WON'T" to establish that both parties understand
the option, and once this is accomplished a more exotic syntax can be
used freely. For example, a party might send a request to alter
(establish) line length. If it is accepted, then a different syntax
can be used for actually negotiating the line length -- such a
"sub-negotiation" might include fields for minimum allowable, maximum
allowable and desired line lengths. The important concept is that
Postel & Reynolds [Page 3]
RFC 854 May 1983
such expanded negotiations should never begin until some prior
(standard) negotiation has established that both parties are capable
of parsing the expanded syntax.
In summary, WILL XXX is sent, by either party, to indicate that
party's desire (offer) to begin performing option XXX, DO XXX and
DON'T XXX being its positive and negative acknowledgments; similarly,
DO XXX is sent to indicate a desire (request) that the other party
(i.e., the recipient of the DO) begin performing option XXX, WILL XXX
and WON'T XXX being the positive and negative acknowledgments. Since
the NVT is what is left when no options are enabled, the DON'T and
WON'T responses are guaranteed to leave the connection in a state
which both ends can handle. Thus, all hosts may implement their
TELNET processes to be totally unaware of options that are not
supported, simply returning a rejection to (i.e., refusing) any
option request that cannot be understood.
As much as possible, the TELNET protocol has been made server-user
symmetrical so that it easily and naturally covers the user-user
(linking) and server-server (cooperating processes) cases. It is
hoped, but not absolutely required, that options will further this
intent. In any case, it is explicitly acknowledged that symmetry is
an operating principle rather than an ironclad rule.
A companion document, "TELNET Option Specifications," should be
consulted for information about the procedure for establishing new
options.
THE NETWORK VIRTUAL TERMINAL
The Network Virtual Terminal (NVT) is a bi-directional character
device. The NVT has a printer and a keyboard. The printer responds
to incoming data and the keyboard produces outgoing data which is
sent over the TELNET connection and, if "echoes" are desired, to the
NVT's printer as well. "Echoes" will not be expected to traverse the
network (although options exist to enable a "remote" echoing mode of
operation, no host is required to implement this option). The code
set is seven-bit USASCII in an eight-bit field, except as modified
herein. Any code conversion and timing considerations are local
problems and do not affect the NVT.
TRANSMISSION OF DATA
Although a TELNET connection through the network is intrinsically
full duplex, the NVT is to be viewed as a half-duplex device
operating in a line-buffered mode. That is, unless and until
Postel & Reynolds [Page 4]
RFC 854 May 1983
options are negotiated to the contrary, the following default
conditions pertain to the transmission of data over the TELNET
connection:
1) Insofar as the availability of local buffer space permits,
data should be accumulated in the host where it is generated
until a complete line of data is ready for transmission, or
until some locally-defined explicit signal to transmit occurs.
This signal could be generated either by a process or by a
human user.
The motivation for this rule is the high cost, to some hosts,
of processing network input interrupts, coupled with the
default NVT specification that "echoes" do not traverse the
network. Thus, it is reasonable to buffer some amount of data
at its source. Many systems take some processing action at the
end of each input line (even line printers or card punches
frequently tend to work this way), so the transmission should
be triggered at the end of a line. On the other hand, a user
or process may sometimes find it necessary or desirable to
provide data which does not terminate at the end of a line;
therefore implementers are cautioned to provide methods of
locally signaling that all buffered data should be transmitted
immediately.
2) When a process has completed sending data to an NVT printer
and has no queued input from the NVT keyboard for further
processing (i.e., when a process at one end of a TELNET
connection cannot proceed without input from the other end),
the process must transmit the TELNET Go Ahead (GA) command.
This rule is not intended to require that the TELNET GA command
be sent from a terminal at the end of each line, since server
hosts do not normally require a special signal (in addition to
end-of-line or other locally-defined characters) in order to
commence processing. Rather, the TELNET GA is designed to help
a user's local host operate a physically half duplex terminal
which has a "lockable" keyboard such as the IBM 2741. A
description of this type of terminal may help to explain the
proper use of the GA command.
The terminal-computer connection is always under control of
either the user or the computer. Neither can unilaterally
seize control from the other; rather the controlling end must
relinguish its control explicitly. At the terminal end, the
hardware is constructed so as to relinquish control each time
that a "line" is terminated (i.e., when the "New Line" key is
typed by the user). When this occurs, the attached (local)
Postel & Reynolds [Page 5]
RFC 854 May 1983
computer processes the input data, decides if output should be
generated, and if not returns control to the terminal. If
output should be generated, control is retained by the computer
until all output has been transmitted.
The difficulties of using this type of terminal through the
network should be obvious. The "local" computer is no longer
able to decide whether to retain control after seeing an
end-of-line signal or not; this decision can only be made by
the "remote" computer which is processing the data. Therefore,
the TELNET GA command provides a mechanism whereby the "remote"
(server) computer can signal the "local" (user) computer that
it is time to pass control to the user of the terminal. It
should be transmitted at those times, and only at those times,
when the user should be given control of the terminal. Note
that premature transmission of the GA command may result in the
blocking of output, since the user is likely to assume that the
transmitting system has paused, and therefore he will fail to
turn the line around manually.
The foregoing, of course, does not apply to the user-to-server
direction of communication. In this direction, GAs may be sent at
any time, but need not ever be sent. Also, if the TELNET
connection is being used for process-to-process communication, GAs
need not be sent in either direction. Finally, for
terminal-to-terminal communication, GAs may be required in
neither, one, or both directions. If a host plans to support
terminal-to-terminal communication it is suggested that the host
provide the user with a means of manually signaling that it is
time for a GA to be sent over the TELNET connection; this,
however, is not a requirement on the implementer of a TELNET
process.
Note that the symmetry of the TELNET model requires that there is
an NVT at each end of the TELNET connection, at least
conceptually.
STANDARD REPRESENTATION OF CONTROL FUNCTIONS
As stated in the Introduction to this document, the primary goal
of the TELNET protocol is the provision of a standard interfacing
of terminal devices and terminal-oriented processes through the
network. Early experiences with this type of interconnection have
shown that certain functions are implemented by most servers, but
that the methods of invoking these functions differ widely. For a
human user who interacts with several server systems, these
differences are highly frustrating. TELNET, therefore, defines a
standard representation for five of these functions, as described
Postel & Reynolds [Page 6]
RFC 854 May 1983
below. These standard representations have standard, but not
required, meanings (with the exception that the Interrupt Process
(IP) function may be required by other protocols which use
TELNET); that is, a system which does not provide the function to
local users need not provide it to network users and may treat the
standard representation for the function as a No-operation. On
the other hand, a system which does provide the function to a
local user is obliged to provide the same function to a network
user who transmits the standard representation for the function.
Interrupt Process (IP)
Many systems provide a function which suspends, interrupts,
aborts, or terminates the operation of a user process. This
function is frequently used when a user believes his process is
in an unending loop, or when an unwanted process has been
inadvertently activated. IP is the standard representation for
invoking this function. It should be noted by implementers
that IP may be required by other protocols which use TELNET,
and therefore should be implemented if these other protocols
are to be supported.
Abort Output (AO)
Many systems provide a function which allows a process, which
is generating output, to run to completion (or to reach the
same stopping point it would reach if running to completion)
but without sending the output to the user's terminal.
Further, this function typically clears any output already
produced but not yet actually printed (or displayed) on the
user's terminal. AO is the standard representation for
invoking this function. For example, some subsystem might
normally accept a user's command, send a long text string to
the user's terminal in response, and finally signal readiness
to accept the next command by sending a "prompt" character
(preceded by <CR><LF>) to the user's terminal. If the AO were
received during the transmission of the text string, a
reasonable implementation would be to suppress the remainder of
the text string, but transmit the prompt character and the
preceding <CR><LF>. (This is possibly in distinction to the
action which might be taken if an IP were received; the IP
might cause suppression of the text string and an exit from the
subsystem.)
It should be noted, by server systems which provide this
function, that there may be buffers external to the system (in
Postel & Reynolds [Page 7]
RFC 854 May 1983
the network and the user's local host) which should be cleared;
the appropriate way to do this is to transmit the "Synch"
signal (described below) to the user system.
Are You There (AYT)
Many systems provide a function which provides the user with
some visible (e.g., printable) evidence that the system is
still up and running. This function may be invoked by the user
when the system is unexpectedly "silent" for a long time,
because of the unanticipated (by the user) length of a
computation, an unusually heavy system load, etc. AYT is the
standard representation for invoking this function.
Erase Character (EC)
Many systems provide a function which deletes the last
preceding undeleted character or "print position"* from the
stream of data being supplied by the user. This function is
typically used to edit keyboard input when typing mistakes are
made. EC is the standard representation for invoking this
function.
*NOTE: A "print position" may contain several characters
which are the result of overstrikes, or of sequences such as
<char1> BS <char2>...
Erase Line (EL)
Many systems provide a function which deletes all the data in
the current "line" of input. This function is typically used
to edit keyboard input. EL is the standard representation for
invoking this function.
THE TELNET "SYNCH" SIGNAL
Most time-sharing systems provide mechanisms which allow a
terminal user to regain control of a "runaway" process; the IP and
AO functions described above are examples of these mechanisms.
Such systems, when used locally, have access to all of the signals
supplied by the user, whether these are normal characters or
special "out of band" signals such as those supplied by the
teletype "BREAK" key or the IBM 2741 "ATTN" key. This is not
necessarily true when terminals are connected to the system
through the network; the network's flow control mechanisms may
cause such a signal to be buffered elsewhere, for example in the
user's host.
Postel & Reynolds [Page 8]
RFC 854 May 1983
To counter this problem, the TELNET "Synch" mechanism is
introduced. A Synch signal consists of a TCP Urgent notification,
coupled with the TELNET command DATA MARK. The Urgent
notification, which is not subject to the flow control pertaining
to the TELNET connection, is used to invoke special handling of
the data stream by the process which receives it. In this mode,
the data stream is immediately scanned for "interesting" signals
as defined below, discarding intervening data. The TELNET command
DATA MARK (DM) is the synchronizing mark in the data stream which
indicates that any special signal has already occurred and the
recipient can return to normal processing of the data stream.
The Synch is sent via the TCP send operation with the Urgent
flag set and the DM as the last (or only) data octet.
When several Synchs are sent in rapid succession, the Urgent
notifications may be merged. It is not possible to count Urgents
since the number received will be less than or equal the number
sent. When in normal mode, a DM is a no operation; when in urgent
mode, it signals the end of the urgent processing.
If TCP indicates the end of Urgent data before the DM is found,
TELNET should continue the special handling of the data stream
until the DM is found.
If TCP indicates more Urgent data after the DM is found, it can
only be because of a subsequent Synch. TELNET should continue
the special handling of the data stream until another DM is
found.
"Interesting" signals are defined to be: the TELNET standard
representations of IP, AO, and AYT (but not EC or EL); the local
analogs of these standard representations (if any); all other
TELNET commands; other site-defined signals which can be acted on
without delaying the scan of the data stream.
Since one effect of the SYNCH mechanism is the discarding of
essentially all characters (except TELNET commands) between the
sender of the Synch and its recipient, this mechanism is specified
as the standard way to clear the data path when that is desired.
For example, if a user at a terminal causes an AO to be
transmitted, the server which receives the AO (if it provides that
function at all) should return a Synch to the user.
Finally, just as the TCP Urgent notification is needed at the
TELNET level as an out-of-band signal, so other protocols which
make use of TELNET may require a TELNET command which can be
viewed as an out-of-band signal at a different level.
Postel & Reynolds [Page 9]
RFC 854 May 1983
By convention the sequence [IP, Synch] is to be used as such a
signal. For example, suppose that some other protocol, which uses
TELNET, defines the character string STOP analogously to the
TELNET command AO. Imagine that a user of this protocol wishes a
server to process the STOP string, but the connection is blocked
because the server is processing other commands. The user should
instruct his system to:
1. Send the TELNET IP character;
2. Send the TELNET SYNC sequence, that is:
Send the Data Mark (DM) as the only character
in a TCP urgent mode send operation.
3. Send the character string STOP; and
4. Send the other protocol's analog of the TELNET DM, if any.
The user (or process acting on his behalf) must transmit the
TELNET SYNCH sequence of step 2 above to ensure that the TELNET IP
gets through to the server's TELNET interpreter.
The Urgent should wake up the TELNET process; the IP should
wake up the next higher level process.
THE NVT PRINTER AND KEYBOARD
The NVT printer has an unspecified carriage width and page length
and can produce representations of all 95 USASCII graphics (codes
32 through 126). Of the 33 USASCII control codes (0 through 31
and 127), and the 128 uncovered codes (128 through 255), the
following have specified meaning to the NVT printer:
NAME CODE MEANING
NULL (NUL) 0 No Operation
Line Feed (LF) 10 Moves the printer to the
next print line, keeping the
same horizontal position.
Carriage Return (CR) 13 Moves the printer to the left
margin of the current line.
Postel & Reynolds [Page 10]
RFC 854 May 1983
In addition, the following codes shall have defined, but not
required, effects on the NVT printer. Neither end of a TELNET
connection may assume that the other party will take, or will
have taken, any particular action upon receipt or transmission
of these:
BELL (BEL) 7 Produces an audible or
visible signal (which does
NOT move the print head).
Back Space (BS) 8 Moves the print head one
character position towards
the left margin.
Horizontal Tab (HT) 9 Moves the printer to the
next horizontal tab stop.
It remains unspecified how
either party determines or
establishes where such tab
stops are located.
Vertical Tab (VT) 11 Moves the printer to the
next vertical tab stop. It
remains unspecified how
either party determines or
establishes where such tab
stops are located.
Form Feed (FF) 12 Moves the printer to the top
of the next page, keeping
the same horizontal position.
All remaining codes do not cause the NVT printer to take any
action.
The sequence "CR LF", as defined, will cause the NVT to be
positioned at the left margin of the next print line (as would,
for example, the sequence "LF CR"). However, many systems and
terminals do not treat CR and LF independently, and will have to
go to some effort to simulate their effect. (For example, some
terminals do not have a CR independent of the LF, but on such
terminals it may be possible to simulate a CR by backspacing.)
Therefore, the sequence "CR LF" must be treated as a single "new
line" character and used whenever their combined action is
intended; the sequence "CR NUL" must be used where a carriage
return alone is actually desired; and the CR character must be
avoided in other contexts. This rule gives assurance to systems
which must decide whether to perform a "new line" function or a
multiple-backspace that the TELNET stream contains a character
following a CR that will allow a rational decision.
Note that "CR LF" or "CR NUL" is required in both directions
Postel & Reynolds [Page 11]
RFC 854 May 1983
(in the default ASCII mode), to preserve the symmetry of the
NVT model. Even though it may be known in some situations
(e.g., with remote echo and suppress go ahead options in
effect) that characters are not being sent to an actual
printer, nonetheless, for the sake of consistency, the protocol
requires that a NUL be inserted following a CR not followed by
a LF in the data stream. The converse of this is that a NUL
received in the data stream after a CR (in the absence of
options negotiations which explicitly specify otherwise) should
be stripped out prior to applying the NVT to local character
set mapping.
The NVT keyboard has keys, or key combinations, or key sequences,
for generating all 128 USASCII codes. Note that although many
have no effect on the NVT printer, the NVT keyboard is capable of
generating them.
In addition to these codes, the NVT keyboard shall be capable of
generating the following additional codes which, except as noted,
have defined, but not reguired, meanings. The actual code
assignments for these "characters" are in the TELNET Command
section, because they are viewed as being, in some sense, generic
and should be available even when the data stream is interpreted
as being some other character set.
Synch
This key allows the user to clear his data path to the other
party. The activation of this key causes a DM (see command
section) to be sent in the data stream and a TCP Urgent
notification is associated with it. The pair DM-Urgent is to
have required meaning as defined previously.
Break (BRK)
This code is provided because it is a signal outside the
USASCII set which is currently given local meaning within many
systems. It is intended to indicate that the Break Key or the
Attention Key was hit. Note, however, that this is intended to
provide a 129th code for systems which require it, not as a
synonym for the IP standard representation.
Interrupt Process (IP)
Suspend, interrupt, abort or terminate the process to which the
NVT is connected. Also, part of the out-of-band signal for
other protocols which use TELNET.
Postel & Reynolds [Page 12]
RFC 854 May 1983
Abort Output (AO)
Allow the current process to (appear to) run to completion, but
do not send its output to the user. Also, send a Synch to the
user.
Are You There (AYT)
Send back to the NVT some visible (i.e., printable) evidence
that the AYT was received.
Erase Character (EC)
The recipient should delete the last preceding undeleted
character or "print position" from the data stream.
Erase Line (EL)
The recipient should delete characters from the data stream
back to, but not including, the last "CR LF" sequence sent over
the TELNET connection.
The spirit of these "extra" keys, and also the printer format
effectors, is that they should represent a natural extension of
the mapping that already must be done from "NVT" into "local".
Just as the NVT data byte 68 (104 octal) should be mapped into
whatever the local code for "uppercase D" is, so the EC character
should be mapped into whatever the local "Erase Character"
function is. Further, just as the mapping for 124 (174 octal) is
somewhat arbitrary in an environment that has no "vertical bar"
character, the EL character may have a somewhat arbitrary mapping
(or none at all) if there is no local "Erase Line" facility.
Similarly for format effectors: if the terminal actually does
have a "Vertical Tab", then the mapping for VT is obvious, and
only when the terminal does not have a vertical tab should the
effect of VT be unpredictable.
TELNET COMMAND STRUCTURE
All TELNET commands consist of at least a two byte sequence: the
"Interpret as Command" (IAC) escape character followed by the code
for the command. The commands dealing with option negotiation are
three byte sequences, the third byte being the code for the option
referenced. This format was chosen so that as more comprehensive use
of the "data space" is made -- by negotiations from the basic NVT, of
course -- collisions of data bytes with reserved command values will
be minimized, all such collisions requiring the inconvenience, and
Postel & Reynolds [Page 13]
RFC 854 May 1983
inefficiency, of "escaping" the data bytes into the stream. With the
current set-up, only the IAC need be doubled to be sent as data, and
the other 255 codes may be passed transparently.
The following are the defined TELNET commands. Note that these codes
and code sequences have the indicated meaning only when immediately
preceded by an IAC.
NAME CODE MEANING
SE 240 End of subnegotiation parameters.
NOP 241 No operation.
Data Mark 242 The data stream portion of a Synch.
This should always be accompanied
by a TCP Urgent notification.
Break 243 NVT character BRK.
Interrupt Process 244 The function IP.
Abort output 245 The function AO.
Are You There 246 The function AYT.
Erase character 247 The function EC.
Erase Line 248 The function EL.
Go ahead 249 The GA signal.
SB 250 Indicates that what follows is
subnegotiation of the indicated
option.
WILL (option code) 251 Indicates the desire to begin
performing, or confirmation that
you are now performing, the
indicated option.
WON'T (option code) 252 Indicates the refusal to perform,
or continue performing, the
indicated option.
DO (option code) 253 Indicates the request that the
other party perform, or
confirmation that you are expecting
the other party to perform, the
indicated option.
DON'T (option code) 254 Indicates the demand that the
other party stop performing,
or confirmation that you are no
longer expecting the other party
to perform, the indicated option.
IAC 255 Data Byte 255.
Postel & Reynolds [Page 14]
RFC 854 May 1983
CONNECTION ESTABLISHMENT
The TELNET TCP connection is established between the user's port U
and the server's port L. The server listens on its well known port L
for such connections. Since a TCP connection is full duplex and
identified by the pair of ports, the server can engage in many
simultaneous connections involving its port L and different user
ports U.
Port Assignment
When used for remote user access to service hosts (i.e., remote
terminal access) this protocol is assigned server port 23
(27 octal). That is L=23.
Postel & Reynolds [Page 15]

48
src/bin/main.rs Normal file
View File

@@ -0,0 +1,48 @@
use std::io::{Error, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use telnet_server::read::Read;
use telnet_server::telnet::{Session, State, StateConfig};
const BIND_ADDRESS: &str = "127.0.0.1:9000";
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind(BIND_ADDRESS)?;
for stream in listener.incoming() {
thread::spawn(move || {
if let Ok(stream) = stream {
let _ = handle_connection(stream);
};
});
}
Ok(())
}
fn handle_connection(tcp_stream: TcpStream) -> Result<(), Error> {
// Set up State and Session
let state_config = StateConfig::default();
let state = State::new(&state_config);
let mut session = Session::new(state, tcp_stream)?;
// Make the Session listen to incoming TCP data in the background
let session_listen = session.clone();
let handle = thread::spawn(move || session_listen.listen());
loop {
// Handle incoming TELNET messages:
let incoming = session.read_line_waiting()?;
let answer = format!("You sent: {incoming}");
if session.write_all(answer.as_bytes()).is_err() {
break;
}
if session.flush().is_err() {
break;
}
}
handle.join().expect("Should await thread")
}

84
src/iter.rs Normal file
View File

@@ -0,0 +1,84 @@
/// Returns whether the sequence `needle` is a part of `haystack`, regardless of
/// its position.
///
/// # Arguments
///
/// * `haystack` - The sequence to look for `needle`
/// * `needle` - The sequence that may be a part of `haystack`
///
/// # Examples
///
/// ```ignore
/// use telnet_server::iter::contains_sequence;
/// // [1, 2, 3] is included in [1, 2, 3, 4, 5] -> returns true
/// assert!(contains_sequence(&[1, 2, 3, 4, 5], &[1, 2, 3]));
/// // [3, 3, 3] is *NOT* included in [1, 2, 3, 4, 5] -> returns false
/// assert!(!contains_sequence(&[1, 2, 3, 4, 5], &[3, 3, 3]));
/// ```
pub fn contains_sequence<T: Eq>(haystack: &[T], needle: &[T]) -> bool {
let haystack_len = haystack.len();
let needle_len = needle.len();
if needle_len > haystack_len {
return false;
}
if haystack_len == 0 {
/* this would be an incorrect edge case otherwise */
return false;
}
let size_diff = haystack_len - needle_len;
'outer: for haystack_start in 0..=size_diff {
for needle_index in 0..needle_len {
if haystack[needle_index + haystack_start] != needle[needle_index] {
continue 'outer;
}
}
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_match_start() {
assert!(contains_sequence(&[1, 2, 3, 4, 5], &[1, 2, 3]));
}
#[test]
fn finds_match_middle() {
assert!(contains_sequence(&[1, 2, 3, 4, 5], &[2, 3, 4]));
}
#[test]
fn finds_match_end() {
assert!(contains_sequence(&[1, 2, 3, 4, 5], &[3, 4, 5]));
}
#[test]
fn finds_no_match() {
assert!(!contains_sequence(&[1, 2, 3, 4, 5], &[3, 3, 3]));
}
#[test]
fn finds_match_empty() {
assert!(contains_sequence(&[1, 2, 3, 4, 5], &[]));
}
#[test]
fn finds_no_match_on_empty_haystack() {
assert!(!contains_sequence(&[], &[1]));
}
#[test]
fn finds_no_match_on_empty_haystack_and_needle() {
assert!(!contains_sequence::<u8>(&[], &[]));
}
}

4
src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub(crate) mod iter;
pub mod read;
pub mod telnet;

36
src/read.rs Normal file
View File

@@ -0,0 +1,36 @@
//! Module containing resources for reading data.
//! The reason for this module to exist is "missing" - but mandatory -
//! functionality in the [`std::io::Read`] trait.
use std::io::Error;
/// Trait for extending [`std::io::Read`] to add "missing" functionality
pub trait Read {
/// Reads line to a [`String`], ensuring it is never empty.
/// Spins until a `\n` has been found, so that - even if the buffer is empty
/// at some point - the result is always a non-empty line.
///
/// # Returns
///
/// * `Ok(String)` with a non-empty string, ending with `\n`
/// * `Err(std::io::Error)` if reading fails
///
/// # Examples
///
/// ```ignore
/// use telnet_server::telnet::{Session, State, StateConfig};
/// use crate::telnet_server::read::Read;
///
/// let mut session = Session::new(State::new(&StateConfig::default()), tcp_stream)?;
///
/// // set up session to receive data...
///
/// let incoming = session.read_line_waiting()?;
///
/// assert!(incoming.len() > 0);
/// assert!(incoming.ends_with('\n'));
///
/// Ok(())
/// ```
fn read_line_waiting(&mut self) -> Result<String, Error>;
}

14
src/telnet/mod.rs Normal file
View File

@@ -0,0 +1,14 @@
//! TELNET module
//!
//! This module contains state and session handling for TCP connections to a
//! TELNET service.
//! It has two sub modules:
//! * [`Session`] handles the TCP connection
//! * [`State`] handles the internal (TELNET) state of an existing connection
//! * [`StateConfig`] can be used to configure the handling of the [`State`]
//! in specific cases.
pub mod session;
pub mod state;
pub use session::Session;
pub use state::{State, StateConfig};

150
src/telnet/session.rs Normal file
View File

@@ -0,0 +1,150 @@
use super::State;
use crate::read;
use std::{
io::{self, ErrorKind, Read, Result, Write},
net::TcpStream,
sync::{Arc, Mutex},
};
/// Handles the TCP connection for a TELNET service, allowing reading and
/// writing access while also handling the internal TELNET state.
///
/// Implements [`std::io::Read`] and [`std::io::Write`] to receive and send
/// messages from/to the connection.
#[derive(Clone)]
pub struct Session {
/// Reference to a TELNET connection [`State`]
state: Arc<Mutex<State>>,
/// Refence to the TCP connection
tcp_stream: Arc<Mutex<TcpStream>>,
}
impl Session {
/// Creates new [`Session`] based on given [`TcpStream`] and a fresh
/// [`State`].
/// Also ensures that the TCP stream is non-blocking as otherwise the
/// session becomes unusable.
///
/// # Arguments
///
/// * `state` - A fresh [`State`]
/// * `tcp_stream` - [`TcpStream`] for a TELNET based session. Notice that
/// there's no instant check if this is for TELNET or anthother protocol.
///
/// # Returns
///
/// * `Ok(Self)` on success
/// * `Err(std::io::Error)` if `tcp_stream` cannot be set to non-blocking
pub fn new(state: State, tcp_stream: TcpStream) -> Result<Self> {
tcp_stream.set_nonblocking(true)?;
Ok(Self {
state: Arc::new(Mutex::new(state)),
tcp_stream: Arc::new(Mutex::new(tcp_stream)),
})
}
/// Listens to and handles incoming TCP data.
/// Should be called in a background thread as it blocks. As the internal
/// TCP stream is set to non-blocking, reading and writing on a cloned
/// [`Session`] is still possible.
///
/// # Returns
///
/// Only returns an `Err(std::io::Error)` on TCP errors as it runs
/// indefinitely.
///
/// # Examples
///
/// ```ignore
/// use std::thread;
/// use telnet_server::telnet::{Session, State, StateConfig};
///
/// let mut session = Session::new(State::new(&StateConfig::default()), tcp_stream)?;
///
/// let session_listen = session.clone();
/// let handle = thread::spawn(move || session_listen.listen());
///
/// // Send and receive messages here...
///
/// handle.join().expect("Should await thread");
///
/// Ok(())
/// ```
pub fn listen(self) -> Result<()> {
let mut buf: [u8; 255] = [0; 255];
loop {
let mut tcp_stream = match self.tcp_stream.try_lock() {
Ok(t) => t,
Err(_) => continue,
};
let tcp_data = match tcp_stream.read(&mut buf) {
Ok(read_bytes) => &buf[..read_bytes],
Err(e) => {
if e.kind() == ErrorKind::WouldBlock {
continue;
}
return Err(e);
}
};
if let Some(telnet_data) = self
.state
.lock()
.expect("Should lock state")
.write(tcp_data)?
{
tcp_stream.write_all(&telnet_data)?;
tcp_stream.flush()?;
}
}
}
}
impl io::Write for Session {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.tcp_stream
.lock()
.expect("Should lock stream")
.write(buf)
}
fn flush(&mut self) -> Result<()> {
self.tcp_stream.lock().expect("Should lock stream").flush()
}
}
impl io::Read for Session {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.state.lock().expect("Should lock state").read(buf)
}
}
impl read::Read for Session {
fn read_line_waiting(&mut self) -> Result<String> {
let mut line = String::new();
let mut buf: [u8; 1] = [0];
loop {
match self.read(&mut buf) {
Ok(1) => {
let next = buf[0] as char;
line.push(next);
if next == '\n' {
break;
}
}
Ok(0) => {
continue;
}
Ok(_) => panic!("Out of range"),
Err(e) => return Err(e),
};
}
Ok(line)
}
}

471
src/telnet/state.rs Normal file
View File

@@ -0,0 +1,471 @@
use crate::iter::contains_sequence;
use std::{
cmp::min,
io::{Error, Read},
};
const ECHO: u8 = 1;
const ERASE_LINE: u8 = 248;
const BEL: u8 = 7;
const CHAR_BACK_SPACE: u8 = 8;
const CHAR_ESCAPE: u8 = 27;
const CHAR_DELETE: u8 = 127;
const CHAR_ERASE: u8 = 247;
const IAC: u8 = 255;
/// "IAC SB"
const IAC_SUBNEGOTIATION_START: u8 = 250;
/// "IAC SE"
const IAC_SUBNEGOTIATION_END: u8 = 240;
const IAC_WILL: u8 = 251;
const IAC_WONT: u8 = 252;
const IAC_DO: u8 = 253;
const IAC_DONT: u8 = 254;
/// Sequence for erasing current line in ANSI terminals
const ANSI_SEQUENCE_ERASE_LINE: [u8; 5] = [CHAR_ESCAPE, 91, 50, 75, 13];
/// Any known ending character of ANSI escape sequences
const CHARS_ESCAPE_SEQUENCE_END: [char; 20] = [
'A', /* CUU */
'B', /* CUD */
'C', /* CUF */
'D', /* CUB */
'E', /* CNL */
'F', /* CPL */
'G', /* CHA */
'H', /* CUP */
'J', /* ED */
'K', /* EL */
'S', /* SU */
'T', /* SD */
'f', /* HVP */
'm', /* SGR */
'i', /* AUX */
'n', /* DSR */
's', /* SCP, SCOSC */
'u', /* RCP, SCORC */
'h', /* DECTCEM */
'l', /* DECTCEM */
];
const CHARS_LINE_BREAK: [u8; 2] = [b'\r', b'\n'];
/// Type for read-only bytes
pub type Bytes = Box<[u8]>;
pub type BytesResult = Result<Option<Bytes>, Error>;
/// Struct that holds and handles the current state of a TELNET session.
/// Implements [`Read`] to get the handled readable, non-command data and a fake
/// `Write` to accept incoming TCP data.
///
/// # Notice
///
/// This struct is independent of any service related input or TCP at all. It is
/// just a bare state machine.
pub struct State {
/// Buffer of "readable" received data that can be obtained by using the
/// [`Read`] trait
output_buffer: Vec<u8>,
/// Current overall mode
mode: Mode,
/// Indicates whether every incoming, non-command char should be echoed back
/// to the connection
is_echoing: bool,
/// If true, ANSI escape sequences will be handled like normal non-command
/// input. Otherwise, sequences will be ignored and a BEL is sent back to
/// notice.
handle_ansi_escape_sequences: bool,
}
/// Configuration to set up a new [`State`]
#[derive(Default)]
pub struct StateConfig {
/// If true, ANSI escape sequences will be handled like normal non-command
/// input. Otherwise, sequences will be ignored and a BEL is sent back to
/// notice.
pub handle_ansi_escape_sequences: bool,
}
/// Enumeration of overall modes that a TELNET state may have
enum Mode {
/// Incoming, non-command data (e.g. text)
Idle,
/// Incoming command data (e.g. WILL, WONT, DO, DONT)
Command,
/// Incoming command data for WILL command
CommandWill,
/// Incoming command data for WONT command
CommandWont,
/// Incoming command data for DO command
CommandDo,
/// Incoming command data for DONT command
CommandDont,
/// Incoming command data for sub negotiation command
SubNegotiation,
/// Incoming escape sequence. This is not a "real" mode but we need it as
/// you can choose to ignore ANSI escape sequences because it doesn't really
/// make sense to evaluate these.
AnsiEscapeSequence,
}
impl State {
/// Creates a new [`State`] with given configuration
///
/// # Arguments
///
/// * `config` - [`StateConfig`] to set up the behaviour of the resutlting
/// [`State`]
///
/// # Returns
///
/// Fresh [`State`] for a TELNET session
///
/// # Examples
///
/// ```rust
/// use telnet_server::telnet::{StateConfig, State};
///
/// let config = StateConfig::default();
/// let state = State::new(&config);
/// ```
pub fn new(config: &StateConfig) -> Self {
State {
output_buffer: vec![],
mode: Mode::Idle,
is_echoing: false,
handle_ansi_escape_sequences: config.handle_ansi_escape_sequences,
}
}
/// Writes a buffer of incoming TELNET data into the state, returning an
/// immediate response to the other part. Consumes the whole buffer _unless_
/// there's an error (e.g. invalid data) which immediatelly results in an
/// [`Error`].
///
/// # Arguments
///
/// * `data` - Incoming TELNET data
///
/// # Returns
///
/// * `Err(std::io::Error)` if an error occurs. In this case the internal
/// TELNET state possibly mismatches the "real" state. This _should_ lead
/// to the termination of the TELNET session at all.
/// * `Ok(Some([u8]))` if data should be sent back
/// * `Ok(None)` if everythings fine
///
/// # Examples
///
/// ```rust
/// use telnet_server::telnet::{StateConfig, State};
///
/// let config = StateConfig::default();
/// let mut state = State::new(&config);
///
/// let data = [255, 253, 1];
/// let result = state.write(&data)?;
///
/// assert!(result.is_some());
/// let result = result.unwrap();
///
/// // write back result to TCP connection...
///
/// Ok::<(), std::io::Error>(())
/// ```
pub fn write(&mut self, buf: &[u8]) -> BytesResult {
let mut response: Vec<u8> = vec![];
for &next in buf {
let result = match self.mode {
Mode::Idle => self.next_on_idle(next),
Mode::Command => self.next_as_command(next),
Mode::CommandWill => self.next_as_will(next),
Mode::CommandWont => self.next_as_wont(next),
Mode::CommandDo => self.next_as_do(next),
Mode::CommandDont => self.next_as_dont(next),
Mode::SubNegotiation => self.next_as_sub_negotiation(next),
Mode::AnsiEscapeSequence => self.next_as_escape_sequence(next),
};
if let Ok(Some(v)) = result {
response.extend_from_slice(&v);
} else if result.is_err() {
return result;
}
}
if !response.is_empty() {
Ok(Some(response.into_boxed_slice()))
} else {
Ok(None)
}
}
/// Handles incoming `next` byte when [`State`] is in idle mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_on_idle(&mut self, next: u8) -> BytesResult {
match next {
IAC => self.mode = Mode::Command,
CHAR_DELETE | CHAR_BACK_SPACE | CHAR_ERASE => {
self.output_buffer.pop();
if self.is_echoing {
/* Return fake backspace on echo mode */
return Ok(Some(Box::new([CHAR_BACK_SPACE, b' ', CHAR_BACK_SPACE])));
}
}
ERASE_LINE => {
Self::erase_current_line(&mut self.output_buffer);
if self.is_echoing {
return Ok(Some(ANSI_SEQUENCE_ERASE_LINE.into()));
}
return Ok(None);
}
CHAR_ESCAPE => {
self.mode = Mode::AnsiEscapeSequence;
if self.is_echoing {
return Ok(Some(Box::new([next])));
}
if self.handle_ansi_escape_sequences {
self.output_buffer.push(next);
}
}
_ => {
self.output_buffer.push(next);
if self.is_echoing {
return Ok(Some(Box::new([next])));
}
}
}
Ok(None)
}
/// Handles incoming `next` byte when [`State`] is in IAC mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_command(&mut self, next: u8) -> BytesResult {
match next {
IAC_WILL => self.mode = Mode::CommandWill,
IAC_WONT => self.mode = Mode::CommandWont,
IAC_DO => self.mode = Mode::CommandDo,
IAC_DONT => self.mode = Mode::CommandDont,
IAC_SUBNEGOTIATION_START => self.mode = Mode::SubNegotiation,
_ => {
return Err(Error::new(
std::io::ErrorKind::InvalidInput,
format!("Unknown command '{next}'"),
))
}
};
Ok(None)
}
/// Handles incoming `next` byte when [`State`] is in IAC WILL mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_will(&mut self, _next: u8) -> BytesResult {
/* Ignore message, just go back to idle state */
self.mode = Mode::Idle;
Ok(None)
}
/// Handles incoming `next` byte when [`State`] is in IAC WONT mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_wont(&mut self, _next: u8) -> BytesResult {
/* Ignore message, just go back to idle state */
self.mode = Mode::Idle;
Ok(None)
}
/// Handles incoming `next` byte when [`State`] is in IAC DO mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_do(&mut self, next: u8) -> BytesResult {
self.mode = Mode::Idle;
if next == ECHO {
self.is_echoing = true;
return Ok(Some(Box::new([IAC, IAC_WILL, ECHO])));
}
/* Whatever they're asking for, we're not supporting it probably. */
Ok(Some(Box::new([IAC, IAC_WONT, next])))
}
/// Handles incoming `next` byte when [`State`] is in IAC DONT mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_dont(&mut self, next: u8) -> BytesResult {
self.mode = Mode::Idle;
if next == ECHO {
self.is_echoing = false;
}
/* Whatever they're asking for, we're not supporting it probably.
* So it's fine to say that we won't do it. */
Ok(Some(Box::new([IAC, IAC_WONT, next])))
}
/// Handles incoming `next` byte when [`State`] is in IAC SB mode
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_sub_negotiation(&mut self, next: u8) -> BytesResult {
/* We're NOT handling sub negotiations right now. */
if next == IAC_SUBNEGOTIATION_END {
self.mode = Mode::Idle;
}
Ok(None)
}
/// Handles incoming `next` byte when [`State`] is in idle mode and in an
/// ANSI escape sequence
///
/// # Returns
///
/// * `Ok(None)` - Everythings okay, no need to write something back
/// * `Ok(Some(Bytes))` - Everythings okay, something has to be written back
/// * `Err` - Data could not be interpreted
fn next_as_escape_sequence(&mut self, next: u8) -> BytesResult {
if self.handle_ansi_escape_sequences {
self.output_buffer.push(next);
if CHARS_ESCAPE_SEQUENCE_END.contains(&(next as char)) {
self.mode = Mode::Idle;
}
if self.is_echoing {
Ok(Some(Box::new([next])))
} else {
Ok(None)
}
} else {
if !CHARS_ESCAPE_SEQUENCE_END.contains(&(next as char)) {
return Ok(None);
}
self.mode = Mode::Idle;
Ok(Some(Box::new([BEL])))
}
}
/// Erases the current line from given text buffer. According to
/// [RFC-854](https://www.rfc-editor.org/rfc/rfc854#page-13), the last
/// CR LF should be kept.
///
/// # Arguments
///
/// * `buffer` - Text buffer that should be updated. All current line
/// characters will be removed from the [`Vec`].
///
/// # Examples
///
/// ```ignore
/// use telnet_server::telnet::State;
///
/// let mut buffer = vec![b'a', b'b', b'c', b'\r', b'\n', b'd', b'e', b'f'];
/// State::erase_current_line(&mut buffer);
/// assert_eq!(buffer, [b'a', b'b', b'c', b'\r', b'\n']);
///
/// State::erase_current_line(&mut buffer);
/// assert_eq!(buffer, [b'a', b'b', b'c', b'\r', b'\n']);
///
/// let mut buffer = vec![b'a', b'b', b'c', b'd', b'e', b'f'];
/// State::erase_current_line(&mut buffer);
/// assert!(buffer.is_empty());
/// ```
fn erase_current_line(buffer: &mut Vec<u8>) {
loop {
let buffer_len = buffer.len();
/* Remove all chars until \r\n reached */
if buffer_len < 2 {
buffer.clear();
break;
}
let start_index = buffer_len - 2;
if contains_sequence(&buffer[start_index..], &CHARS_LINE_BREAK) {
break;
}
buffer.pop();
}
}
}
impl Read for State {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let limit = min(buf.len(), self.output_buffer.len());
let (left, _) = buf.split_at_mut(limit);
left.copy_from_slice(&self.output_buffer[..limit]);
self.output_buffer.drain(..limit);
Ok(limit)
}
}
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn erase_current_line_should_work() {
let mut buffer = vec![b'a', b'b', b'c', b'\r', b'\n', b'd', b'e', b'f'];
State::erase_current_line(&mut buffer);
/* RFC-854: 'The recipient should delete characters from the data stream
* back to, but not including, the last "CR LF" sequence sent over the
* TELNET connection.' */
assert_eq!(buffer, [b'a', b'b', b'c', b'\r', b'\n']);
State::erase_current_line(&mut buffer);
assert_eq!(buffer, [b'a', b'b', b'c', b'\r', b'\n']);
let mut buffer = vec![b'a', b'b', b'c', b'd', b'e', b'f'];
State::erase_current_line(&mut buffer);
assert!(buffer.is_empty());
}
}

5
telnet/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM alpine
RUN apk update && apk add busybox-extras
ENTRYPOINT ["/usr/bin/telnet"]

7
telnet/run_telnet.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
if [ -z "$(docker images -q telnet_docker:latest 2> /dev/null)" ]; then
docker build --tag telnet_docker . --file "$(dirname "$0")/Dockerfile"
fi
docker run --rm -it telnet_docker:latest "$@"