Compare commits
No commits in common. "master" and "haskell" have entirely different histories.
|
@ -1,11 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
|
@ -1,126 +0,0 @@
|
|||
name: Rust
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
# Lint the code with rustfmt and clippy. All warnings are errors.
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust with rustfmt
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
components: rustfmt, clippy
|
||||
override: true
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: -- --check
|
||||
|
||||
- name: Lint with clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
|
||||
|
||||
# Make sure every combination of features and targets produces valid Rust.
|
||||
check:
|
||||
needs: [lint]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
# We should only check targets which we distinguish with config flags.
|
||||
# Currently, we don't distinguish by target at all,
|
||||
# so only one target needs to be enabled.
|
||||
# - "x86_64-apple-darwin"
|
||||
# - "x86_64-pc-windows-gnu"
|
||||
# - "x86_64-pc-windows-msvc"
|
||||
- "x86_64-unknown-linux-gnu"
|
||||
|
||||
features:
|
||||
- ""
|
||||
- "compression"
|
||||
- "encryption"
|
||||
- "authentication"
|
||||
- "compression,encryption"
|
||||
- "compression,authentication"
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --locked --target ${{ matrix.target }} --no-default-features --features "${{ matrix.features }}"
|
||||
|
||||
# Make sure the crate can be built natively on every platform.
|
||||
build:
|
||||
needs: [check]
|
||||
|
||||
# You should always specify the OS in the includes,
|
||||
# but if you do not provide a default, GitHub errors.
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- "x86_64-apple-darwin"
|
||||
- "x86_64-pc-windows-gnu"
|
||||
- "x86_64-pc-windows-msvc"
|
||||
- "x86_64-unknown-linux-gnu"
|
||||
|
||||
include:
|
||||
- target: "x86_64-apple-darwin"
|
||||
os: macos-latest
|
||||
# MacOS is experimental until this issue is resolved: https://github.com/rust-lang/rust/issues/71988
|
||||
experimental: true
|
||||
|
||||
- target: "x86_64-pc-windows-gnu"
|
||||
os: windows-latest
|
||||
|
||||
- target: "x86_64-pc-windows-msvc"
|
||||
os: windows-latest
|
||||
|
||||
- target: "x86_64-unknown-linux-gnu"
|
||||
os: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --locked --target ${{ matrix.target }} --all-features
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
/private
|
||||
.stack-work/
|
||||
tmd.cabal
|
||||
*~
|
||||
|
|
File diff suppressed because it is too large
Load Diff
53
Cargo.toml
53
Cargo.toml
|
@ -1,53 +0,0 @@
|
|||
[package]
|
||||
name = "tmd"
|
||||
version = "0.1.0"
|
||||
authors = ["James Martin <james@jtmar.me>"]
|
||||
edition = "2018"
|
||||
repository = "https://github.com/jamestmartin/tmd"
|
||||
license = "GPL-3.0+"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["authentication", "compression", "encryption"]
|
||||
|
||||
# Enables authentication via Mojang's servers, i.e. online mode.
|
||||
authentication = ["encryption", "num-bigint", "sha-1", "reqwest"]
|
||||
|
||||
# Enables protocol compression.
|
||||
compression = ["flate2"]
|
||||
|
||||
# Enables protocol encryption *without enabling authentication*.
|
||||
#
|
||||
# The protocol itself doesn't depend on authentication to use encryption.
|
||||
# If the client and server both simply *don't* check in with Mojang,
|
||||
# then the protocol will proceed as normal, but encrypted.
|
||||
# However, be warned that there is no way to disable authentication
|
||||
# for Notchian clients (they will disconnect you from the server),
|
||||
# so a modified client would be necessary
|
||||
# to use offline encryption, and none exist that I know of.
|
||||
encryption = ["aes", "cfb8", "rand", "rsa"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.42"
|
||||
clap = { version = "2.33.3", features = ["yaml"] }
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_json = "1.0.61"
|
||||
take_mut = "0.2.2"
|
||||
tokio = { version = "1.0.1", features = ["io-util", "macros", "net", "rt", "rt-multi-thread"] }
|
||||
uuid = { version = "0.8.1", features = ["serde"] }
|
||||
|
||||
# Dependencies required for authentication
|
||||
# Used to format Minecraft's "server id" hash.
|
||||
num-bigint = { version = "0.3.1", optional = true }
|
||||
sha-1 = { version = "0.9.2", optional = true }
|
||||
# Used to make the request to the Mojang servers.
|
||||
reqwest = { version = "0.11.0", optional = true }
|
||||
|
||||
# Dependencies required for compression
|
||||
flate2 = { version = "1.0.19", optional = true }
|
||||
|
||||
# Dependencies required for encryption
|
||||
aes = { version = "0.6.0", optional = true }
|
||||
cfb8 = { version = "0.6.0", optional = true }
|
||||
rand = { version = "0.8.1", optional = true }
|
||||
rsa = { version = "0.3.0", optional = true }
|
|
@ -1,23 +1,21 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
|
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
@ -72,7 +60,7 @@ modification follow.
|
|||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
|
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
55
README.md
55
README.md
|
@ -1,46 +1,21 @@
|
|||
# tmd
|
||||
A Minecraft protocol-compatible server written from scratch in Rust.
|
||||
A Minecraft server implementation in Haskell.
|
||||
|
||||
Supports Minecraft version 1.16.1 (protocol version 736),
|
||||
including protocol compression, encryption, and authentication.
|
||||
"tmd" doesn't stand for anything in particular.
|
||||
It's just an homage to the Minecraft server I used to run.
|
||||
|
||||
Most of the protocol infrastructure is there, so complete protocol support
|
||||
is mostly a matter of defining Play state packets with my handy macros.
|
||||
The protocol infrastructure is actually written bi-directionally
|
||||
and in theory should work as a protocol client as well,
|
||||
although that is not currently actively persued or tested.
|
||||
## Project Status
|
||||
I'm not currently working on this project because I got bored of Minecraft (again).
|
||||
However, I'm leaving it up because some of it could still be valuable if I try to make a Minecraft server again in the future.
|
||||
I will be archiving the repository in the mean time.
|
||||
|
||||
## Features
|
||||
### Compression
|
||||
The server will use protocol compression by default.
|
||||
You can disable it by disabling the crate's `compression` feature,
|
||||
although disabling it would probably be pointless.
|
||||
Please do not judge me based on this code.
|
||||
It was an experiment with dependently-typed Haskell; I would not write real-world code this way.
|
||||
It's not even *good* dependently-typed Haskell, and I acknowledge that.
|
||||
|
||||
The compression threshold is 64 bytes.
|
||||
Also, the current master commit is broken. The most recent working commit was `b000b6c`.
|
||||
|
||||
### Encryption
|
||||
The server will also use protocol encryption by default.
|
||||
You can disable it by disabling the crate's `encryption` feature.
|
||||
|
||||
To use encryption, you will need to a 1024-bit RSA keypair
|
||||
and store it in DER format as `private/pub.der` and `private/priv.der`.
|
||||
|
||||
You can do that with OpenSSL using these comamnds:
|
||||
|
||||
```
|
||||
openssl genpkey -algorithm RSA -aes256 -pkeyopt rsa_keygen_bits:1024 -outform DER -out priv.der
|
||||
openssl rsa -pubout -inform DER -in priv.der -outform DER -out pub.der
|
||||
```
|
||||
|
||||
### Authentication
|
||||
The server will run in 'online mode' by default,
|
||||
which means only players authenticated with Mojang can connect.
|
||||
You can disable it by disabling the crate's `authentication` feature.
|
||||
This feature also depends on the `encryption` feature.
|
||||
|
||||
**If you wish to set your server to 'offline mode', you will have to disable encryption as well.**
|
||||
Although disabling authentication means that the server will allow unauthenticated clients to connect,
|
||||
the official client will disconnect from an encrypted server if it fails to authenticate with Mojang.
|
||||
To be able to connect to a server with authentication off and encryption on,
|
||||
you would either need to be logged in (which defeats the point) or use a modified client.
|
||||
To my knowedge, no such modified client currently exists.
|
||||
## Current Features
|
||||
* Supports protocol version 498 (Minecraft 1.14.4).
|
||||
* Responds to status requests and pings by modern Minecraft clients.
|
||||
* Accepts login handshakes, although the server immediately responds with a Disconnect packet and closes the connection.
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
module Main where
|
||||
|
||||
import Control.Category.Free
|
||||
import Control.Concurrent (forkFinally)
|
||||
import Control.Exception (bracket)
|
||||
import Control.Monad (forever, void)
|
||||
import Control.Monad.Indexed
|
||||
import Control.Monad.State (MonadIO)
|
||||
import Data.Exists
|
||||
import qualified Data.Vector as V
|
||||
import Minecraft.Protocol
|
||||
import Minecraft.Protocol.ByteReader
|
||||
import Minecraft.Protocol.DataTypes
|
||||
import Minecraft.Protocol.Direction
|
||||
import Minecraft.Protocol.State
|
||||
import Minecraft.Protocol.State.Handshake
|
||||
import Minecraft.Protocol.State.Login
|
||||
import Minecraft.Protocol.State.Status
|
||||
import Minecraft.Protocol.Version
|
||||
import Network.Socket hiding (recv, Closed)
|
||||
|
||||
main :: IO ()
|
||||
main = withSocketsDo $ do
|
||||
addr <- resolve "25565"
|
||||
bracket (open addr) close loop
|
||||
where resolve port = do
|
||||
let hints = defaultHints {
|
||||
addrFlags = [ AI_PASSIVE, AI_NUMERICSERV ]
|
||||
, addrSocketType = Stream
|
||||
}
|
||||
addr:_ <- getAddrInfo (Just hints) Nothing (Just port)
|
||||
return addr
|
||||
open addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
setSocketOption sock ReuseAddr 1
|
||||
-- TODO: use withFdSocket when we hit network 3.1
|
||||
setCloseOnExecIfNeeded $ fdSocket sock
|
||||
bind sock (addrAddress addr)
|
||||
listen sock 10
|
||||
return sock
|
||||
loop sock = forever $ do
|
||||
(conn, peer) <- accept sock
|
||||
putStrLn $ "Connection from " ++ show peer
|
||||
void $ forkFinally (readBytes conn talk) (\_ -> close conn)
|
||||
|
||||
data PacketIOF (st :: ProtocolState) (st' :: ProtocolState) a where
|
||||
ReadPacket :: SProtocolStateI st => PacketIOF st st (Packet 'Serverbound st)
|
||||
WritePacket :: Packet 'Clientbound st -> PacketIOF st st ()
|
||||
Transition :: ProtocolStateTransition st st' -> PacketIOF st st' ()
|
||||
|
||||
type PacketIO = Program PacketIOF
|
||||
|
||||
runPacketIO :: forall r a. (MonadIO r, ByteReader r) => PacketIO 'Handshake 'Closed a -> r a
|
||||
runPacketIO = runProgram runPacketIO'
|
||||
where runPacketIO' :: forall st st' a. PacketIOF st st' a -> r a
|
||||
runPacketIO' ReadPacket = nextPacket :: r (Packet 'Serverbound st)
|
||||
runPacketIO' (WritePacket pkt) = writePacket pkt
|
||||
runPacketIO' (Transition _) = return ()
|
||||
|
||||
talk :: forall r. (MonadIO r, ByteReader r) => r ()
|
||||
talk = runPacketIO $ talk' SHandshake
|
||||
where talk' :: forall (st :: ProtocolState). SProtocolState st -> PacketIO st 'Closed ()
|
||||
talk' SClosed = ireturn ()
|
||||
talk' st = readPacket st `ibind` \pkt ->
|
||||
case pkt :: Packet 'Serverbound st of
|
||||
PktHandshake (PHandshake _ _ _ CloseConnection) -> transition CloseConnection :: PacketIO 'Handshake 'Closed ()
|
||||
PktHandshake (PHandshake _ _ _ HandshakeStatus) ->
|
||||
transition HandshakeStatus
|
||||
`iskip` talk' SStatus
|
||||
PktHandshake (PHandshake _ _ _ HandshakeLogin) ->
|
||||
transition HandshakeLogin
|
||||
`iskip` talk' SLogin
|
||||
PktStatus Request ->
|
||||
writePacket (PktStatus $ Response $ StatusResponse
|
||||
{ srVersion = PV498
|
||||
, srPlayersMax = 42
|
||||
, srPlayersOnline = 0
|
||||
, srPlayersSample = V.empty
|
||||
, srDescription = ChatText "Hello, world!"
|
||||
})
|
||||
`iskip` talk' SStatus
|
||||
PktStatus (Ping payload) ->
|
||||
writePacket (PktStatus $ Pong payload)
|
||||
`iskip` transition CloseConnection :: PacketIO 'Status 'Closed ()
|
||||
PktLogin (LoginStart name) ->
|
||||
writePacket (PktLogin $ LDisconnect $ ChatText "Login is not yet implemented.")
|
||||
`iskip` transition CloseConnection
|
||||
readPacket :: SProtocolState st -> PacketIO st st (Packet 'Serverbound st)
|
||||
readPacket st = rProtocolState st $ Command ReadPacket
|
||||
writePacket :: Packet 'Clientbound st -> PacketIO st st ()
|
||||
writePacket = Command . WritePacket
|
||||
transition :: ProtocolStateTransition st st' -> PacketIO st st' ()
|
||||
transition = Command . Transition
|
|
@ -0,0 +1,66 @@
|
|||
name: tmd
|
||||
version: 0.0.0.0
|
||||
github: "jamestmartin/tmd"
|
||||
license: AGPL-3
|
||||
author: "James Martin"
|
||||
maintainer: "james@jtmar.me"
|
||||
copyright: "Copyright: (C) 2019 James Martin"
|
||||
|
||||
extra-source-files:
|
||||
- README.md
|
||||
|
||||
# Metadata used when publishing your package
|
||||
# synopsis: Short description of your package
|
||||
# category: Web
|
||||
|
||||
# To avoid duplicated efforts in documentation and dealing with the
|
||||
# complications of embedding Haddock markup inside cabal files, it is
|
||||
# common to point users to the README.md file.
|
||||
description: Please see the README on GitHub at <https://github.com/jamestmartin/tmd#readme>
|
||||
|
||||
default-extensions:
|
||||
- BlockArguments
|
||||
- ConstraintKinds
|
||||
- DataKinds
|
||||
- FlexibleContexts
|
||||
- FlexibleInstances
|
||||
- GADTs
|
||||
- KindSignatures
|
||||
- LambdaCase
|
||||
- MultiParamTypeClasses
|
||||
- OverloadedStrings
|
||||
- PolyKinds
|
||||
- Rank2Types
|
||||
- ScopedTypeVariables
|
||||
|
||||
dependencies:
|
||||
- aeson >= 1.4 && < 2
|
||||
- attoparsec >= 0.13.2 && < 0.14
|
||||
- base >= 4.7 && < 5
|
||||
- bytestring >= 0.10.8 && < 0.11
|
||||
- free >= 5.1 && < 6
|
||||
- free-category == 0.0.2.0
|
||||
- mtl >= 2.2 && < 3
|
||||
- network >= 2.8 && < 4
|
||||
# Used to deserialize JSON to integers
|
||||
- scientific >= 0.3.6 && < 0.4
|
||||
- text >= 1.2 && < 2
|
||||
# Used to serialize JSON objects
|
||||
- unordered-containers >= 0.2.10 && < 0.3
|
||||
- uuid >= 1.3 && < 2
|
||||
# Used to serialize JSON arrays
|
||||
- vector >= 0.12 && < 0.13
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
|
||||
executables:
|
||||
tmd-exe:
|
||||
main: Main.hs
|
||||
source-dirs: app
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
dependencies:
|
||||
- tmd
|
|
@ -1 +0,0 @@
|
|||
nightly
|
|
@ -1,7 +0,0 @@
|
|||
format_code_in_doc_comments = true
|
||||
match_block_trailing_comma = true
|
||||
max_width = 120
|
||||
newline_style = "Native"
|
||||
normalize_comments = true
|
||||
normalize_doc_attributes = true
|
||||
use_try_shorthand = true
|
|
@ -0,0 +1,24 @@
|
|||
module Control.Monad.Indexed where
|
||||
|
||||
class IndexedMonad m where
|
||||
ireturn :: a -> m st st a
|
||||
ibind :: m st st' a -> (a -> m st' st'' b) -> m st st'' b
|
||||
|
||||
data Program transition st st' a where
|
||||
Return :: a -> Program t st st a
|
||||
Command :: transition st st' a -> Program transition st st' a
|
||||
Bind :: Program t st st' a -> (a -> Program t st' st'' b) -> Program t st st'' b
|
||||
|
||||
instance IndexedMonad (Program transition) where
|
||||
ireturn = Return
|
||||
ibind = Bind
|
||||
|
||||
iskip :: IndexedMonad m => m st st' () -> m st' st'' a -> m st st'' a
|
||||
iskip m n = m `ibind` \() -> n
|
||||
|
||||
runProgram :: forall m transition st st' a. Monad m => (forall st st' a. transition st st' a -> m a) -> Program transition st st' a -> m a
|
||||
runProgram execute = runProgram'
|
||||
where runProgram' :: forall st st' a. Program transition st st' a -> m a
|
||||
runProgram' (Return x) = return x
|
||||
runProgram' (Bind x f) = runProgram' x >>= runProgram' . f
|
||||
runProgram' (Command cmd) = execute cmd
|
|
@ -0,0 +1,9 @@
|
|||
module Data.Exists where
|
||||
|
||||
import GHC.Exts (Constraint)
|
||||
|
||||
data Exists c where
|
||||
Exists :: { fromExists :: c a } -> Exists c
|
||||
|
||||
data ExistsC (f :: k -> *) (constraint :: k -> Constraint) where
|
||||
ExistsC :: constraint a => f a -> ExistsC f constraint
|
|
@ -0,0 +1,117 @@
|
|||
module Data.NetEncoding where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.Word (Word8)
|
||||
|
||||
data Nat = Z | S Nat
|
||||
|
||||
data Prod a b = Prod a b
|
||||
|
||||
type family (x :: Nat) :+ (y :: Nat) :: (sum :: Nat) where
|
||||
Z :+ m = m
|
||||
S n :+ m = S (n :+ m)
|
||||
|
||||
type family Map (f :: a -> b) (xs :: [a]) :: [b] where
|
||||
Map f '[] = '[]
|
||||
Map f (x ': xs) = f x ': Map f xs
|
||||
|
||||
type family (xs :: [a]) :++ (ys :: [a]) :: [a] where
|
||||
'[] :++ ys = ys
|
||||
(x ': xs) :++ ys = x ': (xs :++ ys)
|
||||
|
||||
type family Cart (xs :: [a]) (ys :: [b]) :: [(a, b)] where
|
||||
Cart '[] _ = '[]
|
||||
Cart (x ': xs) ys = Map (Prod x) ys :++ Cart xs ys
|
||||
|
||||
type family Uncurry (f :: a -> b -> c) (x :: Product a b) :: c where
|
||||
Uncurry f ('Prod x y) = f x y
|
||||
|
||||
type EverySequence xs ys = Map (Uncurry (:+))
|
||||
|
||||
type family SeqCart (xs :: [Nat]) (ys :: [Nat]) :: (rs :: [Nat]) where
|
||||
SeqCart '[] _ = '[]
|
||||
SeqCart '(x:xs) ys = Map (x :+) ys :++ SeqCart xs ys
|
||||
|
||||
data NatDecoder :: * -> * where
|
||||
-- Take :: SNat n -> NatDecoder [n] (Vec n Word8)
|
||||
Take :: Int -> NatDecoder ByteString
|
||||
-- FMap :: (a -> b) -> NatDecoder ls a -> NatDecoder ls b
|
||||
FMap :: (a -> b) -> NatDecoder a -> NatDecoder b
|
||||
-- Pure :: a -> NatDecoder [Z] a
|
||||
Pure :: a -> NatDecoder a
|
||||
-- Sequence :: NatDecoder lens (a -> b) -> NatDecoder lens' a -> NatDecoder (SeqCart lens lens') b
|
||||
Sequence :: NatDecoder (a -> b) -> NatDecoder a -> NatDecoder b
|
||||
-- Bind :: NatDecoder lens a -> (a -> NatDecoder lens' b) -> NatDecoder (SeqCart lens lens') b
|
||||
Bind :: NatDecoder a -> (a -> NatDecoder b) -> NatDecoder b
|
||||
-- Empty :: NatDecoder [] a
|
||||
Empty :: NatDecoder a
|
||||
-- Alternate :: NatDecoder lens a -> NatDecoder lens' a -> NatDecoder (Append lens lens')
|
||||
Alternate :: NatDecoder a -> NatDecoder a -> NatDecoder a
|
||||
|
||||
quantumBogoParse :: (ByteString -> ByteString -> a) -> ByteString -> [a]
|
||||
quantumBogoParse f bs = map (uncurry f . flip BS.splitAt bs) [0 .. length bs]
|
||||
|
||||
runNatDecoder :: ByteString -> NatDecoder a -> [a]
|
||||
runNatDecoder bs (Take m)
|
||||
| BS.length n == m = [bs]
|
||||
| otherwise = []
|
||||
runNatDecoder bs (FMap f x) = map f $ runNatDecoder bs x
|
||||
runNatDecoder bs (Pure x)
|
||||
| BS.length bs == 0 = [x]
|
||||
| otherwise = []
|
||||
runNatDecoder bs (Sequence f x)
|
||||
|
||||
data Nat = Z
|
||||
| S !Nat
|
||||
|
||||
data SNat :: Nat -> * where
|
||||
SZ :: SNat 'Z
|
||||
SS :: SNat n -> SNat ('S n)
|
||||
|
||||
data Vec :: Nat -> * -> * where
|
||||
VNil :: Vec 'Z a
|
||||
(:::) :: a -> Vec len a -> Vec ('S len) a
|
||||
|
||||
type BS n = Vec n Word8
|
||||
|
||||
data NatDecoderF :: Nat -> * -> * where
|
||||
Sequence :: NatDecoderF n a -> (a -> NatDecoderF m b) -> NatDecoderF (n :+ m) b
|
||||
Alternate :: NatDecoderF
|
||||
|
||||
data NetDecoderF a
|
||||
= Isolate (Int, NetDecoderF a)
|
||||
| Retrieve (ByteString -> a)
|
||||
| Join (NetDecoderF b, b -> NetDecoderF a)
|
||||
|
||||
data NetDecoderF a
|
||||
= Isolate Int (NetDecoderF a)
|
||||
| Retrieve (ByteString -> a)
|
||||
|
||||
data NetEncoderF a = NetEncoderF
|
||||
{ coIsolate :: (Int, NetEncoderF a)
|
||||
, coRetrieve :: a -> ByteString
|
||||
}
|
||||
|
||||
data NetDecoder :: * -> * where
|
||||
Isolate :: (Int, NetDecoderF a) -> NetDecoderF a
|
||||
Retrieve :: (ByteString -> a) -> NetDecoderF a
|
||||
Join :: (NetDecoderF b, (b -> NetDecoderF a)) -> NetDecoderF a
|
||||
Return :: a -> NetDecoderF a
|
||||
|
||||
data NetEncoder :: * -> * where
|
||||
CoIsolate :: NetEncoderF a -> (Int, NetEncoderF a)
|
||||
CoRetrieve :: NetEncoderF a -> (a -> ByteString)
|
||||
CoJoin :: NetEncoderF a -> (NetEncoderF a -> b) -> NetEncoderF b
|
||||
CoReturn :: NetEncoderF a -> a
|
||||
|
||||
data FreeFunctor :: (* -> *) -> * -> * where
|
||||
Fmap :: (a -> b) -> f a -> Lan f b
|
||||
|
||||
instance Functor (Lan f) where
|
||||
fmap f (Fmap g x) = Fmap (f . g) x
|
||||
|
||||
data FreeContravariant :: (* ->
|
||||
|
||||
NetDecoderF ByteString & ((Int, NetDecoderF a) -> NetDecoderF a)
|
||||
NetEncoderF (-ByteString) + (
|
|
@ -0,0 +1,7 @@
|
|||
module Data.NetParser where
|
||||
|
||||
import Data.ByteString
|
||||
|
||||
data NetParserF a where
|
||||
Isolate :: Int -> NetParserF a -> NetParserF a
|
||||
Rest :: NetParserF ByteString
|
|
@ -0,0 +1,32 @@
|
|||
module Minecraft.Protocol where
|
||||
|
||||
import Minecraft.Protocol.DataTypes (Serialize, Deserialize, serialize, deserialize)
|
||||
import Minecraft.Protocol.Direction
|
||||
import Minecraft.Protocol.State
|
||||
import Minecraft.Protocol.State.Handshake
|
||||
import Minecraft.Protocol.State.Login
|
||||
import Minecraft.Protocol.State.Status
|
||||
|
||||
data Packet :: PacketDirection -> ProtocolState -> * where
|
||||
PktHandshake :: PacketHandshake dir -> Packet dir 'Handshake
|
||||
PktStatus :: PacketStatus dir -> Packet dir 'Status
|
||||
PktLogin :: PacketLogin dir -> Packet dir 'Login
|
||||
|
||||
instance (SProtocolStateI st, SPacketDirectionI dir) => Deserialize (Packet dir st) where
|
||||
deserialize = a \case
|
||||
SClosed -> fail "No packets exist for a closed connection."
|
||||
SHandshake -> PktHandshake <$> deserialize
|
||||
SStatus -> PktStatus <$> deserialize
|
||||
SLogin -> PktLogin <$> deserialize
|
||||
_ -> undefined
|
||||
where a :: (SProtocolStateI st, SPacketDirectionI dir) => (SProtocolState st -> f (g dir st)) -> f (g dir st)
|
||||
a f = f sProtocolState
|
||||
|
||||
instance Serialize (Packet dir st) where
|
||||
serialize (PktHandshake pkt) = serialize pkt
|
||||
serialize (PktStatus pkt) = serialize pkt
|
||||
serialize (PktLogin pkt) = serialize pkt
|
||||
|
||||
class MonadProtocol m where
|
||||
sendPacket :: Packet dir -> m dir ()
|
||||
recvPacket :: SPacketDirectionI dir => m dir Packet
|
|
@ -0,0 +1,60 @@
|
|||
{-# LANGUAGE UndecidableInstances #-}
|
||||
module Minecraft.Protocol.ByteReader where
|
||||
|
||||
import Control.Monad (replicateM)
|
||||
import Control.Monad.Except (MonadError, ExceptT, throwError, runExceptT)
|
||||
import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask)
|
||||
import Control.Monad.State (MonadState, StateT, evalStateT, MonadIO, liftIO)
|
||||
import qualified Control.Monad.State as St
|
||||
import Data.Attoparsec.Zepto
|
||||
import Data.Bits ((.|.), testBit, clearBit, shiftL)
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.ByteString.Builder
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.ByteString.Lazy (toStrict)
|
||||
import Data.Word (Word8)
|
||||
import Minecraft.Protocol.DataTypes
|
||||
import Network.Socket (Socket)
|
||||
import Network.Socket.ByteString (recv, sendMany)
|
||||
|
||||
class (MonadError String m, MonadReader Socket m) => ByteReader m where
|
||||
nextByte :: m Word8
|
||||
|
||||
instance (MonadReader Socket m, MonadState ByteString m, MonadIO m, MonadError String m) => ByteReader m where
|
||||
nextByte = BS.uncons <$> St.get >>= \case
|
||||
Just (byte, bytes) -> St.put bytes >> return byte
|
||||
Nothing -> do
|
||||
conn <- ask
|
||||
fresh <- liftIO $ recv conn 4096
|
||||
-- A null return value means the connection was closed.
|
||||
if BS.null fresh
|
||||
then throwError "EOF"
|
||||
else do St.put fresh ; nextByte
|
||||
|
||||
type ByteReaderStack a = ExceptT String (StateT ByteString (ReaderT Socket IO)) a
|
||||
|
||||
nextVarInt :: ByteReader r => r Int
|
||||
nextVarInt = do
|
||||
h <- nextByte
|
||||
let value = fromIntegral $ clearBit h 7 :: Int
|
||||
if testBit h 7
|
||||
then do r <- nextVarInt
|
||||
return $ value .|. shiftL r 7
|
||||
else return value
|
||||
|
||||
readBytes :: Socket -> ByteReaderStack a -> IO (Either String a)
|
||||
readBytes conn = flip runReaderT conn . flip evalStateT BS.empty . runExceptT
|
||||
|
||||
nextPacket :: (ByteReader r, Deserialize a, MonadIO r) => r a
|
||||
nextPacket = do
|
||||
len <- nextVarInt
|
||||
packet <- parse deserialize <$> BS.pack <$> replicateM len nextByte
|
||||
case packet of
|
||||
Left err -> throwError err
|
||||
Right x -> return x
|
||||
|
||||
writePacket :: (MonadIO m, MonadReader Socket m, Serialize s) => s -> m ()
|
||||
writePacket packet = do
|
||||
conn <- ask
|
||||
liftIO $ sendMany conn $ [toStrict $ toLazyByteString $ serialize $ VarInt $ BS.length bytes, bytes]
|
||||
where bytes = toStrict $ toLazyByteString $ serialize packet
|
|
@ -0,0 +1,88 @@
|
|||
module Minecraft.Protocol.DataTypes where
|
||||
|
||||
import Data.Aeson (ToJSON, toJSON, encode)
|
||||
import qualified Data.Aeson.Types as AT
|
||||
import Data.Attoparsec.Zepto
|
||||
import Data.Bits (Bits, (.|.), clearBit, setBit, testBit, shiftL, shiftR)
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.ByteString.Builder
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||
import Data.Word (Word8, Word16, Word32, Word64)
|
||||
import Prelude hiding (take, takeWhile)
|
||||
|
||||
newtype VarInt = VarInt Int deriving Show
|
||||
|
||||
class Serialize s where
|
||||
serialize :: s -> Builder
|
||||
|
||||
class Deserialize s where
|
||||
deserialize :: Parser s
|
||||
|
||||
anyWord8 :: Parser Word8
|
||||
anyWord8 = BS.head <$> take 1
|
||||
|
||||
buildWord16 :: Word8 -> Word8 -> Word16
|
||||
buildWord16 low' high' = low .|. shiftL high 8
|
||||
where low = fromIntegral low'
|
||||
high = fromIntegral high'
|
||||
|
||||
anyWord16BE :: Parser Word16
|
||||
anyWord16BE = do
|
||||
high <- anyWord8
|
||||
low <- anyWord8
|
||||
return $ buildWord16 low high
|
||||
|
||||
buildWord32 :: Word16 -> Word16 -> Word32
|
||||
buildWord32 low' high' = low .|. shiftL high 16
|
||||
where low = fromIntegral low'
|
||||
high = fromIntegral high'
|
||||
|
||||
buildWordBE :: (Integral b, Bits b) => BS.ByteString -> b
|
||||
buildWordBE ws = case BS.foldl' (\(i, high) low -> (i + 1, fromIntegral low .|. shiftL high i)) (0, 0) ws of
|
||||
(_, x) -> x
|
||||
where imap f xs = imap' xs 0
|
||||
where imap' [] _ = []
|
||||
imap' (x:xs) i = f i x : imap' xs (i + 1)
|
||||
|
||||
anyWord64be :: Parser Word64
|
||||
anyWord64be = buildWordBE <$> take 8
|
||||
|
||||
instance Serialize VarInt where
|
||||
serialize (VarInt x)
|
||||
| x' /= 0 = word8 (setBit word 7) <> serialize (VarInt x')
|
||||
| otherwise = word8 word
|
||||
where x' = shiftR x 7
|
||||
word = clearBit (fromIntegral x :: Word8) 7
|
||||
|
||||
instance Deserialize VarInt where
|
||||
deserialize = VarInt <$> do
|
||||
bytes <- BS.map (`clearBit` 7) <$> takeWhile (`testBit` 7)
|
||||
last <- fromIntegral <$> anyWord8
|
||||
return $ BS.foldr' (\x acc -> fromIntegral x .|. shiftL acc 7) last bytes
|
||||
|
||||
instance Serialize Text where
|
||||
serialize str =
|
||||
serialize (VarInt $ BS.length bytes)
|
||||
<> byteString bytes
|
||||
where bytes = encodeUtf8 str
|
||||
|
||||
instance Deserialize Text where
|
||||
deserialize = do
|
||||
VarInt len <- deserialize
|
||||
decodeUtf8 <$> take len
|
||||
|
||||
data Chat = ChatText Text
|
||||
|
||||
instance ToJSON Chat where
|
||||
toJSON (ChatText text) = AT.Object $ HM.fromList
|
||||
[ ("text", AT.String text)
|
||||
]
|
||||
|
||||
instance Serialize Chat where
|
||||
serialize chat =
|
||||
serialize (VarInt $ fromIntegral $ BS.length bytes)
|
||||
<> byteString bytes
|
||||
where bytes = BSL.toStrict $ encode chat
|
|
@ -0,0 +1,16 @@
|
|||
module Minecraft.Protocol.Direction where
|
||||
|
||||
data PacketDirection = Clientbound | Serverbound
|
||||
|
||||
data SPacketDirection :: PacketDirection -> * where
|
||||
SClientbound :: SPacketDirection 'Clientbound
|
||||
SServerbound :: SPacketDirection 'Serverbound
|
||||
|
||||
class SPacketDirectionI (dir :: PacketDirection) where
|
||||
sPacketDirection :: SPacketDirection dir
|
||||
|
||||
instance SPacketDirectionI 'Clientbound where
|
||||
sPacketDirection = SClientbound
|
||||
|
||||
instance SPacketDirectionI 'Serverbound where
|
||||
sPacketDirection = SServerbound
|
|
@ -0,0 +1,7 @@
|
|||
class Minecraft.Protocol.Packet where
|
||||
|
||||
import Data.NetEncoding
|
||||
|
||||
class Packet a where
|
||||
decode :: NetDecoder a
|
||||
encode :: NetEncoder a
|
|
@ -0,0 +1,131 @@
|
|||
{-# LANGUAGE UndecidableInstances #-}
|
||||
module Minecraft.Protocol.State where
|
||||
|
||||
import Control.Category.Free (Cat (Id, (:.:)))
|
||||
import Data.ByteString.Builder (word8)
|
||||
import Data.Exists (Exists (Exists))
|
||||
import Minecraft.Protocol.DataTypes (Serialize, Deserialize, serialize, deserialize, anyWord8)
|
||||
|
||||
-- | The packets that the client and server may send and recieve
|
||||
-- are dependent on the protocol state.
|
||||
-- Each protocol state uses a separate set of packet IDs.
|
||||
--
|
||||
-- Other sources, like wiki.vg, may refer to this as the connection state.
|
||||
-- I refer to it as the protocol state to distinguish it
|
||||
-- from the state of the connection as a whole,
|
||||
-- which may include information about the player,
|
||||
-- compression and encryption state, and so forth.
|
||||
data ProtocolState
|
||||
-- | The initial state of a new connection.
|
||||
= Handshake
|
||||
-- | The client is querying the server status, but not joining the game.
|
||||
| Status
|
||||
-- | Logging in and initializing protocol state (compression and encryption).
|
||||
| Login
|
||||
-- | The standard gameplay protocol state after login.
|
||||
| Play
|
||||
-- | The connection is closed. No more packets will be sent or recieved.
|
||||
| Closed
|
||||
|
||||
-- | A ProtocolState available as both a type and a value.
|
||||
data SProtocolState :: ProtocolState -> * where
|
||||
SHandshake :: SProtocolState 'Handshake
|
||||
SStatus :: SProtocolState 'Status
|
||||
SLogin :: SProtocolState 'Login
|
||||
SPlay :: SProtocolState 'Play
|
||||
SClosed :: SProtocolState 'Closed
|
||||
|
||||
protocolState :: SProtocolState st -> ProtocolState
|
||||
protocolState SHandshake = Handshake
|
||||
protocolState SStatus = Status
|
||||
protocolState SLogin = Login
|
||||
protocolState SPlay = Play
|
||||
protocolState SClosed = Closed
|
||||
|
||||
-- | An implicitly-passed protocol state.
|
||||
class SProtocolStateI (st :: ProtocolState) where
|
||||
-- | Retrieve the known value of the protocol state singleton.
|
||||
sProtocolState :: SProtocolState st
|
||||
|
||||
instance SProtocolStateI 'Handshake where
|
||||
sProtocolState = SHandshake
|
||||
|
||||
instance SProtocolStateI 'Status where
|
||||
sProtocolState = SStatus
|
||||
|
||||
instance SProtocolStateI 'Login where
|
||||
sProtocolState = SLogin
|
||||
|
||||
instance SProtocolStateI 'Play where
|
||||
sProtocolState = SPlay
|
||||
|
||||
instance SProtocolStateI 'Closed where
|
||||
sProtocolState = SClosed
|
||||
|
||||
-- | Explicitly specify an implicitly-passed protocol state.
|
||||
rProtocolState :: SProtocolState st -> (SProtocolStateI st => t) -> t
|
||||
rProtocolState SHandshake x = x
|
||||
rProtocolState SStatus x = x
|
||||
rProtocolState SLogin x = x
|
||||
rProtocolState SPlay x = x
|
||||
rProtocolState SClosed x = x
|
||||
|
||||
-- | The valid protocol state transitions.
|
||||
data ProtocolStateTransition
|
||||
-- | A transition from this state ...
|
||||
:: ProtocolState
|
||||
-- | ... to this one.
|
||||
-> ProtocolState
|
||||
-> * where
|
||||
HandshakeStatus :: ProtocolStateTransition 'Handshake 'Status
|
||||
HandshakeLogin :: ProtocolStateTransition 'Handshake 'Login
|
||||
LoginPlay :: ProtocolStateTransition 'Login 'Play
|
||||
CloseConnection :: ProtocolStateTransition a 'Closed
|
||||
|
||||
class ProtocolStateTransitionI (to :: ProtocolState) (from :: ProtocolState) where
|
||||
protocolStateTransition :: ProtocolStateTransition to from
|
||||
|
||||
instance ProtocolStateTransitionI 'Handshake 'Status where
|
||||
protocolStateTransition = HandshakeStatus
|
||||
|
||||
instance ProtocolStateTransitionI 'Handshake 'Login where
|
||||
protocolStateTransition = HandshakeLogin
|
||||
|
||||
instance ProtocolStateTransitionI 'Login 'Play where
|
||||
protocolStateTransition = LoginPlay
|
||||
|
||||
instance ProtocolStateTransitionI a 'Closed where
|
||||
protocolStateTransition = CloseConnection
|
||||
|
||||
rProtocolStateTransition :: ProtocolStateTransition st st' -> (ProtocolStateTransitionI st st' => a) -> a
|
||||
rProtocolStateTransition HandshakeStatus x = x
|
||||
rProtocolStateTransition HandshakeLogin x = x
|
||||
rProtocolStateTransition LoginPlay x = x
|
||||
rProtocolStateTransition CloseConnection x = x
|
||||
|
||||
nextState :: ProtocolStateTransition st st' -> SProtocolState st'
|
||||
nextState HandshakeStatus = SStatus
|
||||
nextState HandshakeLogin = SLogin
|
||||
nextState LoginPlay = SPlay
|
||||
nextState CloseConnection = SClosed
|
||||
|
||||
instance Enum (Exists (ProtocolStateTransition 'Handshake)) where
|
||||
toEnum 0 = Exists $ CloseConnection
|
||||
toEnum 1 = Exists $ HandshakeStatus
|
||||
toEnum 2 = Exists $ HandshakeLogin
|
||||
toEnum n = Exists $ error $ "Invalid transition from handshake: " ++ show n
|
||||
|
||||
fromEnum (Exists t) = case t of
|
||||
CloseConnection -> 0
|
||||
HandshakeStatus -> 1
|
||||
HandshakeLogin -> 2
|
||||
|
||||
instance Deserialize (Exists (ProtocolStateTransition 'Handshake)) where
|
||||
deserialize = anyWord8 >>= \case
|
||||
0 -> return $ Exists CloseConnection
|
||||
1 -> return $ Exists HandshakeStatus
|
||||
2 -> return $ Exists HandshakeLogin
|
||||
n -> fail $ "Invalid next state: " ++ show n
|
||||
|
||||
instance Serialize (Exists (ProtocolStateTransition 'Handshake)) where
|
||||
serialize = word8 . fromIntegral . fromEnum
|
|
@ -0,0 +1,52 @@
|
|||
module Minecraft.Protocol.State.Handshake where
|
||||
|
||||
import Data.Attoparsec.Zepto
|
||||
import Data.ByteString.Builder (word16BE)
|
||||
import Data.Exists
|
||||
import Data.Text (Text)
|
||||
import Data.Word (Word16)
|
||||
import Minecraft.Protocol.DataTypes
|
||||
import Minecraft.Protocol.Direction
|
||||
import Minecraft.Protocol.State (ProtocolState (Handshake), ProtocolStateTransition)
|
||||
import Minecraft.Protocol.Version
|
||||
|
||||
type ServerAddress = Text
|
||||
|
||||
type ServerPort = Word16
|
||||
|
||||
data PacketHandshake :: PacketDirection -> * where
|
||||
-- | https://wiki.vg/Server_List_Ping#Handshake
|
||||
-- | The first packet sent from the client to the server on initiating a connection.
|
||||
PHandshake
|
||||
-- | An unspecified 'ProtocolVersion' means the client is querying what version the server prefers.
|
||||
:: Maybe ProtocolVersion
|
||||
-- | The server address is the address (hostname or IP) the client used to connect, after any SRV records have been resolved.
|
||||
-> ServerAddress
|
||||
-- | The port the client used to connect.
|
||||
-> ServerPort
|
||||
-- | The next protocol state to be transitioned to.
|
||||
-> ProtocolStateTransition 'Handshake st'
|
||||
-> PacketHandshake 'Serverbound
|
||||
|
||||
instance Serialize (PacketHandshake dir) where
|
||||
serialize (PHandshake ver addr port next) =
|
||||
serialize (VarInt 0)
|
||||
<> serialize (AmbProtocolVersion ver)
|
||||
<> serialize addr
|
||||
<> word16BE port
|
||||
<> serialize (Exists next)
|
||||
|
||||
instance SPacketDirectionI dir => Deserialize (PacketHandshake dir) where
|
||||
deserialize = a \case
|
||||
SClientbound -> fail "There are no clientbound handshake packets."
|
||||
SServerbound -> do
|
||||
VarInt 0 <- deserialize
|
||||
AmbProtocolVersion ver <- deserialize
|
||||
addr <- deserialize
|
||||
port <- anyWord16BE
|
||||
Exists next <- deserialize
|
||||
return $ PHandshake ver addr port next
|
||||
where a :: SPacketDirectionI dir => (SPacketDirection dir -> f (g dir)) -> f (g dir)
|
||||
a f = f sPacketDirection
|
||||
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
module Minecraft.Protocol.State.Login where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Text (Text)
|
||||
import Data.UUID (UUID)
|
||||
import Minecraft.Protocol.DataTypes
|
||||
import Minecraft.Protocol.Direction
|
||||
|
||||
-- Max 20 characters, empty by default.
|
||||
newtype ServerID = ServerID Text
|
||||
newtype PublicKey = PublicKey ByteString
|
||||
-- 4 bytes by default.
|
||||
newtype VerifyToken = VerifyToken ByteString
|
||||
newtype Identifier = Identifier Text
|
||||
newtype SharedSecret = SharedSecret ByteString
|
||||
|
||||
data PacketLogin :: PacketDirection -> * where
|
||||
LDisconnect :: Chat -> PacketLogin 'Clientbound
|
||||
EncryptionRequest :: ServerID -> PublicKey -> VerifyToken -> PacketLogin 'Clientbound
|
||||
LoginSuccess
|
||||
-- | The player's UUID, presented as a 36-character string.
|
||||
:: UUID
|
||||
-- | The player's username. Max 16 characters.
|
||||
-> Text
|
||||
-> PacketLogin 'Clientbound
|
||||
SetCompression
|
||||
-- | The compression threshold, in bytes. The maximum size of a pre-compressed packet.
|
||||
-- Negative values disable compression.
|
||||
:: Int
|
||||
-> PacketLogin 'Clientbound
|
||||
LoginPluginRequest
|
||||
-- | MessageID
|
||||
:: Int
|
||||
-> Identifier
|
||||
-- | Any data, depending on the channel.
|
||||
-- This is not marked with an explicit length;
|
||||
-- it is inferred from the length of the packet.
|
||||
-> ByteString
|
||||
-> PacketLogin 'Clientbound
|
||||
LoginStart
|
||||
-- | The player's username.
|
||||
:: Text
|
||||
-> PacketLogin 'Serverbound
|
||||
EncryptionResponse
|
||||
:: SharedSecret
|
||||
-> VerifyToken
|
||||
-> PacketLogin 'Serverbound
|
||||
LoginPluginResponse
|
||||
-- | MessageID
|
||||
:: Int
|
||||
-- | Whether the client understands the request.
|
||||
-> Bool
|
||||
-- | Channel-specific data, length inferred from the packet length.
|
||||
-- | No data will be sent if the client does not understand the request.
|
||||
-> ByteString
|
||||
-> PacketLogin 'Serverbound
|
||||
|
||||
instance Serialize (PacketLogin dir) where
|
||||
serialize (LDisconnect reason) =
|
||||
serialize (VarInt 0)
|
||||
<> serialize reason
|
||||
serialize (LoginStart name) =
|
||||
serialize (VarInt 0)
|
||||
<> serialize name
|
||||
|
||||
instance SPacketDirectionI dir => Deserialize (PacketLogin dir) where
|
||||
deserialize = a \case
|
||||
{-SClientbound -> do
|
||||
VarInt 0 <- deserialize
|
||||
LDisconnect <$> deserialize-}
|
||||
SServerbound -> do
|
||||
VarInt 0 <- deserialize
|
||||
LoginStart <$> deserialize
|
||||
where a :: SPacketDirectionI dir => (SPacketDirection dir -> f (g dir)) -> f (g dir)
|
||||
a f = f sPacketDirection
|
|
@ -0,0 +1,118 @@
|
|||
module Minecraft.Protocol.State.Status where
|
||||
|
||||
import Data.Aeson (ToJSON, toJSON, FromJSON, parseJSON)
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Types as AT
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.ByteString.Builder
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.Text (Text)
|
||||
import Data.UUID (UUID)
|
||||
import qualified Data.UUID as U
|
||||
import Data.Vector (Vector)
|
||||
import Data.Word (Word16, Word64)
|
||||
import Minecraft.Protocol.DataTypes
|
||||
import Minecraft.Protocol.Direction
|
||||
import Minecraft.Protocol.Version
|
||||
|
||||
data SamplePlayer = SamplePlayer
|
||||
{ spName :: Text
|
||||
, spUUID :: UUID
|
||||
}
|
||||
|
||||
instance ToJSON SamplePlayer where
|
||||
toJSON sp = AT.Object $ HM.fromList
|
||||
[ ("name", AT.String $ spName sp)
|
||||
, ("id" , AT.String $ U.toText $ spUUID sp)
|
||||
]
|
||||
|
||||
instance FromJSON SamplePlayer where
|
||||
parseJSON json = flip (AT.withObject "Sample Player") json \obj -> do
|
||||
let (Just name) = HM.lookup "name" obj >>= fromATString
|
||||
let (Just uuid) = HM.lookup "id" obj >>= fromATString >>= U.fromText
|
||||
return $ SamplePlayer name uuid
|
||||
where fromATString :: AT.Value -> Maybe Text
|
||||
fromATString (AT.String str) = Just str
|
||||
fromATString _ = Nothing
|
||||
|
||||
|
||||
-- | https://wiki.vg/Server_List_Ping#Response
|
||||
data StatusResponse = StatusResponse
|
||||
{ srVersion :: ProtocolVersion
|
||||
, srPlayersMax :: Word16
|
||||
, srPlayersOnline :: Word16
|
||||
, srPlayersSample :: Vector SamplePlayer
|
||||
, srDescription :: Chat
|
||||
-- TODO: add support for favicons
|
||||
}
|
||||
|
||||
instance ToJSON StatusResponse where
|
||||
toJSON sr = AT.Object $ HM.fromList
|
||||
[ ("version", toJSON $ srVersion sr)
|
||||
, ("players", AT.Object $ HM.fromList
|
||||
[ ("max", AT.Number $ fromIntegral $ srPlayersMax sr)
|
||||
, ("online", AT.Number $ fromIntegral $ srPlayersOnline sr)
|
||||
, ("sample", AT.Array $ fmap toJSON $ srPlayersSample sr)
|
||||
])
|
||||
, ("description", toJSON $ srDescription sr)
|
||||
]
|
||||
|
||||
instance Serialize StatusResponse where
|
||||
serialize sr =
|
||||
serialize (VarInt $ fromIntegral $ BS.length bytes)
|
||||
<> byteString bytes
|
||||
where bytes = BSL.toStrict $ A.encode sr
|
||||
|
||||
data PacketStatus :: PacketDirection -> * where
|
||||
-- ## request / response
|
||||
-- | https://wiki.vg/Server_List_Ping#Request
|
||||
Request :: PacketStatus 'Serverbound
|
||||
-- | https://wiki.vg/Server_List_Ping#Response
|
||||
Response :: StatusResponse -> PacketStatus 'Clientbound
|
||||
-- ## ping / pong
|
||||
-- | https://wiki.vg/Server_List_Ping#Ping
|
||||
Ping
|
||||
-- | The ping "payload", chosen by the client to ensure that the server isn't cheating its 'Pong's.
|
||||
:: Word64
|
||||
-> PacketStatus 'Serverbound
|
||||
-- | https://wiki.vg/Server_List_Ping#Pong
|
||||
Pong
|
||||
-- | The same "payload" provided in 'Ping'.
|
||||
:: Word64
|
||||
-> PacketStatus 'Clientbound
|
||||
|
||||
psId :: PacketStatus a -> VarInt
|
||||
psId Request = VarInt 0
|
||||
psId (Response _) = VarInt 0
|
||||
psId (Ping _) = VarInt 1
|
||||
psId (Pong _) = VarInt 1
|
||||
|
||||
instance SPacketDirectionI dir => Deserialize (PacketStatus dir) where
|
||||
deserialize = a \case
|
||||
{-SClientbound -> do
|
||||
VarInt pid <- deserialize
|
||||
case pid of
|
||||
0 -> Response <$> deserialize
|
||||
1 -> Pong <$> anyWord64be
|
||||
n -> fail $ "Unknown packet ID for clientbound status: " ++ show n-}
|
||||
SServerbound -> do
|
||||
VarInt pid <- deserialize
|
||||
case pid of
|
||||
0 -> return Request
|
||||
1 -> Ping <$> anyWord64be
|
||||
n -> fail $ "Unknown packet ID for serverbound status: " ++ show n
|
||||
where a :: SPacketDirectionI dir => (SPacketDirection dir -> f (g dir)) -> f (g dir)
|
||||
a f = f sPacketDirection
|
||||
|
||||
instance Serialize (PacketStatus dir) where
|
||||
serialize pkt@(Response sr) =
|
||||
serialize (psId pkt)
|
||||
<> serialize sr
|
||||
serialize pkt@(Pong payload) =
|
||||
serialize (psId pkt)
|
||||
<> word64BE payload
|
||||
serialize pkt@Request = serialize $ psId pkt
|
||||
serialize pkt@(Ping payload) =
|
||||
serialize (psId pkt)
|
||||
<> word64BE payload
|
|
@ -0,0 +1,61 @@
|
|||
module Minecraft.Protocol.Version where
|
||||
|
||||
import Control.Arrow ((<<<), (>>>))
|
||||
import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON)
|
||||
import qualified Data.Aeson.Types as AT
|
||||
import Data.ByteString.Builder (word8)
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Scientific (toBoundedInteger)
|
||||
import Data.Text (Text)
|
||||
import Minecraft.Protocol.DataTypes
|
||||
|
||||
data ProtocolVersion = PV498 deriving Show
|
||||
newtype AmbProtocolVersion = AmbProtocolVersion { getAmbProtocolVersion :: Maybe ProtocolVersion }
|
||||
|
||||
versionName :: ProtocolVersion -> Text
|
||||
versionName PV498 = "1.14.4"
|
||||
|
||||
safeToEnum :: Int -> Maybe (Maybe ProtocolVersion)
|
||||
safeToEnum 0 = Just Nothing
|
||||
safeToEnum 498 = Just $ Just PV498
|
||||
safeToEnum _ = Nothing
|
||||
|
||||
instance Enum (Maybe ProtocolVersion) where
|
||||
toEnum = fromJust . safeToEnum
|
||||
|
||||
fromEnum Nothing = 0
|
||||
fromEnum (Just PV498) = 498
|
||||
|
||||
instance Enum ProtocolVersion where
|
||||
toEnum = toEnum >>> fromJust
|
||||
fromEnum = (fromEnum :: Maybe ProtocolVersion -> Int) <<< Just
|
||||
|
||||
instance Deserialize AmbProtocolVersion where
|
||||
deserialize = AmbProtocolVersion <$> do
|
||||
VarInt verNum <- deserialize
|
||||
maybe (fail $ "Unknown protocol version: " ++ show verNum) return $ safeToEnum verNum
|
||||
|
||||
instance Serialize AmbProtocolVersion where
|
||||
serialize = getAmbProtocolVersion >>> fromEnum >>> fromIntegral >>> word8
|
||||
|
||||
instance Deserialize ProtocolVersion where
|
||||
deserialize = deserialize >>= (getAmbProtocolVersion >>> maybe (fail "Must specify protocol version.") return)
|
||||
|
||||
instance Serialize ProtocolVersion where
|
||||
serialize = serialize <<< AmbProtocolVersion <<< Just
|
||||
|
||||
instance ToJSON ProtocolVersion where
|
||||
toJSON pv = AT.Object $ HM.fromList
|
||||
[ ("name", AT.String $ versionName pv)
|
||||
, ("protocol", AT.Number $ fromIntegral $ fromEnum pv)
|
||||
]
|
||||
|
||||
instance FromJSON ProtocolVersion where
|
||||
parseJSON = AT.withObject "Protocol Version" \obj -> do
|
||||
let Just (AT.Number verNum) = HM.lookup "protocol" obj
|
||||
-- Outer just: whether the integer fits in our data type
|
||||
-- Middle just: whether 'protocol' is present in this object
|
||||
-- Inner just: whether the protocol version was specified or ambiguous
|
||||
let Just (Just (Just ver)) = safeToEnum <$> toBoundedInteger verNum
|
||||
return ver
|
13
src/cli.yml
13
src/cli.yml
|
@ -1,13 +0,0 @@
|
|||
name: TMD
|
||||
version: "1.0"
|
||||
author: James Martin
|
||||
about: A Minecraft protocol-compatible server written in Rust.
|
||||
args:
|
||||
- host:
|
||||
long: host
|
||||
help: The IP address the server will use to listen for connections. Defaults to any address (`::`).
|
||||
takes_value: true
|
||||
- port:
|
||||
long: port
|
||||
help: The port the server will accept connections from. Defaults to 25565.
|
||||
takes_value: true
|
134
src/main.rs
134
src/main.rs
|
@ -1,134 +0,0 @@
|
|||
#![allow(incomplete_features)]
|
||||
#![feature(const_generics)]
|
||||
#![feature(never_type)]
|
||||
|
||||
mod net;
|
||||
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
use clap::{load_yaml, App};
|
||||
|
||||
let yaml = load_yaml!("cli.yml");
|
||||
let args = App::from_yaml(yaml).get_matches();
|
||||
let host: IpAddr = args
|
||||
.value_of("host")
|
||||
.unwrap_or("::")
|
||||
.parse()
|
||||
.expect("Invalid host IP address.");
|
||||
let port: u16 = args
|
||||
.value_of("port")
|
||||
.unwrap_or("25565")
|
||||
.parse()
|
||||
.expect("Port must be an integer between 1 an 65535.");
|
||||
|
||||
let listener = TcpListener::bind((host, port))
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Failed to bind to {}:{}.", host, port));
|
||||
|
||||
listen(listener).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn listen(listener: TcpListener) {
|
||||
loop {
|
||||
let (socket, _) = match listener.accept().await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to accept client: {:?}", e);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
tokio::spawn(accept_connection(socket));
|
||||
}
|
||||
}
|
||||
|
||||
async fn accept_connection(socket: TcpStream) -> Option<!> {
|
||||
use crate::net::chat::Chat;
|
||||
use crate::net::listener::*;
|
||||
use crate::net::packet_stream::PacketStreamMaps;
|
||||
use crate::net::protocol::state::play::*;
|
||||
|
||||
#[cfg(not(feature = "compression"))]
|
||||
let compression_threshold = None;
|
||||
#[cfg(feature = "compression")]
|
||||
let compression_threshold = Some(64);
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
let encryption_config = None;
|
||||
#[cfg(feature = "encryption")]
|
||||
let encryption_config = Some({
|
||||
use rsa::RSAPrivateKey;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
let mut public_key_bytes = Vec::new();
|
||||
File::open("private/pub.der")
|
||||
.expect("missing public key")
|
||||
.read_to_end(&mut public_key_bytes)
|
||||
.unwrap();
|
||||
|
||||
let mut private_key_bytes = Vec::new();
|
||||
File::open("private/priv.der")
|
||||
.expect("missing private key")
|
||||
.read_to_end(&mut private_key_bytes)
|
||||
.unwrap();
|
||||
|
||||
let private_key = RSAPrivateKey::from_pkcs1(&private_key_bytes).expect("Invalid private key.");
|
||||
|
||||
EncryptionConfig {
|
||||
private_key,
|
||||
public_key_bytes: public_key_bytes.into_boxed_slice(),
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(not(feature = "authentication"))]
|
||||
let authentication_config = None;
|
||||
#[cfg(feature = "authentication")]
|
||||
let authentication_config = Some(AuthenticationConfig::default());
|
||||
|
||||
let config = ConnectionConfig {
|
||||
enforce_host: Some(vec!["localhost".to_string().into_boxed_str()].into_boxed_slice()),
|
||||
compression_threshold,
|
||||
encryption_config,
|
||||
authentication_config,
|
||||
};
|
||||
|
||||
let (mut con, login_state) = setup_connection(&config, Box::new(socket), || {
|
||||
use crate::net::protocol::state::status::{ResponseData, ResponsePlayers, ResponseVersion};
|
||||
|
||||
ResponseData {
|
||||
version: ResponseVersion {
|
||||
name: "1.16.1".to_string().into_boxed_str(),
|
||||
protocol: 736,
|
||||
},
|
||||
players: ResponsePlayers {
|
||||
max: 255,
|
||||
online: 0,
|
||||
sample: Vec::new(),
|
||||
},
|
||||
description: Chat {
|
||||
text: "Hello, world!".to_string().into_boxed_str(),
|
||||
},
|
||||
favicon: None,
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
eprintln!("Client logged in as: {:?}", login_state);
|
||||
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Goodbye!".to_string().into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
None
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// TODO: Support more features.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Chat {
|
||||
pub text: Box<str>,
|
||||
}
|
|
@ -1,419 +0,0 @@
|
|||
use crate::net::chat::Chat;
|
||||
pub use crate::net::packet_stream::CompressionThreshold;
|
||||
use crate::net::packet_stream::{Client, PacketStream, PacketStreamMaps};
|
||||
use crate::net::protocol::state::handshake;
|
||||
use crate::net::protocol::state::login;
|
||||
use crate::net::protocol::state::login::Login;
|
||||
use crate::net::protocol::state::play::Play;
|
||||
use crate::net::protocol::state::status;
|
||||
use crate::net::protocol::state::status::{ResponseData, Status};
|
||||
use crate::net::Stream;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// The configuration necessary to use Minecraft protocol encryption.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[derive(Clone)]
|
||||
pub struct EncryptionConfig {
|
||||
/// An RSA key pair. The Notchian server uses a 1024-bit key.
|
||||
/// You don't need to worry about signing it or anything like that,
|
||||
/// because it isn't ever checked for authenticity.
|
||||
///
|
||||
/// The Notchian server generates a fresh key every time the server starts.
|
||||
/// The rsa crate allows doing something similar using [`rsa::RSAPrivateKey::new`],
|
||||
/// but this is unusable because you need a DER-formatted public key for [`public_key_bytes`],
|
||||
/// and currently the rsa crate does not allow creating one.
|
||||
/// Instead, I would recommend reading a private key using [`rsa::RSAPrivateKey::from_pkcs1`].
|
||||
///
|
||||
/// You can generate a suitable private key for this purpose using OpenSSL:
|
||||
///
|
||||
/// ```bash
|
||||
/// openssl genpkey -algorithm RSA -outform der -pkeyopt rsa_keygen_bits:1024 -out priv.der
|
||||
/// ```
|
||||
pub private_key: rsa::RSAPrivateKey,
|
||||
|
||||
/// The verbatim bytes of a DER-encoded RSA public key.
|
||||
///
|
||||
/// You can generate this from your private key using OpenSSL:
|
||||
///
|
||||
/// ```bash
|
||||
/// openssl rsa -pubout -inform der -outform der -in priv.der -out pub.der
|
||||
/// ```
|
||||
///
|
||||
/// There is no reason it shouldn't be possible to extract this from the `private_key`,
|
||||
/// but the crate I use for RSA doesn't currently support it.
|
||||
pub public_key_bytes: Box<[u8]>,
|
||||
}
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
type EncryptionConfig = !;
|
||||
|
||||
/// The configuration necessary to allow the server to run in online mode.
|
||||
/// The default value will be suitable in most cases.
|
||||
#[cfg(feature = "authentication")]
|
||||
#[derive(Clone)]
|
||||
pub struct AuthenticationConfig {
|
||||
/// This is the base URL that the server will send the player's username
|
||||
/// and the shared server id hash to to check whether the user is who they claim to be.
|
||||
///
|
||||
/// There's no real need to know the technical details:
|
||||
/// if you don't know what this is, just use the default value,
|
||||
/// which will allow login using Mojang/minecraft.net accounts,
|
||||
/// which is probably what you want.
|
||||
pub has_joined_endpoint: reqwest::Url,
|
||||
|
||||
/// Enable a limited form of defense against
|
||||
/// proxied connections using the authentication server.
|
||||
/// This checks if the IP the client authenticated with
|
||||
/// is the same as the IP of the client connected to the server,
|
||||
/// and if it's not, the client is kicked.
|
||||
pub prevent_proxy_connections: bool,
|
||||
|
||||
/// Allow players to join the server even if authentication fails,
|
||||
/// treating them as though the server were in offline mode.
|
||||
/// **You probably want this to be false.**
|
||||
pub allow_unauthenticated_players: bool,
|
||||
}
|
||||
#[cfg(not(feature = "authentication"))]
|
||||
type AuthenticationConfig = !;
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
impl std::default::Default for AuthenticationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
has_joined_endpoint: reqwest::Url::parse("https://sessionserver.mojang.com/session/minecraft/hasJoined")
|
||||
.unwrap(),
|
||||
prevent_proxy_connections: false,
|
||||
allow_unauthenticated_players: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how connections with clients should be set up.
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionConfig {
|
||||
/// Enforce that one of the correct hostnames or IP addresses is used to connect to the server.
|
||||
/// This gives you flexibility as a server admin
|
||||
/// (e.g. you can update your SRV record or IP address without losing players
|
||||
/// who are connecting directly via IP, because they never count),
|
||||
/// and adds security (to prevent someone from pointing
|
||||
/// to their name and then maliciously changing it later).
|
||||
pub enforce_host: Option<Box<[Box<str>]>>,
|
||||
|
||||
/// The protocol compression theshold (i.e. the minimum packet size to compress).
|
||||
/// Setting this to `None` disables compression.
|
||||
pub compression_threshold: Option<CompressionThreshold>,
|
||||
|
||||
/// The protocol encryption configuration.
|
||||
/// Setting this to `None` disables encryption *and authentication, which requires encryption*.
|
||||
pub encryption_config: Option<EncryptionConfig>,
|
||||
|
||||
/// The authentication configuration.
|
||||
/// Setting this to `None` disables authentication.
|
||||
/// *Authentication requires encryption, so if encryption is disabled, this will be ignored.*
|
||||
pub authentication_config: Option<AuthenticationConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginState {
|
||||
name: Box<str>,
|
||||
/// None if the user is logged in but not authenticated.
|
||||
uuid: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub async fn setup_connection<F>(
|
||||
cfg: &ConnectionConfig,
|
||||
stream: Box<dyn Stream>,
|
||||
get_status: F,
|
||||
) -> Option<(PacketStream<Client, Play>, LoginState)>
|
||||
where
|
||||
F: FnOnce() -> ResponseData,
|
||||
{
|
||||
let mut con = PacketStream::new(stream);
|
||||
|
||||
let handshake::Serverbound::HandshakePkt(handshake_pkt) = con.receive().await.ok()?;
|
||||
|
||||
let mut con = match handshake_pkt.next_state {
|
||||
handshake::HandshakeNextState::Status => {
|
||||
respond_status(con.into_status(), get_status).await?;
|
||||
},
|
||||
handshake::HandshakeNextState::Login => con.into_login(),
|
||||
};
|
||||
|
||||
if let Some(hosts) = &cfg.enforce_host {
|
||||
enforce_hosts(&mut con, &hosts, &handshake_pkt.server_address).await?;
|
||||
}
|
||||
|
||||
use login::*;
|
||||
|
||||
let name = match con.receive().await.ok()? {
|
||||
Serverbound::LoginStart(LoginStart { name }) => name,
|
||||
_ => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "You're supposed to send a LoginStart packet!"
|
||||
.to_string()
|
||||
.into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let uuid = if let Some(ecfg) = &cfg.encryption_config {
|
||||
let shared_secret = enable_encryption(&mut con, &ecfg).await?;
|
||||
if let Some(acfg) = &cfg.authentication_config {
|
||||
authenticate(&mut con, &ecfg, &acfg, &name, &shared_secret).await?
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(threshold) = cfg.compression_threshold {
|
||||
con.send(&Clientbound::SetCompression(SetCompression {
|
||||
threshold: (threshold as i32).into(),
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
con.set_compression(Some(threshold));
|
||||
}
|
||||
|
||||
con.send(&Clientbound::LoginSuccess(LoginSuccess {
|
||||
uuid: uuid.unwrap_or_else(Uuid::nil),
|
||||
username: name.clone(),
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let con = con.into_play();
|
||||
|
||||
let login_state = LoginState { name, uuid };
|
||||
|
||||
Some((con, login_state))
|
||||
}
|
||||
|
||||
async fn respond_status<F>(mut con: PacketStream<Client, Status>, get_status: F) -> Option<!>
|
||||
where
|
||||
F: FnOnce() -> ResponseData,
|
||||
{
|
||||
use status::*;
|
||||
|
||||
// There's no reason theoretically that we couldn't
|
||||
// accept these packets repeated or out of order,
|
||||
// but the `setup_connection` function is supposed to return in finite time,
|
||||
// and looping here would violate that contract.
|
||||
|
||||
match con.receive().await.ok()? {
|
||||
Serverbound::Request(_) => {
|
||||
con.send(&Clientbound::Response(Response { data: get_status() }))
|
||||
.await
|
||||
.ok()?;
|
||||
},
|
||||
// That's not how the status ping is supposed to work!
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
match con.receive().await.ok()? {
|
||||
Serverbound::Ping(status::Ping { payload }) => {
|
||||
con.send(&Clientbound::Pong(Pong { payload })).await.ok()?;
|
||||
},
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
async fn enforce_hosts(con: &mut PacketStream<Client, Login>, hosts: &[Box<str>], server_address: &str) -> Option<()> {
|
||||
use login::*;
|
||||
|
||||
if hosts.iter().any(|host| host.as_ref() == server_address) {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let mut reason = "You can't connect to this server through that IP or domain.\n\
|
||||
Please use one of these instead:\n"
|
||||
.to_string();
|
||||
for host in hosts {
|
||||
reason.push_str(host);
|
||||
reason.push('\n');
|
||||
}
|
||||
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: reason.into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
async fn enable_encryption(_con: &mut PacketStream<Client, Login>, cfg: &EncryptionConfig) -> Option<Box<[u8]>> {
|
||||
*cfg
|
||||
}
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
async fn enable_encryption(con: &mut PacketStream<Client, Login>, cfg: &EncryptionConfig) -> Option<Box<[u8]>> {
|
||||
use login::*;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
use rsa::padding::PaddingScheme;
|
||||
|
||||
let mut verify_token = Vec::new();
|
||||
verify_token.resize(4, 0u8);
|
||||
OsRng.fill(verify_token.as_mut_slice());
|
||||
|
||||
con.send(&Clientbound::EncryptionRequest(EncryptionRequest {
|
||||
server_id: "".to_string().into_boxed_str(),
|
||||
public_key: cfg.public_key_bytes.clone(),
|
||||
verify_token: verify_token.clone().into_boxed_slice(),
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let shared_secret: Box<[u8]> = match con.receive().await.ok()? {
|
||||
Serverbound::EncryptionResponse(EncryptionResponse {
|
||||
shared_secret,
|
||||
verify_token: encrypted_verify_token,
|
||||
}) => {
|
||||
let decrypted_verify_token = cfg
|
||||
.private_key
|
||||
.decrypt(PaddingScheme::PKCS1v15Encrypt, &encrypted_verify_token)
|
||||
.ok()?;
|
||||
|
||||
if decrypted_verify_token != verify_token {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Verify token was not encrypted correctly.".to_string().into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
cfg.private_key
|
||||
.decrypt(PaddingScheme::PKCS1v15Encrypt, &shared_secret)
|
||||
.ok()?
|
||||
.into_boxed_slice()
|
||||
},
|
||||
_ => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "You're supposed to send an EncryptionResponse packet!"
|
||||
.to_string()
|
||||
.into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
con.enable_encryption(&shared_secret).ok()?;
|
||||
|
||||
Some(shared_secret)
|
||||
}
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct HasJoinedResponse {
|
||||
// The response also contains a signature,
|
||||
// but I don't need to check it because I use HTTPS.
|
||||
// There's also additional properties like the player's skin,
|
||||
// but I don't care about those for now.
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "authentication"))]
|
||||
async fn authenticate(
|
||||
_con: &mut PacketStream<Client, Login>,
|
||||
_ecfg: &EncryptionConfig,
|
||||
acfg: &AuthenticationConfig,
|
||||
_name: &str,
|
||||
_shared_secret: &[u8],
|
||||
) -> Option<Option<Uuid>> {
|
||||
*acfg
|
||||
}
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
async fn authenticate(
|
||||
con: &mut PacketStream<Client, Login>,
|
||||
ecfg: &EncryptionConfig,
|
||||
acfg: &AuthenticationConfig,
|
||||
name: &str,
|
||||
shared_secret: &[u8],
|
||||
) -> Option<Option<Uuid>> {
|
||||
use login::*;
|
||||
|
||||
let result = try_authenticate(ecfg, acfg, name, shared_secret).await;
|
||||
|
||||
if acfg.allow_unauthenticated_players {
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
match result {
|
||||
Some(uuid) => Some(Some(uuid)),
|
||||
None => {
|
||||
con.send(&Clientbound::Disconnect(Disconnect {
|
||||
reason: Chat {
|
||||
text: "Authentication failed.".to_string().into_boxed_str(),
|
||||
},
|
||||
}))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "authentication")]
|
||||
async fn try_authenticate(
|
||||
ecfg: &EncryptionConfig,
|
||||
acfg: &AuthenticationConfig,
|
||||
name: &str,
|
||||
shared_secret: &[u8],
|
||||
) -> Option<Uuid> {
|
||||
use num_bigint::BigInt;
|
||||
use reqwest::Client;
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
let server_hash = {
|
||||
let server_hash_bytes = {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(b"");
|
||||
hasher.update(shared_secret);
|
||||
hasher.update(ecfg.public_key_bytes.clone());
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
format!("{:x}", BigInt::from_signed_bytes_be(&server_hash_bytes)).into_boxed_str()
|
||||
};
|
||||
|
||||
// TODO: Allow checking IPs for the anti-proxy feature.
|
||||
let response_body = Client::new()
|
||||
.get(acfg.has_joined_endpoint.clone())
|
||||
.header("Content-Type", "application/json")
|
||||
.query(&[("username", name), ("serverId", &server_hash)])
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let response: HasJoinedResponse = serde_json::from_str(&response_body).ok()?;
|
||||
|
||||
Some(response.id)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
pub mod chat;
|
||||
pub mod listener;
|
||||
pub mod packet_stream;
|
||||
pub mod protocol;
|
||||
pub mod serialize;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
pub trait Stream: AsyncRead + AsyncWrite + Send + Unpin {}
|
||||
impl<S: AsyncRead + AsyncWrite + Send + Unpin> Stream for S {}
|
|
@ -1,238 +0,0 @@
|
|||
#[cfg(feature = "encryption")]
|
||||
mod encryption;
|
||||
mod packet_format;
|
||||
|
||||
use crate::net::packet_stream::packet_format::{AutoPacketFormat, PacketFormat};
|
||||
use crate::net::protocol::packet_map::PacketMap;
|
||||
use crate::net::protocol::state::handshake::Handshake;
|
||||
use crate::net::protocol::state::login::Login;
|
||||
use crate::net::protocol::state::play::Play;
|
||||
use crate::net::protocol::state::status::Status;
|
||||
use crate::net::protocol::state::ProtocolState;
|
||||
use crate::net::Stream;
|
||||
use async_trait::async_trait;
|
||||
use std::io;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Prevents outside types from implementing a trait.
|
||||
mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
use sealed::Sealed;
|
||||
|
||||
/// A remote end of a connection: either a client or server.
|
||||
///
|
||||
/// It is impossible to implement this trait for other types.
|
||||
pub trait Remote: Sealed {}
|
||||
/// The remote end of a connection is a client.
|
||||
pub enum Client {}
|
||||
/// The remote end of a connection is a server.
|
||||
pub enum Server {}
|
||||
impl Sealed for Client {}
|
||||
impl Sealed for Server {}
|
||||
impl Remote for Client {}
|
||||
impl Remote for Server {}
|
||||
|
||||
/// A packet compression theshold (i.e. the minimum packet size to compress).
|
||||
#[cfg(feature = "compression")]
|
||||
pub type CompressionThreshold = usize;
|
||||
#[cfg(not(feature = "compression"))]
|
||||
pub type CompressionThreshold = !;
|
||||
|
||||
/// A shared secret used for packet encryption.
|
||||
#[cfg(feature = "encryption")]
|
||||
pub type SharedSecret = [u8];
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
pub type SharedSecret = !;
|
||||
|
||||
/// A stream of packets.
|
||||
///
|
||||
/// The type parameters are used to ensure using the type system
|
||||
/// that you can only send and receive the correct type of packets.
|
||||
pub struct PacketStream<Rem: Remote, St: ProtocolState> {
|
||||
inner: Box<dyn Stream>,
|
||||
compression_threshold: Option<CompressionThreshold>,
|
||||
encryption_enabled: bool,
|
||||
remote: PhantomData<Rem>,
|
||||
state: PhantomData<St>,
|
||||
}
|
||||
|
||||
impl<Rem: Remote, St: ProtocolState> PacketStream<Rem, St> {
|
||||
/// Coerce the packet stream to any protocol state of your choosing.
|
||||
/// This can be used to break PacketStream's type-enforced correctness guarantees,
|
||||
/// so it should only be used internally.
|
||||
///
|
||||
/// The purpose of this function is to reduce code duplication
|
||||
/// for all the pre-defined safe protocol state transitions.
|
||||
fn into_state<NewSt: ProtocolState>(self) -> PacketStream<Rem, NewSt> {
|
||||
PacketStream {
|
||||
inner: self.inner,
|
||||
compression_threshold: self.compression_threshold,
|
||||
encryption_enabled: self.encryption_enabled,
|
||||
remote: self.remote,
|
||||
state: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send any packet over the stream, ignoring the declared ProtocolState.
|
||||
/// This can be used to break PacketStream's type-enforced correctness guarantees,
|
||||
/// so it should only be used internally.
|
||||
///
|
||||
/// The purpose of this function is to reduce code duplication
|
||||
/// while implementing [`PacketStreamMaps`]'s send and receive;
|
||||
/// the sending and recieving code is always going to be the same,
|
||||
/// but due to some shortcomings of Rust's type system
|
||||
/// (specifically, because there's no way to approximate type-level functions;
|
||||
/// in Haskell you could use multi-parameter type classes or type families),
|
||||
/// we can't implement this generically safely over Remote/ProtocolState combinations.
|
||||
async fn send_generic<Pkt: PacketMap>(&mut self, pkt: &Pkt) -> io::Result<()> {
|
||||
let mut contents = Vec::new();
|
||||
pkt.write(&mut contents);
|
||||
|
||||
AutoPacketFormat(self.compression_threshold)
|
||||
.send(&mut self.inner, &contents)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Receive any packet from the stream, ignoring the declared ProtocolState.
|
||||
/// This can be used to break PacketStream's type-enforced correctness guarantees,
|
||||
/// so it should only be used internally.
|
||||
///
|
||||
/// The purpose of this function is to reduce code duplication
|
||||
/// while implementing [`PacketStreamMaps`]'s send and receive;
|
||||
/// the sending and recieving code is always going to be the same,
|
||||
/// but due to some shortcomings of Rust's type system
|
||||
/// (specifically, because there's no way to approximate type-level functions;
|
||||
/// in Haskell you could use multi-parameter type classes or type families),
|
||||
/// we can't implement this generically safely over Remote/ProtocolState combinations.
|
||||
async fn receive_generic<Pkt: PacketMap>(&mut self) -> io::Result<Pkt> {
|
||||
use crate::net::serialize::VecPacketDeserializer;
|
||||
|
||||
let buf = AutoPacketFormat(self.compression_threshold)
|
||||
.receive(&mut self.inner)
|
||||
.await?;
|
||||
|
||||
Pkt::read(&mut VecPacketDeserializer::new(buf.as_ref()))
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
|
||||
}
|
||||
|
||||
/// Transition to the protocol disconnected state,
|
||||
/// which prevents reading or writing any more packets.
|
||||
#[allow(dead_code)]
|
||||
pub fn into_disconnected(self) -> PacketStream<Rem, !> {
|
||||
self.into_state()
|
||||
}
|
||||
}
|
||||
|
||||
/// A valid combination of inbound and outbound packet maps,
|
||||
/// which are determined by the protocol state and remote.
|
||||
#[async_trait]
|
||||
pub trait PacketStreamMaps: Sealed {
|
||||
/// The kind of packets that can be received from the remote.
|
||||
type Inbound: PacketMap;
|
||||
/// The kind of packets that can be sent to the remote.
|
||||
type Outbound: PacketMap;
|
||||
|
||||
/// Receive a packet from the remote.
|
||||
async fn receive(&mut self) -> io::Result<Self::Inbound>;
|
||||
|
||||
/// Send a packet to the remote.
|
||||
async fn send(&mut self, pkt: &Self::Outbound) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl<St: ProtocolState> Sealed for PacketStream<Client, St> {}
|
||||
#[async_trait]
|
||||
impl<St: ProtocolState> PacketStreamMaps for PacketStream<Client, St> {
|
||||
type Inbound = St::Serverbound;
|
||||
type Outbound = St::Clientbound;
|
||||
|
||||
async fn receive(&mut self) -> io::Result<Self::Inbound> {
|
||||
self.receive_generic().await
|
||||
}
|
||||
|
||||
async fn send(&mut self, pkt: &Self::Outbound) -> io::Result<()> {
|
||||
self.send_generic(pkt).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<St: ProtocolState> Sealed for PacketStream<Server, St> {}
|
||||
#[async_trait]
|
||||
impl<St: ProtocolState> PacketStreamMaps for PacketStream<Server, St> {
|
||||
type Inbound = St::Clientbound;
|
||||
type Outbound = St::Serverbound;
|
||||
|
||||
async fn receive(&mut self) -> io::Result<Self::Inbound> {
|
||||
self.receive_generic().await
|
||||
}
|
||||
|
||||
async fn send(&mut self, pkt: &Self::Outbound) -> io::Result<()> {
|
||||
self.send_generic(pkt).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rem: Remote> PacketStream<Rem, Handshake> {
|
||||
pub fn new(inner: Box<dyn Stream>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
compression_threshold: None,
|
||||
encryption_enabled: false,
|
||||
remote: PhantomData,
|
||||
state: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Transition to the protocol status state.
|
||||
pub fn into_status(self) -> PacketStream<Rem, Status> {
|
||||
self.into_state()
|
||||
}
|
||||
|
||||
/// Transition to the protocol login state.
|
||||
pub fn into_login(self) -> PacketStream<Rem, Login> {
|
||||
self.into_state()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
pub type InvalidKeyNonceLength = cfb8::cipher::stream::InvalidKeyNonceLength;
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
pub type InvalidKeyNonceLength = !;
|
||||
|
||||
impl<Rem: Remote> PacketStream<Rem, Login> {
|
||||
/// Transition to the protocol play state.
|
||||
pub fn into_play(self) -> PacketStream<Rem, Play> {
|
||||
self.into_state()
|
||||
}
|
||||
|
||||
/// Set the compression threshold, i.e. the minimum size of packet to compress.
|
||||
/// Setting the compression threshold to None disables compression.
|
||||
pub fn set_compression(&mut self, threshold: Option<CompressionThreshold>) {
|
||||
self.compression_threshold = threshold;
|
||||
}
|
||||
|
||||
// I want to leave the function available even when encryption is disabled
|
||||
// so that users of PacketStream can use the `Option<SharedSecret>` pattern
|
||||
// to support encryption configuration without needing config features everywhere.
|
||||
|
||||
/// Sets the shared secret, which enables encryption.
|
||||
/// Trying to enable encryption twice is an error, and the function will panic.
|
||||
pub fn enable_encryption(&mut self, shared_secret: &SharedSecret) -> Result<(), InvalidKeyNonceLength> {
|
||||
if self.encryption_enabled {
|
||||
panic!("Tried to enable encryption twice!");
|
||||
}
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
{
|
||||
use crate::net::packet_stream::encryption::EncryptedStream;
|
||||
use cfb8::cipher::stream::NewStreamCipher;
|
||||
use cfb8::Cfb8;
|
||||
|
||||
let cipher: Cfb8<aes::Aes128> = Cfb8::new_var(shared_secret, shared_secret)?;
|
||||
take_mut::take(&mut self.inner, |inner| Box::new(EncryptedStream::new(inner, cipher)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
*shared_secret
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
use crate::net::Stream;
|
||||
use aes::Aes128;
|
||||
use cfb8::cipher::stream::StreamCipher;
|
||||
use cfb8::Cfb8;
|
||||
use std::pin::Pin;
|
||||
use std::task::Context;
|
||||
use std::task::Poll;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, Result};
|
||||
|
||||
pub struct EncryptedStream {
|
||||
rw: Box<dyn Stream>,
|
||||
cipher: Cfb8<Aes128>,
|
||||
write_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EncryptedStream {
|
||||
pub fn new(rw: Box<dyn Stream>, cipher: Cfb8<Aes128>) -> Self {
|
||||
Self {
|
||||
rw,
|
||||
cipher,
|
||||
write_buf: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self, cx: &mut Context) -> Poll<Result<()>> {
|
||||
// We don't know when the internal writer will be ready,
|
||||
// so we have to coax it into returning "pending" and scheduling an interrupt
|
||||
// for us. Either that, or we finish writing our buffer and flush.
|
||||
loop {
|
||||
if self.write_buf.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.rw).poll_write(cx, &self.write_buf) {
|
||||
Poll::Ready(Ok(length)) => {
|
||||
let mut new_buf = Vec::new();
|
||||
new_buf.copy_from_slice(&self.write_buf[length..]);
|
||||
self.write_buf = new_buf;
|
||||
},
|
||||
other => return other.map(|x| x.map(|_| ())),
|
||||
}
|
||||
}
|
||||
|
||||
Pin::new(&mut self.rw).poll_flush(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for EncryptedStream {
|
||||
fn poll_read(self: Pin<&mut Self>, cx: &mut Context, buf: &mut ReadBuf) -> Poll<Result<()>> {
|
||||
let me = Pin::into_inner(self);
|
||||
|
||||
match Pin::new(&mut me.rw).poll_read(cx, buf) {
|
||||
Poll::Ready(Ok(())) => {
|
||||
me.cipher.decrypt(buf.filled_mut());
|
||||
Poll::Ready(Ok(()))
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for EncryptedStream {
|
||||
fn poll_write(self: Pin<&mut Self>, _cx: &mut Context, buf: &[u8]) -> Poll<Result<usize>> {
|
||||
let me = Pin::into_inner(self);
|
||||
|
||||
let index = me.write_buf.len();
|
||||
// Copy data to our write buffer and then encrypt it.
|
||||
me.write_buf.extend_from_slice(buf);
|
||||
me.cipher.encrypt(&mut me.write_buf[index..]);
|
||||
|
||||
Poll::Ready(Ok(buf.len()))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<()>> {
|
||||
Pin::into_inner(self).flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<()>> {
|
||||
let me = Pin::into_inner(self);
|
||||
|
||||
match me.flush(cx) {
|
||||
Poll::Ready(Ok(())) => Pin::new(&mut me.rw).poll_shutdown(cx),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
#[cfg(feature = "compression")]
|
||||
mod compressed;
|
||||
mod default;
|
||||
|
||||
use crate::net::packet_stream::packet_format::default::DefaultPacketFormat;
|
||||
use crate::net::packet_stream::CompressionThreshold;
|
||||
use async_trait::async_trait;
|
||||
use std::io;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
||||
|
||||
pub type Reader = dyn AsyncRead + Unpin + Send;
|
||||
pub type Writer = dyn AsyncWrite + Unpin + Send;
|
||||
|
||||
/// A packet format describes how to read a packet header and retrieve its data.
|
||||
#[async_trait]
|
||||
pub trait PacketFormat: Send + Sync {
|
||||
/// Receive the bytes of a packet's body (its id and header) from the provided stream.
|
||||
/// This involves reading the packet header and performing decompression if necessary.
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>>;
|
||||
|
||||
/// Send the bytes of a packet's body (its id and header) through the provided stream.
|
||||
/// This involves writing the packet header and performing compression if necessary.
|
||||
async fn send(&self, dest: &mut Writer, data: &[u8]) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub struct AutoPacketFormat(pub Option<CompressionThreshold>);
|
||||
|
||||
#[async_trait]
|
||||
impl PacketFormat for AutoPacketFormat {
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
match self.0 {
|
||||
#[cfg(not(feature = "compression"))]
|
||||
Some(x) => x,
|
||||
#[cfg(feature = "compression")]
|
||||
Some(threshold) => {
|
||||
use crate::net::packet_stream::packet_format::compressed::CompressedPacketFormat;
|
||||
CompressedPacketFormat(threshold).receive(src).await
|
||||
},
|
||||
None => DefaultPacketFormat.receive(src).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(&self, dest: &mut Writer, data: &[u8]) -> io::Result<()> {
|
||||
match self.0 {
|
||||
#[cfg(not(feature = "compression"))]
|
||||
Some(x) => x,
|
||||
#[cfg(feature = "compression")]
|
||||
Some(threshold) => {
|
||||
use crate::net::packet_stream::packet_format::compressed::CompressedPacketFormat;
|
||||
CompressedPacketFormat(threshold).send(dest, data).await
|
||||
},
|
||||
None => DefaultPacketFormat.send(dest, data).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A completely arbitrary limitation on the maximum size of a received packet.
|
||||
pub const MAX_PACKET_SIZE: usize = 35565;
|
||||
|
||||
async fn read_varint(src: &mut Reader) -> io::Result<(usize, i32)> {
|
||||
let mut num_read: usize = 0;
|
||||
let mut acc = 0;
|
||||
while num_read < 5 {
|
||||
let byte = src.read_u8().await?;
|
||||
acc |= ((byte & 0b01111111) as i32) << (num_read * 7);
|
||||
|
||||
num_read += 1;
|
||||
|
||||
if byte & 0b10000000 == 0 {
|
||||
return Ok((num_read, acc));
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "VarInt was too long.".to_string()))
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
use crate::net::packet_stream::packet_format::{read_varint, PacketFormat, Reader, Writer, MAX_PACKET_SIZE};
|
||||
use async_trait::async_trait;
|
||||
use std::boxed::Box;
|
||||
use std::io;
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct CompressedPacketFormat(pub usize);
|
||||
|
||||
// A compressed header is in this format:
|
||||
//
|
||||
// packet_length: VarInt
|
||||
// uncompressed_length: VarInt
|
||||
// data: [u8]
|
||||
//
|
||||
// The packet length is the size of the entire packet in bytes,
|
||||
// including the uncompressed length.
|
||||
// The uncompressed length is the size of the uncompressed data in bytes,
|
||||
// or if it is zero, indicates that the data is not compressed.
|
||||
// This is followed by the data, either compressed or uncompressed.
|
||||
|
||||
#[async_trait]
|
||||
impl PacketFormat for CompressedPacketFormat {
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
// First we read in the packet and uncompressed data lengths.
|
||||
let (_, packet_length) = read_varint(src).await?;
|
||||
if packet_length < 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Packet length was negative."));
|
||||
}
|
||||
if packet_length > MAX_PACKET_SIZE as i32 {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Packet was too long."));
|
||||
}
|
||||
let packet_length = packet_length as usize;
|
||||
|
||||
let (data_length_size, data_length) = read_varint(src).await?;
|
||||
if data_length < 0 {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Data length was negative."));
|
||||
}
|
||||
if data_length > MAX_PACKET_SIZE as i32 {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Data was too long."));
|
||||
}
|
||||
let data_length = data_length as usize;
|
||||
|
||||
// Now we receive the remainder of the packet's data.
|
||||
let mut data = Vec::with_capacity(packet_length - data_length_size);
|
||||
data.resize(packet_length, 0);
|
||||
src.read_exact(data.as_mut_slice()).await?;
|
||||
|
||||
// If the data was not compressed, we simply return it.
|
||||
if data_length == 0 {
|
||||
return Ok(data.into_boxed_slice());
|
||||
}
|
||||
|
||||
// Otherwise, we decompress it.
|
||||
let mut decompressed = Vec::new();
|
||||
decompressed.resize(data_length, 0);
|
||||
|
||||
use flate2::{Decompress, FlushDecompress};
|
||||
Decompress::new(true)
|
||||
.decompress(&data, decompressed.as_mut_slice(), FlushDecompress::Finish)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(decompressed.into_boxed_slice())
|
||||
}
|
||||
|
||||
async fn send(&self, dest: &mut Writer, data: &[u8]) -> io::Result<()> {
|
||||
use crate::net::serialize::{PacketSerializer, VarInt};
|
||||
|
||||
// If the length of the uncompressed data exceeds the threshold,
|
||||
// then we will compress this packet.
|
||||
if data.len() >= self.0 {
|
||||
// Now we compress the data.
|
||||
use flate2::{Compress, FlushCompress};
|
||||
|
||||
// 1024 is just an arbitrary amount of extra space reserved
|
||||
// in case the output data ends up larger than the input data
|
||||
// (e.g. due to the zlib header).
|
||||
// FIXME: Further research to figure out the exact maximum capacity necessary.
|
||||
// Perhaps you only need space for the header and the data itself can't get
|
||||
// bigger? And what is the limit to how much bigger the data will
|
||||
// get? Currently I don't actually know for a fact that this won't
|
||||
// ever drop data.
|
||||
let mut compressed = Vec::with_capacity(1024 + data.len());
|
||||
Compress::new(flate2::Compression::best(), true)
|
||||
.compress_vec(data, &mut compressed, FlushCompress::Finish)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
// Since the packet is compressed,
|
||||
// data_length will be the length of the uncompressed data.
|
||||
let mut data_length_buf = Vec::with_capacity(5);
|
||||
data_length_buf.write(VarInt(data.len() as i32));
|
||||
|
||||
let mut packet_length_buf = Vec::with_capacity(5);
|
||||
packet_length_buf.write(VarInt((data_length_buf.len() + compressed.len()) as i32));
|
||||
|
||||
{
|
||||
// I have to keep this import in a block so that
|
||||
// it won't conflict with PacketSerialize::write.
|
||||
use tokio::io::AsyncWriteExt;
|
||||
dest.write(packet_length_buf.as_slice()).await?;
|
||||
dest.write(data_length_buf.as_slice()).await?;
|
||||
dest.write(compressed.as_slice()).await?;
|
||||
dest.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
// Since the packet is uncompressed,
|
||||
// the packet length is just the length of the data plus the data_length,
|
||||
// which will just be 0x00 (1 byte long) because the data isn't compressed.
|
||||
let mut packet_length_buf = Vec::with_capacity(5);
|
||||
packet_length_buf.write(VarInt(data.len() as i32 + 1));
|
||||
|
||||
{
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
dest.write(packet_length_buf.as_slice()).await?;
|
||||
dest.write_u8(0x00).await?;
|
||||
dest.write(data).await?;
|
||||
dest.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
use crate::net::packet_stream::packet_format::{read_varint, PacketFormat, Reader, Writer, MAX_PACKET_SIZE};
|
||||
use async_trait::async_trait;
|
||||
use std::boxed::Box;
|
||||
use std::io;
|
||||
|
||||
pub struct DefaultPacketFormat;
|
||||
|
||||
#[async_trait]
|
||||
impl PacketFormat for DefaultPacketFormat {
|
||||
async fn receive(&self, src: &mut Reader) -> io::Result<Box<[u8]>> {
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
let (_, length) = read_varint(src).await?;
|
||||
if length > MAX_PACKET_SIZE as i32 {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Packet was too long.".to_string()));
|
||||
}
|
||||
|
||||
let mut buf = vec![0; length as usize];
|
||||
src.read_exact(buf.as_mut_slice()).await?;
|
||||
|
||||
Ok(buf.into_boxed_slice())
|
||||
}
|
||||
|
||||
async fn send(&self, dest: &mut Writer, data: &[u8]) -> io::Result<()> {
|
||||
use crate::net::serialize::{PacketSerializer, VarInt};
|
||||
|
||||
let mut packet_length_buf = Vec::with_capacity(5);
|
||||
packet_length_buf.write(VarInt(data.len() as i32));
|
||||
|
||||
{
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
dest.write(packet_length_buf.as_slice()).await?;
|
||||
dest.write(data).await?;
|
||||
dest.flush().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
pub mod packet;
|
||||
pub mod packet_map;
|
||||
pub mod state;
|
|
@ -1,35 +0,0 @@
|
|||
#[macro_export]
|
||||
macro_rules! define_packets {
|
||||
{ $( packet $name:ident { $( $field:ident : $type:ty ),* } )+ } => {
|
||||
$(
|
||||
pub struct $name {
|
||||
$(
|
||||
pub $field: $type,
|
||||
)*
|
||||
}
|
||||
|
||||
impl crate::net::serialize::PacketReadable for $name {
|
||||
fn read(deser: &mut impl crate::net::serialize::PacketDeserializer) -> Result<Self, String> {
|
||||
$(
|
||||
let $field = deser.read::<$type>()?;
|
||||
)*
|
||||
deser.read_eof()?;
|
||||
Ok($name {
|
||||
$(
|
||||
$field,
|
||||
)*
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::net::serialize::PacketWritable for &$name {
|
||||
fn write(self, ser: &mut impl crate::net::serialize::PacketSerializer) {
|
||||
$(
|
||||
self.$field.write(ser);
|
||||
)*
|
||||
ser.write_eof();
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
use crate::net::serialize::{PacketDeserializer, PacketSerializer};
|
||||
|
||||
pub trait PacketMap: Sized + Sync {
|
||||
/// Read a packet from the deserializer.
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String>;
|
||||
/// Write this packet's data to the serializer.
|
||||
fn write(&self, ser: &mut impl PacketSerializer);
|
||||
}
|
||||
|
||||
impl PacketMap for ! {
|
||||
fn read(_deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
Err("Cannot read packets; the connection state is disconnected.".to_string())
|
||||
}
|
||||
|
||||
fn write(&self, _ser: &mut impl PacketSerializer) {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! define_packet_maps {
|
||||
{ $( packet_map $name:ident { $( $id:expr => $packet:ident ),* } )+ } => {
|
||||
$(
|
||||
pub enum $name {
|
||||
$( $packet($packet) ),*
|
||||
}
|
||||
|
||||
impl crate::net::protocol::packet_map::PacketMap for $name {
|
||||
#[allow(unused_variables)]
|
||||
fn read(deser: &mut impl crate::net::serialize::PacketDeserializer)
|
||||
-> Result<Self, String> {
|
||||
let id: i32 = deser.read::<crate::net::serialize::VarInt>()?.into();
|
||||
match id {
|
||||
$( $id => deser.read::<$packet>().map($name::$packet), )*
|
||||
id => Err(format!("Invalid packet id: {}", id))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn write(&self, ser: &mut impl crate::net::serialize::PacketSerializer) {
|
||||
match *self {
|
||||
$( $name::$packet(ref pkt) => {
|
||||
ser.write(crate::net::serialize::VarInt($id));
|
||||
ser.write::<&$packet>(&pkt);
|
||||
} ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
pub mod handshake;
|
||||
pub mod login;
|
||||
pub mod play;
|
||||
pub mod status;
|
||||
|
||||
use crate::net::protocol::packet_map::PacketMap;
|
||||
|
||||
pub trait ProtocolState: Send + Sync {
|
||||
type Clientbound: PacketMap;
|
||||
type Serverbound: PacketMap;
|
||||
}
|
||||
|
||||
impl ProtocolState for ! {
|
||||
type Clientbound = !;
|
||||
type Serverbound = !;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! define_state {
|
||||
( $name:ident , $cb:ty , $sb:ty ) => {
|
||||
#[allow(dead_code)]
|
||||
pub enum $name {}
|
||||
|
||||
impl crate::net::protocol::state::ProtocolState for $name {
|
||||
type Clientbound = $cb;
|
||||
type Serverbound = $sb;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
use crate::net::serialize::{PacketDeserializer, PacketReadable, PacketSerializer, PacketWritable, VarInt};
|
||||
use crate::{define_packet_maps, define_packets, define_state};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum HandshakeNextState {
|
||||
Status,
|
||||
Login,
|
||||
}
|
||||
|
||||
impl PacketReadable for HandshakeNextState {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
use HandshakeNextState::*;
|
||||
|
||||
Ok(match deser.read::<VarInt>()?.into() {
|
||||
1 => Status,
|
||||
2 => Login,
|
||||
n => return Err(format!("Invalid next protocol state in handshake: {}", n)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &HandshakeNextState {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
use HandshakeNextState::*;
|
||||
|
||||
ser.write(VarInt(match self {
|
||||
Status => 1,
|
||||
Login => 2,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
define_packets! {
|
||||
packet HandshakePkt {
|
||||
protocol_version: VarInt,
|
||||
server_address: Box<str>,
|
||||
server_port: u16,
|
||||
next_state: HandshakeNextState
|
||||
}
|
||||
}
|
||||
|
||||
define_packet_maps! {
|
||||
packet_map Clientbound { }
|
||||
|
||||
packet_map Serverbound {
|
||||
0x00 => HandshakePkt
|
||||
}
|
||||
}
|
||||
|
||||
define_state!(Handshake, Clientbound, Serverbound);
|
|
@ -1,69 +0,0 @@
|
|||
use crate::net::chat::Chat;
|
||||
use crate::net::serialize::{Rest, VarInt};
|
||||
use crate::{define_packet_maps, define_packets, define_state};
|
||||
use uuid::Uuid;
|
||||
|
||||
define_packets! {
|
||||
// Clientbound
|
||||
|
||||
packet Disconnect {
|
||||
reason: Chat
|
||||
}
|
||||
|
||||
packet EncryptionRequest {
|
||||
server_id: Box<str>,
|
||||
public_key: Box<[u8]>,
|
||||
verify_token: Box<[u8]>
|
||||
}
|
||||
|
||||
packet LoginSuccess {
|
||||
uuid: Uuid,
|
||||
username: Box<str>
|
||||
}
|
||||
|
||||
packet SetCompression {
|
||||
threshold: VarInt
|
||||
}
|
||||
|
||||
packet LoginPluginRequest {
|
||||
message_id: VarInt,
|
||||
// FIXME: Actually an Identifier.
|
||||
channel: String,
|
||||
data: Rest
|
||||
}
|
||||
|
||||
// Serverbound
|
||||
|
||||
packet LoginStart {
|
||||
name: Box<str>
|
||||
}
|
||||
|
||||
packet EncryptionResponse {
|
||||
shared_secret: Box<[u8]>,
|
||||
verify_token: Box<[u8]>
|
||||
}
|
||||
|
||||
packet LoginPluginResponse {
|
||||
message_id: VarInt,
|
||||
successful: bool,
|
||||
data: Rest
|
||||
}
|
||||
}
|
||||
|
||||
define_packet_maps! {
|
||||
packet_map Clientbound {
|
||||
0x00 => Disconnect,
|
||||
0x01 => EncryptionRequest,
|
||||
0x02 => LoginSuccess,
|
||||
0x03 => SetCompression,
|
||||
0x04 => LoginPluginRequest
|
||||
}
|
||||
|
||||
packet_map Serverbound {
|
||||
0x00 => LoginStart,
|
||||
0x01 => EncryptionResponse,
|
||||
0x02 => LoginPluginResponse
|
||||
}
|
||||
}
|
||||
|
||||
define_state!(Login, Clientbound, Serverbound);
|
|
@ -1,22 +0,0 @@
|
|||
use crate::net::chat::Chat;
|
||||
use crate::{define_packet_maps, define_packets, define_state};
|
||||
|
||||
// TODO: This protocol state isn't even close to entirely mapped.
|
||||
|
||||
define_packets! {
|
||||
packet Disconnect {
|
||||
reason: Chat
|
||||
}
|
||||
}
|
||||
|
||||
define_packet_maps! {
|
||||
packet_map Clientbound {
|
||||
0x1a => Disconnect
|
||||
}
|
||||
|
||||
packet_map Serverbound {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
define_state!(Play, Clientbound, Serverbound);
|
|
@ -1,71 +0,0 @@
|
|||
use crate::net::chat::Chat;
|
||||
use crate::net::serialize::PacketJson;
|
||||
use crate::{define_packet_maps, define_packets, define_state};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ResponseVersion {
|
||||
pub name: Box<str>,
|
||||
pub protocol: u32,
|
||||
}
|
||||
impl PacketJson for ResponseVersion {}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ResponsePlayersSample {
|
||||
pub name: Box<str>,
|
||||
pub id: Uuid,
|
||||
}
|
||||
impl PacketJson for ResponsePlayersSample {}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ResponsePlayers {
|
||||
pub max: u32,
|
||||
pub online: u32,
|
||||
pub sample: Vec<ResponsePlayersSample>,
|
||||
}
|
||||
impl PacketJson for ResponsePlayers {}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct ResponseData {
|
||||
pub version: ResponseVersion,
|
||||
pub players: ResponsePlayers,
|
||||
pub description: Chat,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub favicon: Option<Box<str>>,
|
||||
}
|
||||
impl PacketJson for ResponseData {}
|
||||
|
||||
define_packets! {
|
||||
// Clientbound
|
||||
|
||||
packet Response {
|
||||
data: ResponseData
|
||||
}
|
||||
|
||||
packet Pong {
|
||||
payload: [u8; 8]
|
||||
}
|
||||
|
||||
// Serverbound
|
||||
|
||||
packet Request { }
|
||||
|
||||
packet Ping {
|
||||
payload: [u8; 8]
|
||||
}
|
||||
}
|
||||
|
||||
define_packet_maps! {
|
||||
packet_map Clientbound {
|
||||
0x00 => Response,
|
||||
0x01 => Pong
|
||||
}
|
||||
|
||||
packet_map Serverbound {
|
||||
0x00 => Request,
|
||||
0x01 => Ping
|
||||
}
|
||||
}
|
||||
|
||||
define_state!(Status, Clientbound, Serverbound);
|
|
@ -1,320 +0,0 @@
|
|||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::borrow::Borrow;
|
||||
use std::convert::{From, Into};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub trait PacketSerializer: Sized {
|
||||
/// Write a slice of bytes directly, without a length prefix.
|
||||
fn write_exact(&mut self, value: &[u8]);
|
||||
fn write_eof(&mut self);
|
||||
|
||||
fn write<D: PacketWritable>(&mut self, value: D) {
|
||||
value.write(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketSerializer for Vec<u8> {
|
||||
fn write_exact(&mut self, value: &[u8]) {
|
||||
self.extend_from_slice(value);
|
||||
}
|
||||
|
||||
fn write_eof(&mut self) {}
|
||||
}
|
||||
|
||||
pub trait PacketDeserializer: Sized {
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), String>;
|
||||
fn read_rest(&mut self) -> Result<Box<[u8]>, String>;
|
||||
fn read_eof(&mut self) -> Result<(), String>;
|
||||
|
||||
fn read<D: PacketReadable>(&mut self) -> Result<D, String> {
|
||||
D::read(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VecPacketDeserializer<'a> {
|
||||
data: &'a [u8],
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl VecPacketDeserializer<'_> {
|
||||
pub fn new(data: &[u8]) -> VecPacketDeserializer<'_> {
|
||||
VecPacketDeserializer { data, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketDeserializer for VecPacketDeserializer<'_> {
|
||||
fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), String> {
|
||||
if self.index + buf.len() > self.data.len() {
|
||||
return Err("Tried to read past length of packet.".to_string());
|
||||
}
|
||||
|
||||
let len = buf.len();
|
||||
buf[..].copy_from_slice(&self.data[self.index..self.index + len]);
|
||||
self.index += buf.len();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_rest(&mut self) -> Result<Box<[u8]>, String> {
|
||||
let mut it = Vec::new();
|
||||
it.copy_from_slice(&self.data[self.index..]);
|
||||
self.index = self.data.len();
|
||||
Ok(it.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn read_eof(&mut self) -> Result<(), String> {
|
||||
if self.index != self.data.len() {
|
||||
return Err("Packet contained more data than necessary.".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PacketReadable: Sized {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String>;
|
||||
}
|
||||
|
||||
pub trait PacketWritable {
|
||||
fn write(self, ser: &mut impl PacketSerializer);
|
||||
}
|
||||
|
||||
pub trait PacketData: PacketReadable + PacketWritable {}
|
||||
impl<T: PacketReadable + PacketWritable> PacketData for T {}
|
||||
|
||||
impl PacketReadable for bool {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
let value = deser.read::<u8>()?;
|
||||
match value {
|
||||
0x00 => Ok(false),
|
||||
0x01 => Ok(true),
|
||||
n => Err(format!("{:0X} is not a valid boolean.", n)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for bool {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(self as u8);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_packet_data_for_num {
|
||||
( $( $num:ty, $len:expr );+ ) => {
|
||||
$(
|
||||
impl PacketReadable for $num {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
deser.read::<[u8; $len]>().map(Self::from_be_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for $num {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(self.to_be_bytes())
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
impl_packet_data_for_num!(u8, 1; i8, 1; u16, 2; i16, 2; u32, 4; i32, 4; u64, 8; i64, 8; u128, 16;
|
||||
f32, 4; f64, 8);
|
||||
|
||||
// HACK: There is probably a better solution to this than a macro.
|
||||
// Same goes for the above, but to a lesser degree.
|
||||
macro_rules! impl_varnum {
|
||||
( $( $name:ident, $wraps:ty, $length:expr);+ ) => {
|
||||
$(
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct $name(pub $wraps);
|
||||
|
||||
impl From<$wraps> for $name {
|
||||
fn from(x: $wraps) -> Self { $name(x) }
|
||||
}
|
||||
|
||||
impl From<$name> for $wraps {
|
||||
fn from(x: $name) -> Self { x.0 }
|
||||
}
|
||||
|
||||
impl PacketReadable for $name {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
let mut num_read: usize = 0;
|
||||
let mut acc = 0;
|
||||
while num_read < $length {
|
||||
// If the highest bit is set, there are further bytes to be read;
|
||||
// the rest of the bits are the actual bits of the number.
|
||||
let read = deser.read::<u8>()?;
|
||||
acc |= ((read & 0b01111111) as $wraps) << num_read * 7;
|
||||
|
||||
num_read += 1;
|
||||
|
||||
if (read & 0b10000000) == 0 {
|
||||
// There are no more bytes.
|
||||
return Ok($name(acc));
|
||||
}
|
||||
|
||||
// Make space for the rest of the bits.
|
||||
acc <<= 7;
|
||||
}
|
||||
|
||||
Err(format!("VarNum was more than {} bytes.", $length))
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for $name {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
let mut value = self.0;
|
||||
loop {
|
||||
let mut temp = (value & 0b01111111) as u8;
|
||||
value >>= 7;
|
||||
if value != 0 {
|
||||
temp |= 0b10000000;
|
||||
}
|
||||
ser.write(temp);
|
||||
|
||||
if value == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
impl_varnum!(VarInt, i32, 5; VarLong, i64, 10);
|
||||
|
||||
impl PacketWritable for &[u8] {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(VarInt(self.len() as i32));
|
||||
ser.write_exact(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketReadable for Vec<u8> {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
let length: i32 = deser.read::<VarInt>()?.into();
|
||||
if length < 0 {
|
||||
return Err("Array or string length cannot be negative.".to_string());
|
||||
}
|
||||
|
||||
let mut it = vec![0; length as usize];
|
||||
deser.read_exact(it.as_mut_slice())?;
|
||||
|
||||
Ok(it)
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &Vec<u8> {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(self.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketReadable for Box<[u8]> {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
deser.read::<Vec<u8>>().map(|x| x.into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &Box<[u8]> {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write::<&[u8]>(self.borrow());
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rest(pub Box<[u8]>);
|
||||
|
||||
impl PacketReadable for Rest {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
deser.read_rest().map(Rest)
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &Rest {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &str {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(self.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketReadable for String {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
let bytes = deser.read()?;
|
||||
String::from_utf8(bytes).map_err(|_| "String contained invalid UTF-8.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &String {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write::<&str>(self.borrow());
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketReadable for Box<str> {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
deser.read::<String>().map(|x| x.into_boxed_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &Box<str> {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write::<&str>(self.borrow());
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketReadable for Uuid {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
deser.read::<u128>().map(Uuid::from_u128)
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketWritable for &Uuid {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(self.as_u128());
|
||||
}
|
||||
}
|
||||
|
||||
/// A marker trait indicating that a JSON-serialiable type should be serialized
|
||||
/// as JSON in packets. Most primitive types are already serializable as JSON,
|
||||
/// but we explicitly *don't* want to serialize them as JSON in packets.
|
||||
pub trait PacketJson {}
|
||||
|
||||
impl PacketJson for crate::net::chat::Chat {}
|
||||
|
||||
impl<S: DeserializeOwned + PacketJson + Sized> PacketReadable for S {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
let bytes = deser.read::<Vec<u8>>()?;
|
||||
serde_json::from_slice(&bytes).map_err(|_| "Bad JSON syntax".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: PacketJson + Serialize> PacketWritable for &S {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write(&serde_json::to_vec(self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
// Although according to my organizational scheme, this should go first,
|
||||
// it goes last anyway because constant generics break Atom's syntax
|
||||
// highlighting for all code below it.
|
||||
|
||||
impl<const N: usize> PacketReadable for [u8; N] {
|
||||
fn read(deser: &mut impl PacketDeserializer) -> Result<Self, String> {
|
||||
let mut buf = [0; N];
|
||||
deser.read_exact(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> PacketWritable for [u8; N] {
|
||||
fn write(self, ser: &mut impl PacketSerializer) {
|
||||
ser.write_exact(&self);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
resolver: lts-14.4
|
||||
|
||||
packages:
|
||||
- .
|
||||
|
||||
extra-deps:
|
||||
- free-algebras-0.0.7.2@sha256:3796d89793727177f5e0292b52ce240db90aa565fd31ced2f7f54e3f03738c0b,2719
|
||||
- free-category-0.0.2.0@sha256:fda1880cf8b3446c863ed25e1ae44ecff330723e73ea61ed15c1024c3b1ed878,1456
|
||||
- natural-numbers-0.1.2.0@sha256:0fcbe1220979b83fa612fca73a1adb1a99ff43c84f9652b3ff32b2323309b43c,1407
|
|
@ -0,0 +1,33 @@
|
|||
# This file was autogenerated by Stack.
|
||||
# You should not edit this file by hand.
|
||||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/lock_files
|
||||
|
||||
packages:
|
||||
- completed:
|
||||
hackage: free-algebras-0.0.7.2@sha256:3796d89793727177f5e0292b52ce240db90aa565fd31ced2f7f54e3f03738c0b,2719
|
||||
pantry-tree:
|
||||
size: 1138
|
||||
sha256: f944d3be35376b275a1a2b58ef3e0f431d9b786bab01c65b55f10fe0a673a4c9
|
||||
original:
|
||||
hackage: free-algebras-0.0.7.2@sha256:3796d89793727177f5e0292b52ce240db90aa565fd31ced2f7f54e3f03738c0b,2719
|
||||
- completed:
|
||||
hackage: free-category-0.0.2.0@sha256:fda1880cf8b3446c863ed25e1ae44ecff330723e73ea61ed15c1024c3b1ed878,1456
|
||||
pantry-tree:
|
||||
size: 420
|
||||
sha256: 3884bf91da72d3ce38b596eec46af3e0f9f6c0a0c8641e990c617245ea537edb
|
||||
original:
|
||||
hackage: free-category-0.0.2.0@sha256:fda1880cf8b3446c863ed25e1ae44ecff330723e73ea61ed15c1024c3b1ed878,1456
|
||||
- completed:
|
||||
hackage: natural-numbers-0.1.2.0@sha256:0fcbe1220979b83fa612fca73a1adb1a99ff43c84f9652b3ff32b2323309b43c,1407
|
||||
pantry-tree:
|
||||
size: 220
|
||||
sha256: bd4beb677280f4586265c5657ab4e4a599c16f9e624df43d8edda6bf1e46dcdf
|
||||
original:
|
||||
hackage: natural-numbers-0.1.2.0@sha256:0fcbe1220979b83fa612fca73a1adb1a99ff43c84f9652b3ff32b2323309b43c,1407
|
||||
snapshots:
|
||||
- completed:
|
||||
size: 523884
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/4.yaml
|
||||
sha256: 16f24be248b42c9e16d59db84378836b1e7c239448a041cae46d32daffa45a8b
|
||||
original: lts-14.4
|
Loading…
Reference in New Issue