Compare commits

...

No commits in common. "master" and "haskell" have entirely different histories.

47 changed files with 1085 additions and 3735 deletions

View File

@ -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

View File

@ -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

5
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
/private
.stack-work/
tmd.cabal
*~

1618
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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 }

View File

@ -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/>.

View File

@ -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.

2
Setup.hs Normal file
View File

@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain

93
app/Main.hs Normal file
View File

@ -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

66
package.yaml Normal file
View File

@ -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

View File

@ -1 +0,0 @@
nightly

View File

@ -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

View File

@ -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

9
src/Data/Exists.hs Normal file
View File

@ -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

117
src/Data/NetEncoding.hs Normal file
View File

@ -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) + (

7
src/Data/NetParser.hs Normal file
View File

@ -0,0 +1,7 @@
module Data.NetParser where
import Data.ByteString
data NetParserF a where
Isolate :: Int -> NetParserF a -> NetParserF a
Rest :: NetParserF ByteString

32
src/Minecraft/Protocol.hs Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
class Minecraft.Protocol.Packet where
import Data.NetEncoding
class Packet a where
decode :: NetDecoder a
encode :: NetEncoder a

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -1,7 +0,0 @@
use serde::{Deserialize, Serialize};
// TODO: Support more features.
#[derive(Clone, Serialize, Deserialize)]
pub struct Chat {
pub text: Box<str>,
}

View File

@ -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)
}

View File

@ -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 {}

View File

@ -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
}
}

View File

@ -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,
}
}
}

View File

@ -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()))
}

View File

@ -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(())
}
}
}
}

View File

@ -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(())
}
}

View File

@ -1,3 +0,0 @@
pub mod packet;
pub mod packet_map;
pub mod state;

View File

@ -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();
}
}
)*
}
}

View File

@ -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);
} ),*
}
}
}
)*
}
}

View File

@ -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;
}
};
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}

9
stack.yaml Normal file
View File

@ -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

33
stack.yaml.lock Normal file
View File

@ -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