├── .gitignore ├── .projectile ├── LICENSE ├── README.md ├── config └── config.exs ├── entity_metadata.json ├── lib ├── acceptor │ ├── protocol_state.ex │ └── simple_acceptor.ex ├── connection │ ├── manager.ex │ ├── reader.ex │ └── writer.ex ├── crypto │ ├── login.ex │ ├── server_key_provider.ex │ └── transport.ex ├── datatypes.ex ├── entity_id_rewrite.ex ├── entity_metadata.ex ├── exceptions.ex ├── handler │ ├── handler.ex │ ├── handshake.ex │ ├── kick.ex │ ├── login.ex │ ├── proxy.ex │ ├── reset.ex │ └── status.ex ├── mc_protocol.ex ├── nbt.ex ├── orchestrator │ ├── orchestrator.ex │ └── server.ex ├── packet.ex ├── packet │ ├── decoder.ex │ ├── doc_collector.ex │ ├── in.ex │ ├── overrides.ex │ ├── proto_def_types.ex │ └── utils.ex ├── simple_proxy │ ├── orchestrator.ex │ └── task.ex ├── transport │ ├── read.ex │ └── write.ex ├── util │ └── generate_rsa.ex └── uuid.ex ├── mix.exs ├── mix.lock └── test ├── bigtest.nbt ├── elixir_mc_protocol ├── entity_metadata_test.exs └── nbt_test.exs ├── elixir_mc_protocol_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/elixir,vim,emacs 2 | 3 | ### Elixir ### 4 | /_build 5 | /cover 6 | /deps 7 | erl_crash.dump 8 | *.ez 9 | /rel 10 | /doc 11 | *.beam 12 | /bench 13 | 14 | 15 | ### Vim ### 16 | # swap 17 | [._]*.s[a-w][a-z] 18 | [._]s[a-w][a-z] 19 | # session 20 | Session.vim 21 | # temporary 22 | .netrwhist 23 | *~ 24 | # auto-generated tag files 25 | tags 26 | 27 | 28 | ### Emacs ### 29 | # -*- mode: gitignore; -*- 30 | *~ 31 | \#*\# 32 | /.emacs.desktop 33 | /.emacs.desktop.lock 34 | *.elc 35 | auto-save-list 36 | tramp 37 | .\#* 38 | 39 | # Org-mode 40 | .org-id-locations 41 | *_archive 42 | 43 | # flymake-mode 44 | *_flymake.* 45 | 46 | # eshell files 47 | /eshell/history 48 | /eshell/lastdir 49 | 50 | # elpa packages 51 | /elpa/ 52 | 53 | # reftex files 54 | *.rel 55 | 56 | # AUCTeX auto folder 57 | /auto/ 58 | 59 | # cask packages 60 | .cask/ 61 | 62 | # Flycheck 63 | flycheck_*.el 64 | 65 | # server auth directory 66 | /server/ 67 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- 1 | -_build/ 2 | -deps/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 hansihe (hansihe.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # McProtocol 2 | 3 | Implementation of the Minecraft protocol in Elixir. 4 | 5 | Aims to provide functional ways to interact with the minecraft protocol on all levels, including packet reading and writing, encryption, compression, authentication and more. 6 | 7 | ## Installation 8 | 9 | The package can be installed as: 10 | 11 | 1. Add elixir_mc_protocol to your list of dependencies in `mix.exs`: 12 | 13 | def deps do 14 | [{:elixir_mc_protocol, "~> 0.0.1"}] 15 | end 16 | 17 | 2. Ensure elixir_mc_protocol is started before your application: 18 | 19 | def application do 20 | [applications: [:elixir_mc_protocol]] 21 | end 22 | 23 | ## Documentation 24 | Documentation can be found [here](https://hexdocs.pm/mc_protocol/api-reference.html) with some usages [here](https://github.com/McEx/McProtocol/tree/master/test) 25 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :elixir_mc_protocol, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:elixir_mc_protocol, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | 32 | config :mc_data, :version, "1.9.2" 33 | -------------------------------------------------------------------------------- /entity_metadata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "class": "entity", 4 | "fields": [ 5 | [0, "byte", "flags"], 6 | [1, "short", "air"], 7 | [2, "string", "nameTag"], 8 | [3, "byte", "alwaysShowNameTag"], 9 | [4, "byte", "silent"] 10 | ] 11 | }, 12 | { 13 | "parentClass": "entity", 14 | "class": "entityLivingBase", 15 | "fields": [ 16 | [6, "float", "health"], 17 | [7, "int", "potionEffectColor"], 18 | [8, "byte", "potionEffectAmbient"], 19 | [9, "byte", "numArrows"] 20 | ] 21 | }, 22 | { 23 | "parentClass": "entityLivingBase", 24 | "class": "entityLiving", 25 | "fields": [ 26 | [15, "byte", "aiDisabled"] 27 | ] 28 | }, 29 | { 30 | "parentClass": "entityLiving", 31 | "class": "ageable", 32 | "fields": [ 33 | [12, "byte", "entityAge"] 34 | ] 35 | }, 36 | { 37 | "parentClass": "entityLivingBase", 38 | "class": "armorStand", 39 | "fields": [ 40 | [10, "byte", "flags"], 41 | [11, "rot", "headPosition"], 42 | [12, "rot", "bodyPosition"], 43 | [13, "rot", "leftArmPosition"], 44 | [14, "rot", "rightArmPosition"], 45 | [15, "rot", "leftLegPosition"], 46 | [16, "rot", "rightLegPosition"], 47 | ] 48 | }, 49 | { 50 | "parentClass": "entityLiving", 51 | "class": "human", 52 | "fields": [ 53 | [10, "byte", "skinFlags"], 54 | [17, "float", "absorptionHearts"], 55 | [18, "int", "score"] 56 | ] 57 | }, 58 | { 59 | "parentClass": "ageable", 60 | "class": "horse", 61 | "fields": [ 62 | [16, "int", "horseFlags"], 63 | [19, "byte", "horseType"], 64 | [20, "int", "horseColors"], 65 | [21, "string", "ownerName"], 66 | [22, "int", "horseArmor"] 67 | ] 68 | }, 69 | { 70 | "parentClass": "entityLiving", 71 | "class": "bat", 72 | "fields": [ 73 | [16, "byte", "hanging"] 74 | ] 75 | }, 76 | { 77 | "parentClass": "ageable", 78 | "class": "tameable", 79 | "fields": [ 80 | [16, "byte", "flags"], 81 | [17, "string", "ownerName"] 82 | ] 83 | }, 84 | { 85 | "parentClass": "tameable", 86 | "class": "ocelot", 87 | "fields": [ 88 | [18, "byte", "type"] 89 | ] 90 | }, 91 | { 92 | "parentClass": "tameable", 93 | "class": "wolf", 94 | "fields": [ 95 | [16, "byte", "flags"], 96 | [18, "float", "health"], 97 | [19, "byte", "begging"], 98 | [20, "byte", "collarColor"] 99 | ] 100 | }, 101 | { 102 | "parentClass": "ageable", 103 | "class": "pig", 104 | "fields": [ 105 | [16, "byte", "hasSaddle"] 106 | ] 107 | }, 108 | { 109 | "parentClass": "ageable", 110 | "class": "rabbit", 111 | "fields": [ 112 | [18, "byte", "rabbitType"] 113 | ] 114 | }, 115 | { 116 | "parentClass": "ageable", 117 | "class": "sheep", 118 | "fields": [ 119 | [16, "byte", "woolFlags"] 120 | ] 121 | }, 122 | { 123 | "parentClass": "ageable", 124 | "class": "villager", 125 | "fields": [ 126 | [16, "int", "villagerType"] 127 | ] 128 | }, 129 | { 130 | "parentClass": "entityLiving", 131 | "class": "enderman", 132 | "fields": [ 133 | [16, "short", "carriedBlock"], 134 | [17, "byte", "carriedBlockData"], 135 | [18, "byte", "isScreaming"] 136 | ] 137 | }, 138 | { 139 | "parentClass": "entityLiving", 140 | "class": "zombie", 141 | "fields": [ 142 | [12, "byte", "isChild"], 143 | [13, "byte", "isVillager"], 144 | [14, "byte", "isConverting"] 145 | ] 146 | }, 147 | { 148 | "parentClass": "zombie", 149 | "class": "zombiePigman", 150 | "fields": [] 151 | }, 152 | { 153 | "parentClass": "entityLiving", 154 | "class": "blaze", 155 | "fields": [ 156 | [16, "byte", "onFire"] 157 | ] 158 | }, 159 | { 160 | "parentClass": "entityLiving", 161 | "class": "spider", 162 | "fields": [ 163 | [16, "byte", "climbing"] 164 | ] 165 | }, 166 | { 167 | "parentClass": "spider", 168 | "class": "caveSpider", 169 | "fields": [] 170 | }, 171 | { 172 | "parentClass": "entityLiving", 173 | "class": "creeper", 174 | "fields": [ 175 | [16, "byte", "fuseState"], 176 | [17, "byte", "isPowered"] 177 | ] 178 | }, 179 | { 180 | "parentClass": "entityLiving", 181 | "class": "ghast", 182 | "fields": [ 183 | [16, "byte", "isAttacking"] 184 | ] 185 | }, 186 | { 187 | "parentClass": "entityLiving", 188 | "class": "slime", 189 | "fields": [ 190 | [16, "byte", "size"] 191 | ] 192 | }, 193 | { 194 | "parentClass": "slime", 195 | "class": "magmaCube", 196 | "fields": [] 197 | }, 198 | { 199 | "parentClass": "entityLiving", 200 | "class": "skeleton", 201 | "fields": [ 202 | [13, "byte", "type"] 203 | ] 204 | }, 205 | { 206 | "parentClass": "entityLiving", 207 | "class": "witch", 208 | "fields": [ 209 | [21, "byte", "isAggressive"] 210 | ] 211 | }, 212 | { 213 | "parentClass": "entityLiving", 214 | "class": "ironGolem", 215 | "fields": [ 216 | [16, "byte", "isAggressive"] 217 | ] 218 | }, 219 | { 220 | "parentClass": "entityLiving", 221 | "class": "wither", 222 | "fields": [ 223 | [17, "int", "watchedTarget1"], 224 | [18, "int", "watchedTarget2"], 225 | [19, "int", "watchedTarget3"], 226 | [20, "int", "invulnerableTime"] 227 | ] 228 | }, 229 | { 230 | "parentClass": "entityLiving", 231 | "class": "guardian", 232 | "fields": [ 233 | [16, "byte", "flags"], 234 | [17, "int", "targetEntityId"] 235 | ] 236 | }, 237 | { 238 | "parentClass": "entity", 239 | "class": "boat", 240 | "fields": [ 241 | [17, "int", "timeSinceHit"], 242 | [18, "int", "forwardDirection"], 243 | [19, "float", "damageTaken"] 244 | ] 245 | }, 246 | { 247 | "parentClass": "entity", 248 | "class": "minecart", 249 | "fields": [ 250 | [17, "int", "shakingPower"], 251 | [18, "int", "shakingDirection"], 252 | [19, "float", "damageTakenOrShakingMultiplier"], 253 | [20, "int", "block"], 254 | [21, "int", "blockYPos"], 255 | [22, "byte", "showBlock"] 256 | ] 257 | }, 258 | { 259 | "parentClass": "minecart", 260 | "class": "furnaceMinecart", 261 | "fields": [ 262 | [16, "byte", "isPowered"] 263 | ] 264 | }, 265 | { 266 | "parentClass": "entity", 267 | "class": "item", 268 | "fields": [ 269 | [10, "slot", "item"] 270 | ] 271 | }, 272 | { 273 | "parentClass": "entity", 274 | "class": "arrow", 275 | "fields": [ 276 | [16, "byte", "isCritical"] 277 | ] 278 | }, 279 | { 280 | "parentClass": "entity", 281 | "class": "firework", 282 | "fields": [ 283 | [8, "slot", "fireworkInfo"] 284 | ] 285 | }, 286 | { 287 | "parentClass": "entity", 288 | "class": "itemFrame", 289 | "fields": [ 290 | [8, "slot", "item"], 291 | [9, "byte", "rotation"] 292 | ] 293 | }, 294 | { 295 | "parentClass": "entity", 296 | "class": "enderCrystal", 297 | "fields": [ 298 | [8, "int", "health"] 299 | ] 300 | }, 301 | ] 302 | -------------------------------------------------------------------------------- /lib/acceptor/protocol_state.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Acceptor.ProtocolState do 2 | 3 | defmodule Connection do 4 | defstruct control: nil, reader: nil, writer: nil, write: nil 5 | 6 | def write_packet(%__MODULE__{write: write}, struct) do 7 | write.(struct) 8 | end 9 | end 10 | 11 | defstruct user: nil, connection: nil, config: %{online_mode: false} 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/acceptor/simple_acceptor.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Acceptor.SimpleAcceptor do 2 | require Logger 3 | 4 | @tcp_listen_options [:binary, packet: :raw, active: false, reuseaddr: true] 5 | 6 | def accept(port, accept_fun, socket_transferred_fun \\ (fn (_, _) -> nil end)) do 7 | {:ok, listen} = :gen_tcp.listen(port, @tcp_listen_options) 8 | Logger.info("Listening on port #{port}") 9 | accept_loop(listen, accept_fun, socket_transferred_fun) 10 | end 11 | 12 | defp accept_loop(listen, accept_fun, socket_transferred_fun) do 13 | {:ok, socket} = :gen_tcp.accept(listen) 14 | {:ok, pid} = accept_fun.(socket) 15 | :ok = :gen_tcp.controlling_process(socket, pid) 16 | socket_transferred_fun.(pid, socket) 17 | accept_loop(listen, accept_fun, socket_transferred_fun) 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /lib/connection/manager.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Connection.Manager do 2 | use GenServer 3 | require Logger 4 | 5 | alias McProtocol.Connection.{Reader, Writer} 6 | 7 | # Client 8 | 9 | def start_link(socket, direction, orch_module, opts \\ []) do 10 | GenServer.start_link(__MODULE__, {socket, direction, orch_module}, opts) 11 | end 12 | 13 | def start_reading(pid) do 14 | GenServer.cast(pid, :start_reading) 15 | end 16 | 17 | # Server 18 | 19 | defstruct [ 20 | socket: nil, 21 | reader: nil, 22 | writer: nil, 23 | stash: nil, 24 | 25 | handler: nil, 26 | handler_state: nil, 27 | handler_pid: nil, 28 | 29 | orch_module: nil, 30 | orch_pid: nil, 31 | ] 32 | 33 | def init({socket, direction, orch_module}) do 34 | 35 | control_process = self 36 | {:ok, reader} = Reader.start_link(socket, fn 37 | (:packet, data) -> GenServer.cast(control_process, {:packet, data}) 38 | (:closed, :tcp_closed) -> GenServer.cast(control_process, :tcp_closed) 39 | end) 40 | 41 | {:ok, writer} = Writer.start_link(socket) 42 | 43 | connection = %McProtocol.Acceptor.ProtocolState.Connection{ 44 | control: self, 45 | reader: reader, 46 | writer: writer, 47 | write: fn str -> 48 | GenServer.cast(control_process, {:write_struct, str}) 49 | end, 50 | } 51 | 52 | stash = %McProtocol.Handler.Stash{ 53 | direction: direction, 54 | connection: connection, 55 | } 56 | 57 | {:ok, orch_pid} = orch_module.start_link(self) 58 | {handler, params} = orch_module.next(orch_pid, :connect) 59 | {transitions, handler_state} = handler.enter(params, stash) 60 | 61 | state = %__MODULE__{ 62 | socket: socket, 63 | reader: reader, 64 | writer: writer, 65 | stash: stash, 66 | 67 | handler: handler, 68 | handler_state: nil, 69 | 70 | orch_module: orch_module, 71 | orch_pid: orch_pid, 72 | } 73 | state = apply_transitions(transitions, state, {handler, :enter, params}) 74 | 75 | {:ok, state} 76 | end 77 | 78 | def handle_cast(:start_reading, state) do 79 | :gen_tcp.controlling_process(state.socket, state.reader) 80 | Reader.start_reading(state.reader) 81 | {:noreply, state} 82 | end 83 | 84 | def handle_cast({:write_struct, str}, state) do 85 | Writer.write_struct(state.writer, str) 86 | {:noreply, state} 87 | end 88 | 89 | def handle_cast({:packet, data}, state) do 90 | packet = McProtocol.Packet.In.construct(state.stash.direction, 91 | state.stash.mode, data) 92 | 93 | {transitions, handler_state} = state.handler.handle( 94 | packet, state.stash, state.handler_state) 95 | state = %{state | handler_state: handler_state} 96 | 97 | state = apply_transitions(transitions, state, {state.handler, :next, packet}) 98 | 99 | {:noreply, state} 100 | end 101 | 102 | def apply_transitions(transitions, state, _error_context) do 103 | Enum.reduce(transitions, state, &(apply_transition(&1, &2))) 104 | end 105 | 106 | def apply_transition({:set_encryption, encr}, state) do 107 | Writer.set_encryption(state.writer, encr) 108 | Reader.set_encryption(state.reader, encr) 109 | state 110 | end 111 | def apply_transition({:set_compression, compr}, state) do 112 | Writer.set_compression(state.writer, compr) 113 | Reader.set_compression(state.reader, compr) 114 | state 115 | end 116 | def apply_transition({:send_packet, packet_struct}, state) do 117 | Writer.write_struct(state.writer, packet_struct) 118 | state 119 | #out_write_struct(packet_struct, state) 120 | end 121 | #def apply_transition({:send_data, packet_data}, state) do 122 | # out_write_data(packet_data, state) 123 | #end 124 | def apply_transition({:stash, %McProtocol.Handler.Stash{} = stash}, state) do 125 | %{state | 126 | stash: stash, 127 | } 128 | end 129 | # TODO: 130 | #def apply_transition({:handler_process, pid}, %{} = state) when is_pid(pid) do 131 | # 132 | #end 133 | def apply_transition({:next, return}, state) do 134 | {handler, params} = apply(state.orch_module, :next, 135 | [state.orch_pid, {state.handler, return}]) 136 | 137 | apply(state.handler, :leave, [state.stash, state.handler_state]) 138 | {transitions, handler_state} = apply(handler, :enter, [params, state.stash]) 139 | 140 | state = 141 | %{state | 142 | handler: handler, 143 | handler_state: handler_state 144 | } 145 | state = apply_transitions(transitions, state, {handler, :enter, params}) 146 | 147 | state 148 | end 149 | def apply_transition(:close, state) do 150 | exit(:normal) 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /lib/connection/reader.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Connection.Reader do 2 | use GenServer 3 | 4 | alias McProtocol.Transport.Read 5 | alias McProtocol.Crypto.Transport.CryptData 6 | 7 | # Client 8 | 9 | def start_link(socket, sink) do 10 | GenServer.start_link(__MODULE__, {socket, sink}) 11 | end 12 | 13 | def start_reading(pid) do 14 | GenServer.call(pid, :start_reading) 15 | end 16 | 17 | def set_encryption(pid, encr = %CryptData{}) do 18 | GenServer.call(pid, {:set_encryption, encr}) 19 | end 20 | def set_compression(pid, compr) do 21 | GenServer.call(pid, {:set_compression, compr}) 22 | end 23 | 24 | # Server 25 | 26 | defstruct [ 27 | state: :init, 28 | socket: nil, 29 | read_state: nil, 30 | sink: nil, 31 | ] 32 | 33 | def init({socket, sink_fun}) do 34 | state = %__MODULE__{ 35 | socket: socket, 36 | read_state: Read.initial_state, 37 | sink: sink_fun, 38 | } 39 | {:ok, state} 40 | end 41 | 42 | def handle_call(:start_reading, _from, state = %__MODULE__{state: :init}) do 43 | state = %{state | state: :started} 44 | |> recv_once 45 | 46 | {:reply, :ok, state} 47 | end 48 | 49 | def handle_call({:set_encryption, encr}, _from, state) do 50 | read_state = Read.set_encryption(state.read_state, encr) 51 | state = %{state | read_state: read_state} 52 | {:reply, :ok, state} 53 | end 54 | def handle_call({:set_compression, encr}, _from, state) do 55 | read_state = Read.set_compression(state.read_state, encr) 56 | state = %{state | read_state: read_state} 57 | {:reply, :ok, state} 58 | end 59 | 60 | def handle_info({:tcp, socket, data}, 61 | state = %__MODULE__{socket: socket, state: :started}) do 62 | {packets, read_state} = Read.process(data, state.read_state) 63 | 64 | Enum.map(packets, &state.sink.(:packet, &1)) 65 | 66 | state = %{state | read_state: read_state} 67 | |> recv_once 68 | 69 | {:noreply, state} 70 | end 71 | def handle_info({:tcp_closed, socket}, state = %__MODULE__{socket: socket}) do 72 | state.sink.(:closed, :tcp_closed) 73 | {:stop, {:shutdown, :tcp_closed}, state} 74 | end 75 | 76 | defp recv_once(state) do 77 | :inet.setopts(state.socket, active: :once) 78 | state 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /lib/connection/writer.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Connection.Writer do 2 | use GenServer 3 | require Logger 4 | 5 | alias McProtocol.Transport.Write 6 | alias McProtocol.Crypto.Transport.CryptData 7 | 8 | # Client 9 | 10 | def start_link(socket) do 11 | GenServer.start_link(__MODULE__, socket) 12 | end 13 | 14 | def write_struct(pid, packet) do 15 | GenServer.cast(pid, {:write_struct, packet}) 16 | end 17 | def write_raw(pid, data) do 18 | GenServer.cast(pid, {:write_raw, data}) 19 | end 20 | 21 | def set_encryption(pid, encr = %CryptData{}) do 22 | GenServer.call(pid, {:set_encryption, encr}) 23 | end 24 | def set_compression(pid, compr) do 25 | GenServer.call(pid, {:set_compression, compr}) 26 | end 27 | 28 | # Server 29 | 30 | defstruct [ 31 | socket: nil, 32 | write_state: nil, 33 | ] 34 | 35 | def init(socket) do 36 | state = %__MODULE__{ 37 | socket: socket, 38 | write_state: Write.initial_state, 39 | } 40 | {:ok, state} 41 | end 42 | 43 | def handle_cast({:write_struct, packet}, state) do 44 | state = out_write_struct(packet, state) 45 | {:noreply, state} 46 | end 47 | def handle_cast({:write_raw, data}, state) do 48 | state = out_write_data(data, state) 49 | {:noreply, state} 50 | end 51 | 52 | def handle_call({:set_encryption, encr}, _from, state) do 53 | write_state = Write.set_encryption(state.write_state, encr) 54 | state = %{state | write_state: write_state} 55 | {:reply, :ok, state} 56 | end 57 | def handle_call({:set_compression, compr}, _from, state) do 58 | write_state = Write.set_compression(state.write_state, compr) 59 | state = %{state | write_state: write_state} 60 | {:reply, :ok, state} 61 | end 62 | 63 | def out_write_struct(packet, state) do 64 | packet_data = 65 | try do 66 | McProtocol.Packet.write(packet) 67 | rescue 68 | error -> handle_write_error(error, packet, state) 69 | end 70 | 71 | out_write_data(packet_data, state) 72 | end 73 | 74 | def out_write_data(data, state) do 75 | {out_data, write_state} = Write.process(data, state.write_state) 76 | state = %{state | write_state: write_state} 77 | 78 | :ok = socket_write_raw(out_data, state) 79 | 80 | # TODO: This should only be done when big packets are sent 81 | :erlang.garbage_collect 82 | 83 | state 84 | end 85 | 86 | def socket_write_raw(data, state) do 87 | case :gen_tcp.send(state.socket, data) do 88 | :ok -> :ok 89 | # This might seem like a REALLY bad idea, and it probably is, 90 | # but a socket closed error should be handled by the reader process, 91 | # as that is what actually gets notified of it. 92 | # Handling it in one place makes things easier for us. 93 | _ -> :ok 94 | end 95 | end 96 | 97 | defp handle_write_error(error, struct, state) do 98 | error_format = Exception.format(:error, error) 99 | error_msg = error_format <> "When encoding packet:\n" <> inspect(struct) <> "\n" 100 | Logger.error(error_msg) 101 | exit(:packet_write_error) 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /lib/crypto/login.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Crypto.Login do 2 | 3 | @join_verify_url "https://sessionserver.mojang.com/session/minecraft/hasJoined?" 4 | 5 | defp stupid_sha1(data) do 6 | <> = :crypto.hash(:sha, data) 7 | 8 | sign = hash < 0 9 | if sign, do: hash = -hash 10 | 11 | hash_string = String.downcase(Integer.to_string(hash, 16)) 12 | 13 | if sign, do: hash_string, else: "-" <> hash_string 14 | end 15 | 16 | def verification_hash(secret, pubkey) do 17 | stupid_sha1(secret <> pubkey) 18 | end 19 | 20 | defmodule LoginVerifyResponse do 21 | @moduledoc false 22 | defstruct [:id, :name] 23 | end 24 | 25 | def verify_user_login(pubkey, secret, name) do 26 | hash = verification_hash(secret, pubkey) 27 | query = URI.encode_query(%{username: name, serverId: hash}) 28 | response = %{status_code: 200, body: json} = HTTPotion.get(@join_verify_url <> query) 29 | %{name: ^name} = Poison.decode!(json, as: LoginVerifyResponse) 30 | end 31 | 32 | def get_auth_init_data(key_server) do 33 | {McProtocol.Crypto.ServerKeyProvider.get_keys, gen_token} 34 | end 35 | defp gen_token do 36 | :crypto.strong_rand_bytes(16) 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/crypto/server_key_provider.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Crypto.ServerKeyProvider do 2 | require Record 3 | Record.defrecord :rsa_priv_key, :RSAPrivateKey, Record.extract(:RSAPrivateKey, from_lib: "public_key/include/OTP-PUB-KEY.hrl") 4 | Record.defrecord :rsa_pub_key, :RSAPublicKey, Record.extract(:RSAPublicKey, from_lib: "public_key/include/OTP-PUB-KEY.hrl") 5 | 6 | use GenServer 7 | 8 | # Public 9 | 10 | def start_link(opts \\ []) do 11 | GenServer.start_link(__MODULE__, :ok, opts) 12 | end 13 | 14 | def get_keys do 15 | GenServer.call(__MODULE__, :get_keys) 16 | end 17 | 18 | # Private 19 | 20 | def init(:ok) do 21 | # {:ok, private_key_1} = :cutkey.rsa(1024, 65537, return: :key) 22 | private_key = McProtocol.Util.GenerateRSA.gen(1024) 23 | private_key_data = rsa_priv_key(private_key) 24 | public_key = rsa_pub_key(modulus: private_key_data[:modulus], publicExponent: private_key_data[:publicExponent]) 25 | {:SubjectPublicKeyInfo, public_key_asn, :not_encrypted} = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) 26 | {:ok, {public_key_asn, private_key}} 27 | end 28 | 29 | def handle_call(:get_keys, _from, state) do 30 | {:reply, state, state} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/crypto/transport.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Crypto.Transport do 2 | 3 | defmodule CryptData do 4 | defstruct key: nil, ivec: nil 5 | end 6 | 7 | def encrypt(plaintext, %CryptData{} = cryptdata) do 8 | encrypt(plaintext, cryptdata, []) 9 | end 10 | 11 | defp encrypt(<>, %CryptData{key: key, ivec: ivec} = cryptdata, ciph_base) do 12 | ciphertext = :crypto.block_encrypt(:aes_cfb8, key, ivec, plain_byte) 13 | ivec = update_ivec(ivec, ciphertext) 14 | encrypt(plain_rest, %{cryptdata | ivec: ivec}, [ciph_base, ciphertext]) 15 | end 16 | defp encrypt(<<>>, %CryptData{} = cryptdata, ciphertext) do 17 | {cryptdata, IO.iodata_to_binary(ciphertext)} 18 | end 19 | 20 | def decrypt(ciphertext, %CryptData{} = cryptdata) do 21 | decrypt(ciphertext, cryptdata, []) 22 | end 23 | 24 | defp decrypt(<> = ciph, %CryptData{key: key, ivec: ivec} = cryptdata, plain_base) do 25 | plaintext = :crypto.block_decrypt(:aes_cfb8, key, ivec, ciph_byte) 26 | ivec = update_ivec(ivec, ciph_byte) 27 | decrypt(ciph_rest, %{cryptdata | ivec: ivec}, [plain_base, plaintext]) 28 | end 29 | defp decrypt(<<>>, %CryptData{} = cryptdata, plaintext) do 30 | {cryptdata, IO.iodata_to_binary(plaintext)} 31 | end 32 | 33 | defp update_ivec(ivec, data) when byte_size(data) == 1 and byte_size(ivec) == 16 do 34 | <<_::binary-size(1), ivec_end::binary-size(15)>> = ivec 35 | <> 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/datatypes.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.DataTypes do 2 | @moduledoc false 3 | 4 | # For <<< (left shift) operator 5 | use Bitwise 6 | 7 | defmodule ChatMessage do 8 | @moduledoc false 9 | 10 | defstruct [:text, :translate, :with, :score, :selector, :extra, 11 | :bold, :italic, :underligned, :strikethrough, :obfuscated, :color, 12 | :clickEvent, :hoverEvent, :insertion] 13 | 14 | defmodule Score do 15 | @moduledoc false 16 | defstruct [:name, :objective] 17 | end 18 | defmodule ClickEvent do 19 | @moduledoc false 20 | defstruct [:action, :value] 21 | end 22 | defmodule HoverEvent do 23 | @moduledoc false 24 | defstruct [:action, :value] 25 | end 26 | end 27 | 28 | defmodule Slot do 29 | @moduledoc false 30 | defstruct id: nil, count: 0, damage: 0, nbt: nil 31 | end 32 | 33 | defmodule Decode do 34 | 35 | @spec varint(binary) :: {integer, binary} 36 | def varint(data) do 37 | {:ok, resp} = varint?(data) 38 | resp 39 | end 40 | 41 | def varint?(data) do 42 | decode_varint(data, 0, 0) 43 | end 44 | defp decode_varint(<<1::1, curr::7, rest::binary>>, num, acc) when num < (64 - 7) do 45 | decode_varint(rest, num + 7, (curr <<< num) + acc) 46 | end 47 | defp decode_varint(<<0::1, curr::7, rest::binary>>, num, acc) do 48 | {:ok, {(curr <<< num) + acc, rest}} 49 | end 50 | defp decode_varint(_, num, _) when num >= (64 - 7), do: :too_big 51 | defp decode_varint("", _, _), do: :incomplete 52 | defp decode_varint(_, _, _), do: :error 53 | 54 | @spec bool(binary) :: {boolean, binary} 55 | def bool(<>) do 56 | case value do 57 | 1 -> {true, rest} 58 | _ -> {false, rest} 59 | end 60 | end 61 | 62 | def string(data) do 63 | {length, data} = varint(data) 64 | <> = data 65 | {to_string(result), rest} 66 | #result = :binary.part(data, {0, length}) 67 | #{result, :binary.part(data, {length, byte_size(data)-length})} 68 | end 69 | 70 | def chat(data) do 71 | json = string(data) 72 | Poison.decode!(json, as: McProtocol.DataTypes.ChatMessage) 73 | end 74 | 75 | def slot(data) do 76 | <> = data 77 | slot_with_id(data, id) 78 | end 79 | defp slot_with_id(data, -1), do: {%McProtocol.DataTypes.Slot{}, data} 80 | defp slot_with_id(data, id) do 81 | <> = data 82 | {nbt, data} = slot_nbt(data) 83 | struct = %McProtocol.DataTypes.Slot{id: id, count: count, damage: damage, nbt: nbt} 84 | {struct, data} 85 | end 86 | defp slot_nbt(<<0, data::binary>>), do: {nil, data} 87 | defp slot_nbt(data), do: McProtocol.NBT.read(data) 88 | 89 | def varint_length_binary(data) do 90 | {length, data} = varint(data) 91 | result = :binary.part(data, {0, length}) 92 | {result, :binary.part(data, {length, byte_size(data)-length})} 93 | end 94 | 95 | def byte(data) do 96 | <> = data 97 | {num, data} 98 | end 99 | def fixed_point_byte(data) do 100 | {num, data} = byte(data) 101 | {num / 32, data} 102 | end 103 | def u_byte(data) do 104 | <> = data 105 | {num, data} 106 | end 107 | 108 | def short(data) do 109 | <> = data 110 | {num, data} 111 | end 112 | def u_short(data) do 113 | <> = data 114 | {num, data} 115 | end 116 | 117 | def int(data) do 118 | <> = data 119 | {num, data} 120 | end 121 | def fixed_point_int(data) do 122 | {num, data} = int(data) 123 | {num / 32, data} 124 | end 125 | def long(data) do 126 | <> = data 127 | {num, data} 128 | end 129 | 130 | def float(data) do 131 | <> = data 132 | {num, data} 133 | end 134 | def double(data) do 135 | <> = data 136 | {num, data} 137 | end 138 | 139 | def rotation(data) do 140 | <> = data 142 | 143 | {{x, y, z}, data} 144 | end 145 | def position(data) do 146 | <> = data 147 | {{x, y, z}, data} 148 | end 149 | 150 | def byte_array_rest(data) do 151 | {data, <<>>} 152 | end 153 | 154 | def byte_flags(data) do 155 | <> = data 156 | {flags, data} 157 | end 158 | end 159 | 160 | defmodule Encode do 161 | 162 | def byte_flags(bin) do 163 | bin 164 | end 165 | 166 | @spec varint(integer) :: binary 167 | def varint(num) when num <= 127, do: <<0::1, num::7>> 168 | def varint(num) when num >= 128 do 169 | <<1::1, band(num, 127)::7, varint(num >>> 7)::binary>> 170 | end 171 | 172 | @spec bool(boolean) :: binary 173 | def bool(bool) do 174 | if bool do 175 | <<1::size(8)>> 176 | else 177 | <<0::size(8)>> 178 | end 179 | end 180 | 181 | def string(string) do 182 | <> 183 | end 184 | def chat(struct) do 185 | string(Poison.Encoder.encode(struct, [])) 186 | end 187 | 188 | def slot(%McProtocol.DataTypes.Slot{id: nil}), do: <<-1::signed-integer-2*8>> 189 | def slot(%McProtocol.DataTypes.Slot{id: -1}), do: <<-1::signed-integer-2*8>> 190 | def slot(nil), do: <<-1::signed-integer-2*8>> 191 | def slot(%McProtocol.DataTypes.Slot{} = slot) do 192 | [ <>, 195 | McProtocol.NBT.write(slot.nbt, true)] 196 | end 197 | 198 | def varint_length_binary(data) do 199 | <> 200 | end 201 | 202 | def byte(num) when is_integer(num) do 203 | <> 204 | end 205 | def fixed_point_byte(num) do 206 | byte(round(num * 32)) 207 | end 208 | def u_byte(num) do 209 | <> 210 | end 211 | 212 | def short(num) do 213 | <> 214 | end 215 | def u_short(num) do 216 | <> 217 | end 218 | 219 | def int(num) do 220 | <> 221 | end 222 | def fixed_point_int(num) do 223 | int(round(num * 32)) 224 | end 225 | def long(num) do 226 | <> 227 | end 228 | 229 | def float(num) do 230 | <> 231 | end 232 | def double(num) do 233 | <> 234 | end 235 | 236 | def position({x, y, z}) do 237 | <> 238 | end 239 | 240 | def data(data) do 241 | data 242 | end 243 | 244 | def uuid_string(%McProtocol.UUID{} = dat) do 245 | string(McProtocol.UUID.hex_hyphen(dat)) 246 | end 247 | def uuid(%McProtocol.UUID{} = dat) do 248 | #<> 249 | McProtocol.UUID.bin dat 250 | end 251 | 252 | def angle(num) do 253 | byte(num) 254 | end 255 | def metadata(meta) do 256 | McProtocol.EntityMeta.write(meta) 257 | end 258 | end 259 | 260 | end 261 | -------------------------------------------------------------------------------- /lib/entity_id_rewrite.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.EntityIdRewrite.Util do 2 | 3 | def rewrite_info(module) when is_atom(module), do: {module, [:entity_id]} 4 | def rewrite_info({module, field}), do: {module, field} 5 | 6 | end 7 | 8 | defmodule McProtocol.EntityIdRewrite do 9 | alias McProtocol.Packet.{Client, Server} 10 | 11 | # Simple rewrites 12 | rewrites = [ 13 | Server.Play.SpawnEntity, 14 | Server.Play.SpawnEntityExperienceOrb, 15 | Server.Play.SpawnEntityWeather, 16 | Server.Play.SpawnEntityLiving, 17 | Server.Play.SpawnEntityPainting, 18 | Server.Play.NamedEntitySpawn, 19 | Server.Play.Animation, 20 | Server.Play.BlockBreakAnimation, 21 | Server.Play.EntityStatus, 22 | Server.Play.EntityMove, 23 | Server.Play.EntityMoveLook, 24 | Server.Play.EntityLook, 25 | Server.Play.Entity, 26 | Server.Play.Bed, 27 | Server.Play.RemoveEntityEffect, 28 | Server.Play.EntityHeadRotation, 29 | {Server.Play.Camera, [:camera_id]}, 30 | Server.Play.EntityMetadata, 31 | {Server.Play.AttachEntity, [:entity_id, :vehicle_id]}, 32 | Server.Play.EntityVelocity, 33 | Server.Play.EntityEquipment, 34 | {Server.Play.Collect, [:collected_entity_id, :collector_entity_id]}, 35 | Server.Play.EntityTeleport, 36 | Server.Play.EntityUpdateAttributes, 37 | Server.Play.EntityEffect, 38 | 39 | {Client.Play.UseEntity, [:target]}, 40 | Client.Play.EntityAction, 41 | ] 42 | 43 | def rewrite_eid(eid, {eid, replace}), do: replace 44 | def rewrite_eid(eid, {replace, eid}), do: replace 45 | def rewrite_eid(eid, {_, _}), do: eid 46 | 47 | def rewrite(packet = %Server.Play.EntityDestroy{}, ids) do 48 | %{packet | 49 | entity_ids: Enum.map(packet.entity_ids, fn 50 | eid -> rewrite_eid(eid, ids) 51 | end) 52 | } 53 | end 54 | 55 | def rewrite(packet = %Server.Play.CombatEvent{event: 1}, ids) do 56 | %{packet | entity_id: rewrite_eid(packet.entity_id, ids)} 57 | end 58 | def rewrite(packet = %Server.Play.CombatEvent{event: 2}, ids) do 59 | %{packet | 60 | entity_id: rewrite_eid(packet.entity_id, ids), 61 | player_id: rewrite_eid(packet.player_id, ids), 62 | } 63 | end 64 | 65 | def rewrite(packet = %Server.Play.SetPassengers{}, ids) do 66 | %{packet | 67 | entity_id: rewrite_eid(packet.entity_id, ids), 68 | passengers: Enum.map(packet.passengers, fn 69 | eid -> rewrite_eid(eid, ids) 70 | end) 71 | } 72 | end 73 | 74 | for rewrite_data <- rewrites do 75 | {module, fields} = McProtocol.EntityIdRewrite.Util.rewrite_info(rewrite_data) 76 | 77 | packet_var = Macro.var(:packet, __MODULE__) 78 | ids_var = Macro.var(:ids, __MODULE__) 79 | 80 | fields = Enum.map(fields, fn 81 | field -> 82 | {field, 83 | quote do 84 | rewrite_eid( 85 | unquote(packet_var).unquote(field), 86 | unquote(ids_var) 87 | ) 88 | end 89 | } 90 | end) 91 | 92 | def rewrite(unquote(packet_var) = %unquote(module){}, unquote(ids_var)) do 93 | %{unquote(packet_var) | 94 | unquote_splicing(fields) 95 | } 96 | end 97 | end 98 | 99 | def rewrite(packet, _) do 100 | packet 101 | end 102 | 103 | end 104 | -------------------------------------------------------------------------------- /lib/entity_metadata.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.EntityMeta do 2 | alias McProtocol.DataTypes.{Encode, Decode} 3 | 4 | type_idx = [ 5 | byte: 0, 6 | varint: 1, 7 | float: 2, 8 | string: 3, 9 | chat: 4, 10 | slot: 5, 11 | boolean: 6, 12 | rotation: 7, 13 | position: 8, 14 | opt_position: 9, 15 | direction: 10, 16 | opt_uuid: 11, 17 | block_id: 12, 18 | ] 19 | for {ident, num} <- type_idx do 20 | def type_idx(unquote(ident)), do: unquote(num) 21 | def idx_type(unquote(num)), do: unquote(ident) 22 | end 23 | 24 | def read(<<0xff::unsigned-1*8, rest::binary>>, entries) do 25 | {Enum.reverse(entries), rest} 26 | end 27 | def read(<>, entries) do 28 | {type, value, rest} = read_type_body(rest) 29 | read(rest, [{index, type, value} | entries]) 30 | end 31 | 32 | defp read_type_body(<>) do 33 | type = idx_type(type_num) 34 | {value, rest} = read_type(rest, type) 35 | {type, value, rest} 36 | end 37 | 38 | defp read_type(data, :byte), do: Decode.byte(data) 39 | defp read_type(data, :varint), do: Decode.varint(data) 40 | defp read_type(data, :float), do: Decode.float(data) 41 | defp read_type(data, :string), do: Decode.string(data) 42 | defp read_type(data, :chat), do: Decode.chat(data) 43 | defp read_type(data, :slot), do: Decode.slot(data) 44 | defp read_type(data, :boolean), do: Decode.bool(data) 45 | defp read_type(data, :rotation), do: Decode.rotation(data) 46 | defp read_type(data, :position), do: Decode.position(data) 47 | defp read_type(data, :opt_position) do 48 | {bool, data} = Decode.bool(data) 49 | if bool do 50 | Decode.position(data) 51 | end 52 | end 53 | defp read_type(data, :direction) do 54 | {direction, data} = Decode.varint(data) 55 | case direction do 56 | 0 -> :down 57 | 1 -> :up 58 | 2 -> :north 59 | 3 -> :south 60 | 4 -> :west 61 | 5 -> :east 62 | _ -> raise "Unknown direction: #{direction}" 63 | end 64 | end 65 | defp read_type(data, :opt_uuid) do 66 | {bool, data} = Decode.bool(data) 67 | if bool do 68 | raise "unimplemented" 69 | end 70 | end 71 | defp read_type(data, :block_id), do: Decode.varint(data) 72 | 73 | def write(input), do: write_data(input, []) 74 | 75 | defp write_data([], data), do: [data, <<0xff::unsigned-1*8>>] 76 | defp write_data([item | rest], data) do 77 | write_data(rest, [data, write_item(item)]) 78 | end 79 | 80 | defp write_item({index, type, value}) do 81 | [<>, type_idx(type), write_type(type, value)] 82 | end 83 | 84 | defp write_type(:byte, value), do: Encode.byte(value) 85 | defp write_type(:varint, value), do: Encode.varint(value) 86 | defp write_type(:float, value), do: Encode.float(value) 87 | defp write_type(:string, value), do: Encode.string(value) 88 | defp write_type(:chat, value), do: Encode.chat(value) 89 | defp write_type(:slot, value), do: Encode.slot(value) 90 | 91 | end 92 | -------------------------------------------------------------------------------- /lib/exceptions.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.ClosedError do 2 | defexception [] 3 | def message(_exception) do 4 | "Connection closed" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/handler/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler do 2 | 3 | @type t :: module 4 | 5 | @moduledoc """ 6 | Basic component for the connection state machine. 7 | 8 | This behaviour is one of the two components that makes McProtocol flexible, 9 | the other one being the Orchestrator. To interact directly with the protocol 10 | on the standard acceptor, you need to implement this behavior. 11 | 12 | ## Interaction 13 | 14 | A handler has two ways to interact with the connection it's associated with, 15 | synchronous, and asynchronous. 16 | 17 | Synchronous interaction is named a transition (it transitions the connection 18 | state machine into a new state). Transitions can do things like set protocol 19 | encryption, send packets, raw data, or transition to a new protocol state. It 20 | allows you to control the precise order of operations. 21 | 22 | (Most of this is not done yet) Asynchronous interaction is done by messaging. 23 | Any process can interact with any connection, as long as it has the pid and 24 | state cookie. Because the exact order of operations can not be controlled, 25 | things like setting encryption or compression is not possible. 26 | """ 27 | 28 | @type protocol_direction :: :Client | :Server 29 | @type protocol_mode :: :Handshake | :Status | :Login | :Play 30 | 31 | @typedoc """ 32 | The protocol play mode contains additional information on what state the 33 | connection is in when the protocol_mode is :Play. 34 | 35 | There are currently 3 defined modes: 36 | 37 | * :init - When the connection just switched from the login mode. 38 | * :reset - The connection has been reset, and is in a well defined state so 39 | that another handler can take over. 40 | * :in_world - The player is currently in a world, and you should expect to 41 | receive movement packets and more from the client. Care should be taken to 42 | handle things like movement packets when returning the connection to the :reset 43 | play_mode. 44 | 45 | When the connection play_mode is set to :reset, the connection is required to 46 | be in the following state: 47 | 48 | * Respawn or Join Game packet has just been sent. (as in not yet spawned in 49 | world) 50 | * Gamemode set is 0 51 | * Dimension set is 0 52 | * Difficulty set if 0 53 | * Level type is "default" 54 | * Reduced Debug Info is false 55 | """ 56 | @type play_mode :: :init | :reset | :in_world | nil 57 | 58 | @typedoc """ 59 | A command to transition the connection state machine to a new state. 60 | 61 | * set_encryption - Sets the encryption data for both reading and writing. 62 | * send_packet - Encodes and sends the provided packet struct. 63 | * send_data - Sends the provided raw data to the socket. DO NOT use. 64 | * stash - Updates the stash of the socket. When using this, make sure you 65 | are only updating things you are allowed to touch. 66 | * handler_process - Tells the connection to monitor this process. If the 67 | process stops, it will be handled as a handler crash. 68 | * next - Tells the orchestrator that the handler is done with the connection. 69 | The second element will be returned to the orchestrator as the handler return 70 | value. 71 | * close - There is nothing more that can be done on this connection, and 72 | it should be closed. Examples of this are when the player has been kicked or 73 | when the status exchange has been completed. 74 | """ 75 | @type transition ::{:set_encryption, %McProtocol.Crypto.Transport.CryptData{}} 76 | | {:set_compression, non_neg_integer} 77 | | {:send_packet, McProtocol.Packet.t} 78 | | {:send_data, iodata} 79 | | {:stash, Stash.t} 80 | | {:handler_process, pid} 81 | | {:next, return_value :: any} 82 | | :close 83 | @type transitions :: [transition] 84 | 85 | @type handler :: module 86 | @type handler_state :: term 87 | 88 | @doc """ 89 | This callback is the first thing called when the handler is given control. 90 | """ 91 | @callback enter(args :: any, stash :: Stash.t) :: {transitions, handler_state} 92 | 93 | @doc """ 94 | When a packet is received on the connection, this callback is called. 95 | """ 96 | @callback handle(packet :: McProtocol.Packet.In.t, stash :: Stash.t, state :: handler_state) :: {transitions, handler_state} 97 | 98 | @doc """ 99 | This callback the absolute last thing called when control is taken away from 100 | a handler. You are not able to influence the state of anything related to the 101 | connection from here, and it should only be used to gracefully stop things 102 | like related processes. 103 | """ 104 | @callback leave(stash :: Stash.t, state :: handler_state) :: nil 105 | 106 | defmacro __using__(opts) do 107 | quote do 108 | @behaviour McProtocol.Handler 109 | 110 | def leave(_stash, _handler_state), do: nil 111 | 112 | defoverridable [leave: 2] 113 | end 114 | end 115 | 116 | defmodule Stash do 117 | @type t :: %__MODULE__{ 118 | direction: McProtocol.Handler.protocol_direction, 119 | mode: McProtocol.Handler.protocol_mode, 120 | play_mode: McProtocol.Handler.play_mode, 121 | connection: %McProtocol.Acceptor.ProtocolState.Connection{}, 122 | identity: %{authed: boolean, name: String.t, uuid: McProtocol.UUID.t} | nil, 123 | entity_id: non_neg_integer, 124 | } 125 | 126 | defstruct( 127 | direction: nil, 128 | mode: :Handshake, 129 | play_mode: nil, 130 | connection: nil, 131 | 132 | # Stores player identity from the authentication protocol phase. 133 | identity: nil, 134 | 135 | # Because the entity id of a player can never change once it's set by the 136 | # server, we need to keep track of this through the lifetime of the connection. 137 | # Currently set statically to 0 for simplicity. 138 | entity_id: 0, 139 | ) 140 | end 141 | 142 | end 143 | -------------------------------------------------------------------------------- /lib/handler/handshake.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler.Handshake do 2 | use McProtocol.Handler 3 | 4 | alias McProtocol.Packet 5 | alias McProtocol.Packet.Client 6 | 7 | def enter(_args, %{direction: :Client, mode: :Handshake} = stash) do 8 | {[], nil} 9 | end 10 | 11 | def state_atom(1), do: :Status 12 | def state_atom(2), do: :Login 13 | 14 | def handle(packet_in, stash, nil) do 15 | packet_in = packet_in |> McProtocol.Packet.In.fetch_packet 16 | packet = %Client.Handshake.SetProtocol{} = packet_in.packet 17 | 18 | mode = state_atom(packet.next_state) 19 | 20 | transitions = [ 21 | {:stash, 22 | %{stash | 23 | mode: mode, 24 | }}, 25 | {:next, mode}, 26 | ] 27 | 28 | {transitions, nil} 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/handler/kick.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler.Kick do 2 | use McProtocol.Handler 3 | 4 | @moduledoc """ 5 | Kicks the player from the server with a message. Can be used in Login and Play. 6 | 7 | The argument should be a map in the Chat format. 8 | """ 9 | 10 | alias McProtocol.Packet.Server 11 | 12 | def enter(reason, %{direction: :Client, mode: :Login}) do 13 | transitions = [ 14 | {:send_packet, 15 | %Server.Login.Disconnect{ 16 | reason: Poison.encode!(reason), 17 | }}, 18 | :close, 19 | ] 20 | {transitions, nil} 21 | end 22 | def enter(reason, %{direction: :Client, mode: :Play}) do 23 | transitions = [ 24 | {:send_packet, 25 | %Server.Play.KickDisconnect{ 26 | reason: Poison.encode!(reason), 27 | }}, 28 | :close, 29 | ] 30 | {transitions, nil} 31 | end 32 | 33 | def handle(_, _, _), do: raise "should never happen" 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/handler/login.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler.Login do 2 | use McProtocol.Handler 3 | 4 | alias McProtocol.Packet.Client 5 | alias McProtocol.Packet.Server 6 | 7 | def enter(_params, %{direction: :Client, mode: :Login}) do 8 | {[], %{}} 9 | end 10 | 11 | def handle(packet_in, stash, state) do 12 | packet_in = packet_in |> McProtocol.Packet.In.fetch_packet 13 | handle_packet(packet_in.packet, stash, state) 14 | end 15 | 16 | def handle_packet(%Client.Login.LoginStart{username: name}, stash, state) do 17 | # TODO: Online mode config 18 | handle_start(false, name, stash, state) 19 | end 20 | def handle_packet(packet = %Client.Login.EncryptionBegin{}, stash, state) do 21 | %{shared_secret: encr_shared_secret, verify_token: encr_token} = packet 22 | %{auth_init_data: {{pub_key, priv_key}, token}, identity: identity} = state 23 | 24 | ^token = :public_key.decrypt_private(encr_token, priv_key) 25 | shared_secret = :public_key.decrypt_private(encr_shared_secret, priv_key) 26 | 16 = byte_size(shared_secret) 27 | 28 | verification_response = McProtocol.Crypto.Login.verify_user_login(pub_key, shared_secret, 29 | identity.name) 30 | name = identity.name 31 | ^name = verification_response.name 32 | uuid = McProtocol.UUID.from_hex(verification_response.id) 33 | 34 | transitions = [ 35 | {:set_encryption, 36 | %McProtocol.Crypto.Transport.CryptData{ 37 | key: shared_secret, 38 | ivec: shared_secret, 39 | }}, 40 | ] 41 | 42 | identity = %{identity | uuid: uuid} 43 | state = state 44 | |> Map.put(:identity, identity) 45 | |> Map.put(:finished, true) 46 | 47 | {transitions_finish, state} = finish_login(stash, state) 48 | {transitions ++ transitions_finish, state} 49 | end 50 | 51 | # Online 52 | def handle_start(true, name, stash, state) do 53 | auth_init_data = {{pubkey, _}, token} = McProtocol.Crypto.Login.get_auth_init_data 54 | 55 | transitions = [ 56 | {:send_packet, 57 | %Server.Login.EncryptionBegin{ 58 | server_id: "", 59 | public_key: pubkey, 60 | verify_token: token 61 | }}, 62 | ] 63 | 64 | state = state 65 | |> Map.put(:identity, %{online: true, name: name, uuid: nil}) 66 | |> Map.put(:auth_init_data, auth_init_data) 67 | 68 | {transitions, state} 69 | end 70 | # Offline 71 | def handle_start(false, name, stash, state) do 72 | uuid = McProtocol.UUID.java_name_uuid("OfflinePlayer:" <> name) 73 | 74 | state = state 75 | |> Map.put(:identity, %{online: false, name: name, uuid: uuid}) 76 | |> Map.put(:finished, true) 77 | finish_login(stash, state) 78 | end 79 | 80 | def finish_login(stash, %{finished: true} = state) do 81 | %{name: name, uuid: uuid} = state.identity 82 | # TODO: Don't make this conversion, should be done in encoder 83 | uuid_str = McProtocol.UUID.hex_hyphen(uuid) 84 | 85 | transitions = [ 86 | {:send_packet, %Server.Login.Compress{threshold: 256}}, 87 | {:set_compression, 256}, 88 | {:send_packet, %Server.Login.Success{username: name, uuid: uuid_str}}, 89 | {:stash, 90 | %{stash | 91 | identity: state.identity, 92 | mode: :Play, 93 | play_mode: :init, 94 | }}, 95 | {:next, nil}, 96 | ] 97 | 98 | {transitions, state} 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /lib/handler/proxy.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler.Proxy do 2 | use McProtocol.Handler 3 | use GenServer 4 | 5 | alias McProtocol.Packet.{Client, Server} 6 | alias McProtocol.Connection.{Reader, Writer} 7 | 8 | defmodule Args do 9 | defstruct [ 10 | host: nil, 11 | port: nil, 12 | ] 13 | end 14 | 15 | @socket_connect_opts [:binary, packet: :raw, active: false] 16 | 17 | def enter(args = %Args{}, %{direction: :Client, mode: :Play} = stash) do 18 | {:ok, handler_pid} = GenServer.start(__MODULE__, {args, stash}) 19 | transitions = GenServer.call(handler_pid, {:enter, stash}) 20 | {transitions, handler_pid} 21 | end 22 | 23 | def handle(packet_data, stash, pid) do 24 | transitions = GenServer.call(pid, {:client_packet, packet_data, stash}) 25 | {transitions, pid} 26 | end 27 | 28 | def leave(stash, pid) do 29 | GenServer.call(pid, {:leave, stash}) 30 | GenServer.stop(pid) 31 | nil 32 | end 33 | 34 | # GenServer 35 | def init({args, _stash}) do 36 | {:ok, args} 37 | end 38 | 39 | def handle_call({:enter, stash}, _from, args) do 40 | 41 | {:ok, socket} = :gen_tcp.connect(String.to_char_list(args.host), args.port, @socket_connect_opts) 42 | 43 | control_process = self 44 | {:ok, reader} = Reader.start_link(socket, fn 45 | {:packet, data} -> GenServer.call(control_process, {:server_packet, data}) 46 | end) 47 | 48 | {:ok, writer} = Writer.start_link(socket) 49 | 50 | #Writer.write_struct( 51 | # writer, 52 | # %Client.Handshake.SetProtocol{ 53 | # 54 | # } 55 | #) 56 | 57 | state = %{ 58 | socket: socket, 59 | reader: reader, 60 | writer: writer, 61 | } 62 | 63 | {:reply, [], state} 64 | end 65 | 66 | def handle_call({:client_packet, packet, stash}, _from, state) do 67 | IO.inspect packet 68 | {:reply, :ok, state} 69 | end 70 | 71 | def handle_call({:server_packet, data}, _from, state) do 72 | IO.inspect data 73 | {:reply, :ok, state} 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/handler/reset.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler.Reset do 2 | use McProtocol.Handler 3 | 4 | alias McProtocol.Packet.{Client, Server} 5 | 6 | def enter(_, %{direction: :Client, mode: :Play, play_mode: :init} = stash) do 7 | transitions = [ 8 | {:send_packet, 9 | %Server.Play.Login{ # See the docs for McProtocol.Handler.play_mode 10 | entity_id: stash.entity_id, 11 | game_mode: 0, 12 | dimension: 0, 13 | difficulty: 0, 14 | max_players: 0, # TODO: What do? 15 | level_type: "default", 16 | reduced_debug_info: false, 17 | }}, 18 | {:stash, %{stash | play_mode: :reset}}, 19 | {:next, nil}, 20 | ] 21 | {transitions, %{first: true}} 22 | end 23 | def enter(_, %{direction: :Client, mode: :Play, play_mode: :in_world} = stash) do 24 | 25 | keep_alive_uid = 1 26 | 27 | transitions = [ 28 | {:send_packet, 29 | %Server.Play.Respawn{ # See the docs for McProtocol.Handler.play_mode 30 | dimension: 0, 31 | difficulty: 0, 32 | gamemode: 0, 33 | level_type: "default", 34 | }}, 35 | {:send_packet, 36 | %Server.Play.EntityStatus{ 37 | entity_id: stash.entity_id, 38 | entity_status: 23, # Reduced Debug Info: false 39 | }}, 40 | {:send_packet, 41 | %Server.Play.KeepAlive{ 42 | keep_alive_id: keep_alive_uid, 43 | }}, 44 | {:stash, %{stash | play_mode: :reset}} 45 | ] 46 | 47 | {transitions, %{keep_alive_id: keep_alive_uid}} 48 | end 49 | 50 | def handle(packet_in, stash, state) do 51 | packet_in = packet_in |> McProtocol.Packet.In.fetch_packet 52 | handle_packet(packet_in.packet, stash, state) 53 | end 54 | 55 | def handle_packet(%Client.Play.KeepAlive{keep_alive_id: id} = packet, _stash, 56 | %{keep_alive_id: id}) do 57 | transitions = [ 58 | {:next, nil}, 59 | ] 60 | {transitions, nil} 61 | end 62 | def handle_packet(_packet, _stash, state) do 63 | {[], state} 64 | end 65 | 66 | def respawn_into_world( 67 | respawn_state, 68 | stash = %{direction: :Client, mode: :Play, play_mode: :reset}) do 69 | # TODO: Reduced debug info 70 | 71 | respawn_base = [ 72 | {:send_packet, 73 | %Server.Play.Respawn{ 74 | dimension: respawn_state.dimension, 75 | difficulty: respawn_state.difficulty, 76 | gamemode: respawn_state.difficulty, 77 | level_type: respawn_state.level_type, 78 | }}, 79 | {:stash, %{stash | play_mode: :in_world}} 80 | ] 81 | 82 | if respawn_state.dimension == 0 do 83 | [ 84 | {:send_packet, 85 | %Server.Play.Respawn{ 86 | dimension: 1, 87 | difficulty: 0, 88 | gamemode: 0, 89 | level_type: "default", 90 | }} 91 | | respawn_base 92 | ] 93 | else 94 | respawn_base 95 | end 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/handler/status.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Handler.Status do 2 | use McProtocol.Handler 3 | 4 | alias McProtocol.Packet.Client 5 | alias McProtocol.Packet.Server 6 | 7 | def enter(args, %{direction: :Client, mode: :Status}) do 8 | {[], [response: server_list_response(Map.get(args, :response, %{}))]} 9 | end 10 | 11 | def handle(packet_data, stash, state) do 12 | packet_data = packet_data |> McProtocol.Packet.In.fetch_packet 13 | handle_packet(packet_data.packet, state) 14 | end 15 | 16 | def handle_packet(%Client.Status.PingStart{}, [response: response] = state) do 17 | reply = %Server.Status.ServerInfo{response: response} 18 | {[{:send_packet, reply}], state} 19 | end 20 | def handle_packet(%Client.Status.Ping{time: payload}, state) do 21 | reply = %Server.Status.Ping{time: payload} 22 | 23 | transitions = [ 24 | {:send_packet, reply}, 25 | :close, 26 | ] 27 | 28 | {transitions, state} 29 | end 30 | 31 | def server_list_response(response) do 32 | %{ 33 | version: %{ 34 | name: "1.9.2", 35 | protocol: 109, 36 | }, 37 | players: %{ 38 | max: 0, 39 | online: 0, 40 | }, 41 | description: %{ 42 | text: "Minecraft server in Elixir!\nhttps://github.com/McEx/McProtocol", 43 | }, 44 | } 45 | |> Map.merge(response) 46 | |> Poison.encode! 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/mc_protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol do 2 | end 3 | -------------------------------------------------------------------------------- /lib/nbt.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.NBT do 2 | 3 | @moduledoc """ 4 | Module for reading and writing NBT (http://wiki.vg/NBT) 5 | 6 | The optional argument on the read/write functions allows the root tag to be nil. 7 | This encodes as a NBT end tag. 8 | """ 9 | 10 | @type tag_name :: binary | nil 11 | 12 | @type integer_tag :: {:byte | :short | :int | :long, tag_name, integer} 13 | @type float_tag :: {:float | :double, tag_name, float} 14 | @type byte_array_tag :: {:byte_array, tag_name, binary} 15 | @type string_tag :: {:string, tag_name, binary} 16 | @type list_tag :: {:list, tag_name, [tag]} 17 | @type compound_tag :: {:compound, tag_name, [tag]} 18 | @type int_array_tag :: {:int_array, tag_name, [integer]} 19 | 20 | @type tag :: integer_tag | float_tag | byte_array_tag | string_tag | list_tag | 21 | compound_tag | int_array_tag 22 | 23 | @type t :: compound_tag 24 | 25 | @spec read(binary, boolean) :: t 26 | def read(bin, optional \\ false), do: McProtocol.NBT.Read.read(bin, optional) 27 | 28 | @spec read_gzip(binary, boolean) :: t 29 | def read_gzip(bin, optional \\ false), do: McProtocol.NBT.Read.read_gzip(bin, optional) 30 | 31 | @spec write(t, boolean) :: t 32 | def write(struct, optional \\ false), do: McProtocol.NBT.Write.write(struct, optional) 33 | 34 | defmodule Read do 35 | @moduledoc false 36 | 37 | def read_gzip(bin, optional \\ false) do 38 | decomp = :zlib.gunzip(bin) 39 | read(decomp, optional) 40 | end 41 | def read(bin, optional \\ false) do 42 | {start_tag, bin} = read_tag_id(bin) 43 | if optional and start_tag == :end do 44 | nil 45 | else 46 | read_tag(:compound, bin) 47 | end 48 | end 49 | 50 | defp read_tag_id(<<0::8, bin::binary>>), do: {:end, bin} 51 | defp read_tag_id(<<1::8, bin::binary>>), do: {:byte, bin} 52 | defp read_tag_id(<<2::8, bin::binary>>), do: {:short, bin} 53 | defp read_tag_id(<<3::8, bin::binary>>), do: {:int, bin} 54 | defp read_tag_id(<<4::8, bin::binary>>), do: {:long, bin} 55 | defp read_tag_id(<<5::8, bin::binary>>), do: {:float, bin} 56 | defp read_tag_id(<<6::8, bin::binary>>), do: {:double, bin} 57 | defp read_tag_id(<<7::8, bin::binary>>), do: {:byte_array, bin} 58 | defp read_tag_id(<<8::8, bin::binary>>), do: {:string, bin} 59 | defp read_tag_id(<<9::8, bin::binary>>), do: {:list, bin} 60 | defp read_tag_id(<<10::8, bin::binary>>), do: {:compound, bin} 61 | defp read_tag_id(<<11::8, bin::binary>>), do: {:int_array, bin} 62 | 63 | defp read_tag(tag, bin) do 64 | {name, bin} = read_type(:string, bin) 65 | {val, bin} = read_type(tag, bin) 66 | {{tag, name, val}, bin} 67 | end 68 | 69 | defp read_type(:byte, <>), do: {val, bin} 70 | defp read_type(:short, <>), do: {val, bin} 71 | defp read_type(:int, <>), do: {val, bin} 72 | defp read_type(:long, <>), do: {val, bin} 73 | defp read_type(:float, <>), do: {val, bin} 74 | defp read_type(:double, <>), do: {val, bin} 75 | defp read_type(:byte_array, bin) do 76 | <> = bin 77 | {data, bin} 78 | end 79 | defp read_type(:string, bin) do 80 | <> = bin 81 | {to_string(name), bin} 82 | end 83 | defp read_type(:list, bin) do 84 | {tag, bin} = read_tag_id(bin) 85 | <> = bin 86 | read_list_item(bin, tag, length, []) 87 | end 88 | defp read_type(:compound, bin) do 89 | {tag, bin} = read_tag_id(bin) 90 | read_compound_item(bin, tag, []) 91 | end 92 | defp read_type(:int_array, bin) do 93 | <> = bin 94 | read_int_array(bin, length, []) 95 | end 96 | 97 | defp read_list_item(bin, _, 0, results) do 98 | {results, bin} 99 | end 100 | defp read_list_item(bin, tag, num, results) when is_integer(num) and num > 0 do 101 | {val, bin} = read_type(tag, bin) 102 | read_list_item(bin, tag, num-1, results ++ [{tag, nil, val}]) 103 | end 104 | 105 | defp read_compound_item(bin, :end, results) do 106 | {results, bin} 107 | end 108 | defp read_compound_item(bin, next_tag, results) do 109 | {result, bin} = read_tag(next_tag, bin) 110 | {tag, bin} = read_tag_id(bin) 111 | read_compound_item(bin, tag, results ++ [result]) 112 | end 113 | 114 | defp read_int_array(bin, 0, results) do 115 | {results, bin} 116 | end 117 | defp read_int_array(<>, num, results) when is_integer(num) and num > 0 do 118 | read_int_array(bin, num-1, results ++ [val]) 119 | end 120 | end 121 | 122 | defmodule Write do 123 | @moduledoc false 124 | 125 | def write(struct, optional \\ false) do 126 | if (!struct or (struct == nil)) and optional do 127 | write_tag_id(:end) 128 | else 129 | {:compound, name, value} = struct 130 | IO.iodata_to_binary write_tag(:compound, name, value) 131 | end 132 | end 133 | 134 | # Writes a single tag id 135 | defp write_tag_id(:end), do: <<0::8>> 136 | defp write_tag_id(:byte), do: <<1::8>> 137 | defp write_tag_id(:short), do: <<2::8>> 138 | defp write_tag_id(:int), do: <<3::8>> 139 | defp write_tag_id(:long), do: <<4::8>> 140 | defp write_tag_id(:float), do: <<5::8>> 141 | defp write_tag_id(:double), do: <<6::8>> 142 | defp write_tag_id(:byte_array), do: <<7::8>> 143 | defp write_tag_id(:string), do: <<8::8>> 144 | defp write_tag_id(:list), do: <<9::8>> 145 | defp write_tag_id(:compound), do: <<10::8>> 146 | defp write_tag_id(:int_array), do: <<11::8>> 147 | 148 | # Writes a complete tag, including tag type, name and value 149 | defp write_tag(tag, name, value) do 150 | [write_tag_id(tag), write_type(:string, name), write_type(tag, value)] 151 | end 152 | 153 | # Writes a tag value of the supplied type 154 | defp write_type(:byte, value) when is_integer(value), do: <> 155 | defp write_type(:short, value) when is_integer(value), do: <> 156 | defp write_type(:int, value) when is_integer(value), do: <> 157 | defp write_type(:long, value) when is_integer(value), do: <> 158 | defp write_type(:float, value) when is_float(value), do: <> 159 | defp write_type(:double, value) when is_float(value), do: <> 160 | defp write_type(:byte_array, value) when is_binary(value) do 161 | [<>, value] 162 | end 163 | defp write_type(:string, value) when is_binary(value) do 164 | [<>, value] 165 | end 166 | defp write_type(:list, values) when is_list(values) do 167 | {bin, tag} = write_list_values(values) 168 | [write_tag_id(tag), write_type(:int, length(values)), bin] 169 | end 170 | defp write_type(:compound, [{tag, name, value} | rest]) do 171 | [write_tag(tag, name, value), write_type(:compound, rest)] 172 | end 173 | defp write_type(:compound, []) do 174 | write_tag_id(:end) 175 | end 176 | defp write_type(:int_array, values) when is_list(values) do 177 | [write_type(:int, length(values)), write_int_array_values(values)] 178 | end 179 | 180 | defp write_list_values(values) do 181 | {tag, nil, _} = hd(values) 182 | {write_list_values(tag, values), tag} 183 | end 184 | defp write_list_values(tag, values) do 185 | Enum.map(values, fn({f_tag, nil, val}) -> 186 | ^tag = f_tag 187 | write_type(tag, val) 188 | end) 189 | end 190 | 191 | defp write_int_array_values(values) do 192 | Enum.map(values, fn(value) -> write_type(:int, value) end) 193 | end 194 | 195 | end 196 | 197 | end 198 | -------------------------------------------------------------------------------- /lib/orchestrator/orchestrator.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Orchestrator do 2 | 3 | @moduledoc """ 4 | Orchestrates what Handler is active at what time. 5 | 6 | While an orchestrator is not able to send or receive data directly on the 7 | connection, it is able to activate handlers that does so. If you wanted 8 | to implement something equivalent to BungeeCord, you would implement it 9 | as an orchestrator. 10 | 11 | A connection has a single orchestrator which is active for the lifetime 12 | of the connection. If the orchestrator crashes, the connection is 13 | immediately closed. 14 | """ 15 | 16 | @doc """ 17 | Called when a connection is opened to create a new ochestrator. 18 | """ 19 | @callback start_link(connection_pid :: pid) :: {:ok, pid} 20 | 21 | @doc """ 22 | Called when a handler has given up control of the connection. 23 | """ 24 | @callback next(orchestrator_pid :: pid, last_handler :: 25 | {McProtocol.Handler.t, return_val :: any | :crash} | :connect) 26 | :: {McProtocol.Handler.t, any} 27 | 28 | defmacro __using__(opts) do 29 | quote do 30 | @behaviour McProtocol.Orchestrator 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/orchestrator/server.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Orchestrator.Server do 2 | 3 | @callback handle_next(module, any, any) :: {module, any, any} 4 | 5 | defmacro __using__(opts) do 6 | quote do 7 | use GenServer 8 | use McProtocol.Orchestrator 9 | 10 | def start_link(connection_pid) do 11 | GenServer.start_link(__MODULE__, connection_pid) 12 | end 13 | def next(orch_pid, last_handler) do 14 | GenServer.call(orch_pid, {:next, last_handler}) 15 | end 16 | 17 | def handle_call({:next, :connect}, _from, state) do 18 | {next_handler, args, state} = handle_next(:connect, nil, state) 19 | {:reply, {next_handler, args}, state} 20 | end 21 | def handle_call({:next, {handler_module, return}}, _from, state) do 22 | {next_handler, args, state} = handle_next(handler_module, return, state) 23 | {:reply, {next_handler, args}, state} 24 | end 25 | 26 | defoverridable [start_link: 1, next: 2] 27 | end 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/packet.ex: -------------------------------------------------------------------------------- 1 | raw_data = McData.Protocol.protocol_data("1.9.2") 2 | 3 | packets = raw_data 4 | |> Enum.filter(fn {name, _} -> name != "types" end) 5 | |> Enum.flat_map(fn {state_name, directions} -> 6 | state = McProtocol.Packet.Utils.state_name_to_ident(state_name) 7 | Enum.map(directions, fn {direction_name, data} -> 8 | direction = McProtocol.Packet.Utils.direction_name_to_ident(direction_name) 9 | types = data["types"] 10 | 11 | packet_map_type = types["packet"] 12 | packets = McProtocol.Packet.Utils.extract_packet_mappings(packet_map_type) 13 | 14 | %{ 15 | state: state, 16 | direction: direction, 17 | packets: packets, 18 | types: types, 19 | } 20 | end) 21 | end) 22 | 23 | ctx = ProtoDef.context |> McProtocol.Packet.ProtoDefTypes.add_types 24 | 25 | defmodule McProtocol.Packet do 26 | 27 | @moduledoc """ 28 | Handles encoding and decoding of packets on the lowest level. 29 | 30 | `packet bytes <-> packet struct` 31 | """ 32 | 33 | @callback read(binary) :: struct 34 | @callback write(struct) :: iolist 35 | 36 | @callback id :: non_neg_integer 37 | @callback structure :: term 38 | 39 | @callback name :: atom 40 | @callback state :: atom 41 | @callback direction :: atom 42 | 43 | @spec id_module(atom, atom, non_neg_integer) :: module 44 | @doc """ 45 | Gets the packet module for the given direction, mode and id combination. 46 | 47 | The returned module will have this module as it's behaviour. 48 | """ 49 | def id_module(direction, mode, id) 50 | 51 | @spec module_id(module) :: non_neg_integer 52 | @doc """ 53 | Gets the packet id for the given packet module. 54 | """ 55 | def module_id(module) 56 | 57 | for state_packets <- packets, {id, ident, type_name} <- state_packets.packets do 58 | module = McProtocol.Packet.Utils.make_module_name(state_packets.direction, state_packets.state, ident) 59 | 60 | def id_module(unquote(state_packets.direction), unquote(state_packets.state), unquote(id)), do: unquote(module) 61 | def module_id(unquote(module)), do: unquote(id) 62 | end 63 | 64 | def write(%{__struct__: struct_mod} = struct) do 65 | [ 66 | McProtocol.DataTypes.Encode.varint(apply(struct_mod, :id, [])), 67 | apply(struct_mod, :write, [struct]), 68 | ] 69 | end 70 | def read(direction, state, id, data) do 71 | mod = id_module(direction, state, id) 72 | {resp, ""} = apply(mod, :read, [data]) 73 | resp 74 | end 75 | def read(direction, state, data) do 76 | {id, data} = McProtocol.DataTypes.Decode.varint(data) 77 | read(direction, state, id, data) 78 | end 79 | 80 | end 81 | 82 | {:ok, doc_collector} = McProtocol.Packet.DocCollector.start_link() 83 | 84 | for mode <- packets, {id, ident, type_name} <- mode.packets do 85 | module = McProtocol.Packet.Utils.make_module_name(mode.direction, mode.state, ident) 86 | compiled = ProtoDef.compile_json_type(mode.types[type_name], ctx) 87 | fields = Enum.map(compiled.structure, fn {name, _} -> name end) 88 | 89 | doc_data = Map.merge(compiled, 90 | %{module: module, id: id, ident: ident, type_name: type_name}) 91 | McProtocol.Packet.DocCollector.collect_packet(doc_collector, doc_data) 92 | 93 | contents = quote do 94 | @behaviour McProtocol.Packet 95 | @moduledoc false 96 | 97 | defstruct unquote(Macro.escape(fields)) 98 | 99 | # Useful for debugging 100 | def compiler_output, do: unquote(Macro.escape(compiled)) 101 | 102 | def read(unquote(ProtoDef.Type.data_var)) do 103 | {resp, rest} = unquote(compiled.decoder_ast) 104 | resp = Map.put(resp, :__struct__, __MODULE__) 105 | {resp, rest} 106 | end 107 | def write(%__MODULE__{} = inp) do 108 | unquote(ProtoDef.Type.input_var) = Map.delete(inp, :__struct__) 109 | unquote(compiled.encoder_ast) 110 | end 111 | def id, do: unquote(id) 112 | def structure, do: unquote(Macro.escape(compiled.structure)) 113 | def name, do: unquote(ident) 114 | def state, do: unquote(mode.state) 115 | def direction, do: unquote(mode.direction) 116 | end 117 | 118 | Module.create(module, contents, Macro.Env.location(__ENV__)) 119 | 120 | end 121 | 122 | McProtocol.Packet.DocCollector.finish(doc_collector) 123 | -------------------------------------------------------------------------------- /lib/packet/decoder.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Packet.Decoder do 2 | alias McProtocol.Packet.Client 3 | alias McProtocol.Packet.Server 4 | 5 | @moduledoc """ 6 | 7 | ## Events emitted 8 | 9 | ### Handshake 10 | 11 | #### From client 12 | 13 | * {:set_mode, mode} - Set the next protocol state. One of :Status or :Login 14 | 15 | ### Status 16 | 17 | #### From client 18 | 19 | * {:start_ping} - Indicates the start of a new ping exchange. Should respond with :server_info 20 | * {:ping, time} - Should reply with the same event. 21 | 22 | ### Login 23 | 24 | #### From client 25 | 26 | * {:start_login, username} - Indicates the start of a player login. 27 | 28 | """ 29 | 30 | 31 | # TODO: Implement decoders for Server 32 | 33 | # Handshake 34 | 35 | def decode(%Client.Handshake.SetProtocol{} = packet) do 36 | mode = 37 | case packet.next_state do 38 | 1 -> :Status 39 | 2 -> :Login 40 | end 41 | [{:set_mode, mode}] 42 | end 43 | 44 | # Status 45 | 46 | def decode(%Client.Status.PingStart{}), do: [{:ping_start}] 47 | def decode(%Client.Status.Ping{time: payload}), do: [{:ping, payload}] 48 | 49 | # Login 50 | 51 | def decode(%Client.Login.LoginStart{username: user}), do: {:start_login, user} 52 | def decode(%Client.Login.EncryptionBegin{}) do 53 | 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /lib/packet/doc_collector.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Packet.DocCollector do 2 | use GenServer 3 | 4 | # Client 5 | 6 | defp default_build_path do 7 | Mix.Project.build_path(build_per_environment: false) <> "/PACKETS.md" 8 | end 9 | 10 | def start_link(path \\ nil) do 11 | path = path || default_build_path 12 | File.mkdir_p!(Path.dirname(path)) 13 | GenServer.start_link(__MODULE__, path) 14 | end 15 | 16 | def collect_packet(pid, data) do 17 | GenServer.call(pid, {:collect_packet, data}) 18 | end 19 | 20 | def finish(pid) do 21 | GenServer.call(pid, :finish) 22 | end 23 | 24 | # Server 25 | 26 | def init(path) do 27 | {:ok, %{path: path, packets: []}} 28 | end 29 | 30 | def handle_call({:collect_packet, data}, _from, state) do 31 | state = %{state | packets: [data | state.packets]} 32 | {:reply, :ok, state} 33 | end 34 | 35 | def handle_call(:finish, _from, state) do 36 | packets_sorted = Enum.sort( 37 | state.packets, 38 | &(Atom.to_string(&1.module) < Atom.to_string(&2.module))) 39 | 40 | out_text = Enum.map(packets_sorted, fn packet -> 41 | "Elixir." <> module = Atom.to_string(packet.module) 42 | [ 43 | "## #{module}\n", 44 | "```\n", 45 | "#{inspect(packet.structure, pretty: true)}\n", 46 | "```\n", 47 | ] 48 | end) 49 | 50 | file = File.open!(state.path, [:write, encoding: :utf8]) 51 | :ok = IO.write(file, out_text) 52 | :ok = File.close(file) 53 | 54 | {:stop, :normal, :ok, state} 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/packet/in.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Packet.In do 2 | 3 | @moduledoc """ 4 | This represents a packet being received from a Server or Client. 5 | 6 | This abstraction ensures that a packet is only decoded if it's actually needed. A proxy 7 | could look at the packet type, if it doesn't need to touch it, it could send on the raw 8 | data without even decoding it. 9 | 10 | Even if there are several layers calling fetch_packet/1, this also ensures that the 11 | packet data is only decoded once. If fetch_packet/1 is never called, the packet is never 12 | decoded. 13 | """ 14 | 15 | @type t :: %__MODULE__{} 16 | 17 | @directions [:Client, :Server] 18 | @modes [:Handshake, :Status, :Login, :Play] 19 | 20 | defstruct direction: nil, mode: nil, id: nil, module: nil, raw: nil, packet: nil 21 | 22 | @spec construct(atom, atom, binary) :: t 23 | @doc """ 24 | Constructs a new In struct, without decoding the packet data. This function would most 25 | likely be used in the part of your application that receives the packets from the network. 26 | 27 | If you use the supplied Acceptor, you should not need to use this. 28 | """ 29 | def construct(direction, mode, raw) when direction in @directions and mode in @modes do 30 | # A packet always starts with a packet ID, read that. 31 | {id, raw} = McProtocol.DataTypes.Decode.varint(raw) 32 | module = McProtocol.Packet.id_module(direction, mode, id) 33 | 34 | %__MODULE__{ 35 | direction: direction, 36 | mode: mode, 37 | id: id, 38 | module: module, 39 | raw: raw, 40 | } 41 | end 42 | 43 | @spec fetch_packet(%McProtocol.Packet.In{}) :: t 44 | @doc """ 45 | Ensures that the packet is decoded. After this call has succeeded, the packet field of 46 | the returned struct is guaranteed to be set. 47 | """ 48 | def fetch_packet(%__MODULE__{packet: nil, module: mod, raw: raw} = holder) do 49 | {packet, ""} = apply(mod, :read, [raw]) 50 | %{ holder | 51 | packet: packet, 52 | } 53 | end 54 | # We already decoded the packet, don't do it again 55 | def fetch_packet(%__MODULE__{} = holder), do: holder 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/packet/overrides.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Packet.Overrides do 2 | @moduledoc false 3 | 4 | def packet_name("RelEntityMove"), do: "EntityMove" 5 | def packet_name(name), do: name 6 | 7 | end 8 | -------------------------------------------------------------------------------- /lib/packet/proto_def_types.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Packet.ProtoDefTypes do 2 | 3 | def types, do: %{ 4 | "string" => {:inline, ["pstring", %{"countType" => "varint"}]}, 5 | "slot" => {:simple, 6 | {__MODULE__, :encode_slot}, 7 | {__MODULE__, :decode_slot}}, 8 | "position" => {:simple, 9 | {__MODULE__, :encode_position}, 10 | {__MODULE__, :decode_position}}, 11 | "entityMetadata" => {:simple, 12 | {__MODULE__, :encode_entity_metadata}, 13 | {__MODULE__, :decode_entity_metadata}}, 14 | "UUID" => {:simple, 15 | {__MODULE__, :encode_uuid}, 16 | {__MODULE__, :decode_uuid}}, 17 | "restBuffer" => {:simple, 18 | {__MODULE__, :encode_rest}, 19 | {__MODULE__, :decode_rest}}, 20 | "nbt" => {:simple, 21 | {__MODULE__, :encode_nbt}, 22 | {__MODULE__, :decode_nbt}}, 23 | "optionalNbt" => {:simple, 24 | {__MODULE__, :encode_optional_nbt}, 25 | {__MODULE__, :decode_optional_nbt}}, 26 | } 27 | 28 | def add_types(ctx) do 29 | Enum.reduce(types, ctx, fn({name, defin}, ctx) -> 30 | ProtoDef.Compiler.Context.type_add(ctx, name, defin) 31 | end) 32 | end 33 | 34 | def encode_slot(data), do: McProtocol.DataTypes.Encode.slot(data) 35 | def decode_slot(data), do: McProtocol.DataTypes.Decode.slot(data) 36 | 37 | def encode_position({x, y, z}) do 38 | <> 39 | end 40 | def decode_position(data) do 41 | <> = data 42 | {{x, y, z}, data} 43 | end 44 | 45 | def encode_entity_metadata(data), do: McProtocol.EntityMeta.write(data) 46 | def decode_entity_metadata(data), do: McProtocol.EntityMeta.read(data) 47 | 48 | def encode_uuid(data) do 49 | McProtocol.UUID.bin(data) 50 | end 51 | def decode_uuid(data) do 52 | <> = data 53 | uuid = McProtocol.UUID.from_bin(uuid_bin) 54 | {uuid, data} 55 | end 56 | 57 | def encode_rest(data), do: data 58 | def decode_rest(data), do: {data, <<>>} 59 | 60 | def encode_nbt(data), do: McProtocol.NBT.Write.write(data) 61 | def decode_nbt(data), do: McProtocol.NBT.Read.read(data) 62 | 63 | def encode_optional_nbt(data), do: McProtocol.NBT.Write.write(data, true) 64 | def decode_optional_nbt(data), do: McProtocol.NBT.Write.read(data, true) 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/packet/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Packet.Utils do 2 | @moduledoc false 3 | 4 | def state_name_to_ident("play"), do: :Play 5 | def state_name_to_ident("handshaking"), do: :Handshake 6 | def state_name_to_ident("status"), do: :Status 7 | def state_name_to_ident("login"), do: :Login 8 | 9 | def direction_name_to_ident("toClient"), do: :Server 10 | def direction_name_to_ident("toServer"), do: :Client 11 | 12 | def extract_packet_mappings(typ) do 13 | ["container", [id_mapper, name_switch]] = typ 14 | 15 | %{"name" => "name", "type" => ["mapper", %{"mappings" => id_mappings}]} = id_mapper 16 | %{"name" => "params", "type" => ["switch", %{"fields" => name_fields}]} = name_switch 17 | 18 | Enum.map(id_mappings, fn {hex_id, packet_name} -> 19 | id = parse_hex_num(hex_id) 20 | name = packet_name 21 | |> Macro.camelize 22 | |> McProtocol.Packet.Overrides.packet_name 23 | |> String.to_atom 24 | type_name = name_fields[packet_name] 25 | {id, name, type_name} 26 | end) 27 | end 28 | 29 | def parse_hex_num("0x" <> num) do 30 | {parsed, ""} = Integer.parse(num, 16) 31 | parsed 32 | end 33 | 34 | def make_module_name(direction, state, ident) do 35 | Module.concat([ 36 | McProtocol.Packet, 37 | direction, 38 | state, 39 | ident 40 | ]) 41 | end 42 | 43 | def pmap(collection, fun) do 44 | me = self 45 | collection 46 | |> Enum.map(fn (elem) -> 47 | spawn_link fn -> (send me, { self, fun.(elem) }) end 48 | end) 49 | |> Enum.map(fn (pid) -> 50 | receive do { ^pid, result } -> result end 51 | end) 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/simple_proxy/orchestrator.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.SimpleProxy.Orchestrator do 2 | use McProtocol.Orchestrator.Server 3 | 4 | def init(connection_pid) do 5 | {:ok, %{connection: connection_pid}} 6 | end 7 | 8 | def handle_next(:connect, _, state) do 9 | {McProtocol.Handler.Handshake, %{}, state} 10 | end 11 | def handle_next(McProtocol.Handler.Handshake, :Status, state) do 12 | # TODO: query proxied 13 | {McProtocol.Handler.Status, %{}, state} 14 | end 15 | def handle_next(McProtocol.Handler.Handshake, :Login, state) do 16 | {McProtocol.Handler.Login, %{}, state} 17 | end 18 | def handle_next(McProtocol.Handler.Login, _, state) do 19 | # {McProtocol.Handler.Kick, %{text: "boo"}, state} 20 | args = %McProtocol.Handler.Proxy.Args{ 21 | host: "localhost", 22 | port: 25564, 23 | } 24 | {McProtocol.Handler.Proxy, args, state} 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/simple_proxy/task.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.McProtocol.Proxy do 2 | use Mix.Task 3 | 4 | @shortdoc "Starts a simple minecraft proxy. For testing, not production." 5 | 6 | def run(args) do 7 | #spawn fn -> 8 | # McProtocol.Acceptor.SimpleAcceptor.accept(25565, &handle_connect(&1)) 9 | #end 10 | spawn fn -> acceptor end 11 | 12 | Mix.Task.run "run", run_args 13 | end 14 | 15 | def acceptor do 16 | McProtocol.Acceptor.SimpleAcceptor.accept( 17 | 25565, 18 | fn socket -> 19 | McProtocol.Connection.Manager.start_link( 20 | socket, :Client, 21 | McProtocol.SimpleProxy.Orchestrator) 22 | end, 23 | fn pid, _socket -> 24 | McProtocol.Connection.Manager.start_reading(pid) 25 | end 26 | ) 27 | end 28 | 29 | def handle_connect(socket) do 30 | McProtocol.Acceptor.Connection.start_link(socket, :Client, 31 | McProtocol.SimpleProxy.Orchestrator) 32 | end 33 | 34 | defp run_args do 35 | if iex_running?, do: [], else: ["--no-halt"] 36 | end 37 | 38 | defp iex_running? do 39 | Code.ensure_loaded(IEx) && IEx.started? 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/transport/read.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Transport.Read do 2 | 3 | defstruct buffer: "", packet_length: nil, compression: nil, encryption: nil 4 | 5 | # TODO: Make these apply to buffered data when set 6 | def set_encryption(%__MODULE__{} = state, encr), do: %{ state | encryption: encr } 7 | def set_compression(%__MODULE__{} = state, compr), do: %{ state | compression: compr } 8 | 9 | def initial_state do 10 | %__MODULE__{} 11 | end 12 | 13 | @doc """ 14 | This will take the received data and a read state. 15 | It returns a list of packet data and a updated read state. 16 | """ 17 | def process(data, state) do 18 | {data, state} = decrypt(data, state) 19 | state = %{ state | buffer: state.buffer <> data } 20 | decode(state) 21 | end 22 | 23 | defp decompress(data, state = %{ compression: nil }) do 24 | {data, state} 25 | end 26 | defp decompress(data, state = %{ compression: _threshold }) do 27 | {packet_length, data} = McProtocol.DataTypes.Decode.varint(data) 28 | # As per the minecraft protocol, if the varint after the total packet length is 0, 29 | # the packet is not compressed. 30 | if packet_length == 0 do 31 | {data, state} 32 | else 33 | {inflate(data), state} 34 | end 35 | end 36 | 37 | defp inflate(data) do 38 | z = :zlib.init 39 | :zlib.inflateInit(z) 40 | infl = :zlib.inflate(z, data) 41 | :zlib.inflateEnd(z) 42 | :zlib.close(z) 43 | infl 44 | end 45 | 46 | defp decrypt(data, state = %{ encryption: nil }) do 47 | {data, state} 48 | end 49 | defp decrypt(data, state = %{ encryption: enc }) do 50 | {enc, data} = McProtocol.Crypto.Transport.decrypt(data, enc) 51 | {data, %{ state | encryption: enc }} 52 | end 53 | 54 | # Quick out. 55 | defp decode(state = %{ packet_length: nil, buffer: "" }) do 56 | {[], state} 57 | end 58 | # We haven't decoded a packet length yet, if there is enough data, do that and 59 | # call the decode function with the new state. 60 | defp decode(state = %{ packet_length: nil }) do 61 | case McProtocol.DataTypes.Decode.varint?(state.buffer) do 62 | {:ok, {len, rest}} -> 63 | %{ state | 64 | packet_length: len, 65 | buffer: rest 66 | } 67 | |> decode 68 | :incomplete -> {[], state} 69 | end 70 | end 71 | # We decoded the packet length, but we haven't received enough data to decode 72 | # the entire packet yet. 73 | defp decode(state = %{ packet_length: len, buffer: buf }) when len > byte_size(buf) do 74 | {[], state} 75 | end 76 | # We decoded a packet length, and we have enough data to decode a packet. 77 | defp decode(state) do 78 | len = state.packet_length 79 | 80 | packet_data = binary_part(state.buffer, 0, state.packet_length) 81 | packet_rest = binary_part(state.buffer, len, byte_size(state.buffer) - len) 82 | 83 | {packet_data, state} = decompress(packet_data, state) 84 | 85 | {packets, state} = decode(%{ state | buffer: packet_rest, packet_length: nil }) 86 | {[packet_data | packets], state} 87 | end 88 | 89 | # This performs the final processing of the packet data. 90 | # This does nothing for now. 91 | defp decode_packet(packet_data, state) do 92 | {packet_data, state} 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /lib/transport/write.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Transport.Write do 2 | 3 | defstruct compression: nil, encryption: nil 4 | 5 | def set_encryption(%__MODULE__{} = state, encr), do: %{ state | encryption: encr } 6 | def set_compression(%__MODULE__{} = state, compr), do: %{ state | compression: compr } 7 | 8 | def initial_state do 9 | %__MODULE__{} 10 | end 11 | 12 | def process(data, state) do 13 | data_len = IO.iodata_length(data) 14 | 15 | compressed = compress(data, data_len, state) 16 | compressed_len = IO.iodata_length(compressed) 17 | 18 | compressed_len_encoded = McProtocol.DataTypes.Encode.varint(compressed_len) 19 | complete = [compressed_len_encoded, compressed] 20 | 21 | {compressed, state} = encrypt(complete, state) 22 | 23 | {compressed, state} 24 | end 25 | 26 | defp encrypt(data, state = %{ encryption: nil }) do 27 | {data, state} 28 | end 29 | defp encrypt(data, state = %{ encryption: encr_data }) do 30 | data = IO.iodata_to_binary(data) 31 | {encr_data, ciphertext} = McProtocol.Crypto.Transport.encrypt(data, encr_data) 32 | {ciphertext, %{ state | encryption: encr_data }} 33 | end 34 | 35 | defp compress(data, _data_size, %{ compression: nil }) do 36 | data 37 | end 38 | defp compress(data, data_size, %{ compression: thr }) when data_size > thr do 39 | compressed = deflate(data) 40 | data_size_encoded = McProtocol.DataTypes.Encode.varint(data_size) 41 | [data_size_encoded, compressed] 42 | end 43 | defp compress(data, _data_size, _state) do 44 | [0, data] 45 | end 46 | 47 | defp deflate(data) do 48 | # TODO: Reuse zstream 49 | z = :zlib.open 50 | :zlib.deflateInit(z) 51 | compr = :zlib.deflate(z, data, :finish) 52 | :zlib.deflateEnd(z) 53 | :zlib.close(z) 54 | compr 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /lib/util/generate_rsa.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Util.GenerateRSA do 2 | 3 | @moduledoc """ 4 | Utilities for generating RSA keys. 5 | """ 6 | 7 | @doc """ 8 | Generates an RSA key with the size of key_size. 9 | 10 | Calls out to the openssl commend line executable for key generation. 11 | 12 | Returns an RSA private key in the form of a :RSAPrivateKey record. 13 | """ 14 | def gen(key_size) do 15 | {command, args} = gen_command(key_size) 16 | {output, 0} = System.cmd(command, args) 17 | 18 | split_output = output 19 | |> String.split("\n") 20 | 21 | {_, raw_values} = Enum.reduce(split_output, {nil, %{}}, fn(line, {mode, map}) -> 22 | case is_key(line) do 23 | :skip -> {mode, map} 24 | false -> 25 | {mode, Map.put(map, mode, [line | Map.fetch!(map, mode)])} 26 | key -> 27 | {short_key, list_beginning} = decode_key(key) 28 | {short_key, Map.put(map, short_key, list_beginning)} 29 | end 30 | end) 31 | 32 | values = raw_values 33 | |> Enum.map(fn {k, v} -> 34 | value = v 35 | |> Enum.reverse 36 | |> Enum.map(&String.strip/1) 37 | |> Enum.join 38 | |> String.replace(":", "") 39 | {k, value} 40 | end) 41 | |> Enum.into(%{}) 42 | 43 | [pub_exp_text, _] = values["publicExponent"] |> String.split(" ") 44 | {pub_exp, ""} = pub_exp_text |> Integer.parse 45 | 46 | modulus = values["modulus"] |> Base.decode16!(case: :lower) |> as_num 47 | priv_exp = values["privateExponent"] |> Base.decode16!(case: :lower) |> as_num 48 | prime_1 = values["prime1"] |> Base.decode16!(case: :lower) |> as_num 49 | prime_2 = values["prime2"] |> Base.decode16!(case: :lower) |> as_num 50 | exp_1 = values["exponent1"] |> Base.decode16!(case: :lower) |> as_num 51 | exp_2 = values["exponent2"] |> Base.decode16!(case: :lower) |> as_num 52 | coeff = values["coefficient"] |> Base.decode16!(case: :lower) |> as_num 53 | 54 | {:RSAPrivateKey, :"two-prime", 55 | modulus, pub_exp, priv_exp, 56 | prime_1, prime_2, exp_1, exp_2, coeff, 57 | :asn1_NOVALUE} 58 | end 59 | 60 | defp as_num(bin) do 61 | size = byte_size(bin) 62 | <> = bin 63 | num 64 | end 65 | 66 | defp gen_command(bits) when is_number(bits) do 67 | {"openssl", 68 | ["genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:#{bits}", "-text"]} 69 | end 70 | 71 | defp decode_key(key) do 72 | [key, list_first] = String.split(key, ":") 73 | {key, [list_first]} 74 | end 75 | 76 | defp is_key("-----BEGIN PRIVATE KEY-----"), do: "privateKeyBlock:" 77 | defp is_key("-----END PRIVATE KEY-----"), do: :skip 78 | defp is_key(""), do: :skip 79 | defp is_key(str) do 80 | cond do 81 | String.starts_with?(str, " ") -> false 82 | match?({:ok, _}, Base.decode64(str)) -> false 83 | true -> str#binary_part(str, 0, byte_size(str) - 1) 84 | end 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/uuid.ex: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.UUID do 2 | 3 | @moduledoc """ 4 | Utilities for working with UUIDs in Minecraft. 5 | 6 | Can deal with both hyphenated, unhyphenated and binary UUIDs. 7 | """ 8 | 9 | @type t :: %__MODULE__{} 10 | 11 | defstruct bin: nil, hex: nil 12 | 13 | def uuid4 do 14 | from_hex(String.replace(UUID.uuid4, "-", "")) 15 | end 16 | 17 | def java_name_uuid(name) do 18 | <> = :crypto.hash(:md5, name) 19 | bin_uuid = <> 20 | 21 | from_bin(bin_uuid) 22 | end 23 | 24 | @spec from_hex(str) :: t | :error when str: binary 25 | def from_hex(hex_data) when byte_size(hex_data) == 32 do 26 | case Base.decode16(hex_data, case: :lower) do 27 | {:ok, bin_data} -> %McProtocol.UUID { hex: hex_data, bin: bin_data } 28 | _ -> :error 29 | end 30 | end 31 | def from_hex(str) when byte_size(str) == 36 do 32 | from_hex(String.replace(str, "-", ",")) 33 | end 34 | 35 | @spec from_bin(bin) :: t when bin: binary 36 | def from_bin(bin_data) when byte_size(bin_data) == 16 do 37 | %McProtocol.UUID { 38 | bin: bin_data, 39 | hex: Base.encode16(bin_data, case: :lower) 40 | } 41 | end 42 | 43 | def hex(%McProtocol.UUID{hex: hex_data}), do: hex_data 44 | def hex_hyphen(%McProtocol.UUID{hex: hex_data}), do: hyphenize_string(hex_data) 45 | def bin(%McProtocol.UUID{bin: bin_data}), do: bin_data 46 | 47 | defp hyphenize_string(uuid) when byte_size(uuid) == 32 do 48 | uuid 49 | |> String.to_char_list 50 | |> List.insert_at(20, "-") |> List.insert_at(16, "-") |> List.insert_at(12, "-") |> List.insert_at(8, "-") 51 | |> List.to_string 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :mc_protocol, 6 | version: "0.0.2", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | description: description, 11 | package: package, 12 | deps: deps, 13 | docs: docs] 14 | end 15 | 16 | # Configuration for the OTP application 17 | # 18 | # Type "mix help compile.app" for more information 19 | def application do 20 | [applications: [:logger]] 21 | end 22 | 23 | # Dependencies can be Hex packages: 24 | # 25 | # {:mydep, "~> 0.3.0"} 26 | # 27 | # Or git/path repositories: 28 | # 29 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 30 | # 31 | # Type "mix help deps" for more examples and options 32 | defp deps do 33 | [{:uuid, "~> 1.1"}, 34 | {:proto_def, "~> 0.0.4"}, 35 | {:mc_data, "~> 0.0.5"}, 36 | {:credo, "~> 0.3", only: [:dev, :test]}, 37 | {:earmark, "~> 0.1", only: :dev}, 38 | {:ex_doc, "~> 0.11", only: :dev}, 39 | {:benchfella, "~> 0.3.0", only: [:dev, :test]}] 40 | end 41 | 42 | defp description do 43 | """ 44 | Implementation of the Minecraft protocol in Elixir. 45 | Aims to provide functional ways to interact with the minecraft protocol on all levels, including packet reading and writing, encryption, compression, authentication and more. 46 | """ 47 | end 48 | 49 | defp package do 50 | [ 51 | files: ["lib", "priv", "mix.exs", "README*", "LICENSE*"], 52 | maintainers: ["hansihe"], 53 | licenses: ["MIT"], 54 | links: %{"GitHub" => "https://github.com/McEx/McProtocol"}, 55 | ] 56 | end 57 | 58 | defp docs do 59 | [ 60 | extras: ["_build/shared/PACKETS.md"], 61 | ] 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"benchfella": {:hex, :benchfella, "0.3.4", "41d2c017b361ece5225b5ba2e3b30ae53578c57c6ebc434417b4f1c2c94cf4f3", [:mix], []}, 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, 3 | "credo": {:hex, :credo, "0.7.3", "9827ab04002186af1aec014a811839a06f72aaae6cd5eed3919b248c8767dbf3", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]}, 4 | "cutkey": {:git, "https://github.com/imtal/cutkey.git", "67375ea3d2ee3c729bc9a93baca447203930fd12", []}, 5 | "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, 6 | "estree": {:hex, :estree, "2.5.1", "c93a8fa8a29886e6a6f6c489ba6dc949b998d2985b189967e41e69a92b58e846", [:mix], []}, 7 | "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]}, 8 | "json_minecraft_data": {:git, "https://github.com/PrismarineJS/minecraft-data.git", "a13abc7f468698e5e37592dca6faefdcd6527cb4", [ref: "a13abc7f468698e5e37592dca6faefdcd6527cb4"]}, 9 | "mc_data": {:hex, :mc_data, "0.0.5", "e6fbec66e8aafb065e1840cf9c0e953fa9f56605d1f756fe11f8daa58f38351a", [:mix], [{:poison, "~> 2.0", [hex: :poison, optional: false]}]}, 10 | "minecraft_data": {:git, "https://github.com/PrismarineJS/minecraft-data.git", "a13abc7f468698e5e37592dca6faefdcd6527cb4", []}, 11 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 12 | "proto_def": {:hex, :proto_def, "0.0.4", "5a56ca4ce54def73f8afe76523db678117f46d1bffa748a576606a18d24a0690", [:mix], [{:estree, "~> 2.3", [hex: :estree, optional: false]}, {:poison, "~> 2.0", [hex: :poison, optional: false]}]}, 13 | "uuid": {:hex, :uuid, "1.1.7", "007afd58273bc0bc7f849c3bdc763e2f8124e83b957e515368c498b641f7ab69", [:mix], []}} 14 | -------------------------------------------------------------------------------- /test/bigtest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/McEx/McProtocol/4b3011338af573c6f583f541c410fb23574f4c10/test/bigtest.nbt -------------------------------------------------------------------------------- /test/elixir_mc_protocol/entity_metadata_test.exs: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.EntityMetaTest do 2 | use ExUnit.Case, async: true 3 | alias McProtocol.EntityMeta 4 | 5 | test "type idx conversion" do 6 | assert EntityMeta.type_idx(:varint) == 1 7 | assert EntityMeta.idx_type(1) == :varint 8 | assert EntityMeta.type_idx(:position) == 8 9 | assert EntityMeta.idx_type(8) == :position 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/elixir_mc_protocol/nbt_test.exs: -------------------------------------------------------------------------------- 1 | defmodule McProtocol.NBTTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "decode and encode bigtest.nbt" do 5 | data_compressed = File.read!("test/bigtest.nbt") 6 | data = :zlib.gunzip(data_compressed) 7 | 8 | {nbt, ""} = McProtocol.NBT.read(data) 9 | data_encoded = McProtocol.NBT.write(nbt) 10 | 11 | assert data === data_encoded 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /test/elixir_mc_protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule McProtocolTest do 2 | use ExUnit.Case 3 | doctest McProtocol 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------