diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73e0c2d --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +ERLC=/usr/local/bin/erlc +ERLCFLAGS=-o +SRCDIR=. +BEAMDIR=./ebin + +compile: + $(ERLC) $(ERLCFLAGS) $(BEAMDIR) $(SRCDIR)/*.erl ; + +run: compile + erl -pa ./ebin/ -boot start_sasl -s minecraft_server + +server_download: + @mkdir -p server; cd server; test -f minecraft_server.jar || curl -O "https://s3.amazonaws.com/MinecraftDownload/launcher/minecraft_server.jar" + +server: server_download + @cd server; java -Xmx1024M -Xms1024M -jar minecraft_server.jar nogui diff --git a/minecraft.erl b/minecraft.erl new file mode 100644 index 0000000..a60b8ef --- /dev/null +++ b/minecraft.erl @@ -0,0 +1,74 @@ +-module(minecraft). +-compile(export_all). + +-define(KEEPALIVE, 16#00). +-define(LOGIN, 16#01). +-define(HANDSHAKE, 16#02). +-define(TIME_UPDATE, 16#04). +-define(KICK, 16#FF). + +-define(HANDSHAKE_MESSAGE(Username), list_to_binary([<>, string_to_unicode_binary(Username)])). + +-define(KEEPALIVE_PATTERN, <>). +-define(HANDSHAKE_PATTERN, <>). +-define(KICK_PATTERN, <>). + +start(MinecraftHost) -> + application:start(inets), + application:start(crypto), + application:start(public_key), + application:start(ssl), + Pid = spawn(fun() -> connect(MinecraftHost) end), + register(minecraft_socket, Pid). + +connect(MinecraftHost) -> + {ok,Socket} = gen_tcp:connect(MinecraftHost, 25565, [binary, {packet, 0}]), + loop(Socket, []). + +loop(Socket, Listeners) -> + receive + {tcp,Socket,Bin} -> + case Bin of + ?KEEPALIVE_PATTERN -> gen_tcp:send(Socket, <>); + ?HANDSHAKE_PATTERN -> notify(Listeners, {handshake, utf16_binary_to_list(Hash)}); + ?KICK_PATTERN -> notify(Listeners, {kick, utf16_binary_to_list(Reason)}) + end, + loop(Socket, Listeners); + {tcp_send,Packet} -> + gen_tcp:send(Socket,Packet), + loop(Socket, Listeners); + {tcp_listen, Listener} -> + loop(Socket, [Listener|Listeners]) + end. + +notify([], _) -> []; +notify([Listener|Rest], Message) -> + Listener ! Message, + notify(Rest, Message). + +login(User, Password) -> + case login_request(User, Password) of + {ok, {_, 200, _}, _, Body} -> + [_,_,_,_SessionID] = string:tokens(Body, ":") + end, + minecraft_socket ! {tcp_send, ?HANDSHAKE_MESSAGE(User)}. + +login_request(User, Password) -> + httpc:request(post, + {"https://login.minecraft.net/", + [], + "application/x-www-form-urlencoded", + lists:flatten(io_lib:format("user=~s&password=~s&version=9999", [User, Password]))}, + [], []). + +listen() -> + minecraft_socket ! {tcp_listen, self()}. + +%% Utility functions +string_to_unicode_binary(Str) -> + StrLen = string:len(Str), + StrBin = unicode:characters_to_binary(Str, utf8, utf16), + <>. + +utf16_binary_to_list(Bin) -> + unicode:characters_to_list(Bin, utf16). diff --git a/minecraft_server.erl b/minecraft_server.erl new file mode 100644 index 0000000..1805f59 --- /dev/null +++ b/minecraft_server.erl @@ -0,0 +1,113 @@ +-module(minecraft_server). + +-behaviour(gen_server). +-define(SERVER, ?MODULE). + +-define(KEEPALIVE, 16#00). +-define(LOGIN, 16#01). +-define(HANDSHAKE, 16#02). +-define(TIME_UPDATE, 16#04). +-define(KICK, 16#FF). + +-define(HANDSHAKE_MESSAGE(Username), list_to_binary([<>, minecraft:string_to_unicode_binary(Username)])). + +-define(KEEPALIVE_PATTERN, <>). +-define(HANDSHAKE_PATTERN, <>). +-define(KICK_PATTERN, <>). + +%% ------------------------------------------------------------------ +%% API Function Exports +%% ------------------------------------------------------------------ + +-export([start_link/0]). +-export([start/0, connect/2, connect_local/0]). + +%% ------------------------------------------------------------------ +%% gen_server Function Exports +%% ------------------------------------------------------------------ + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). + +-record(state, { + socket, + session_id, + user, + listeners + }). + +%% ------------------------------------------------------------------ +%% API Function Definitions +%% ------------------------------------------------------------------ + +start_link() -> + application:start(inets), + application:start(crypto), + application:start(public_key), + application:start(ssl), + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +start() -> + start_link(). + +connect(IP, Port) -> + gen_server:call(?MODULE, {connect, IP, Port}), + case minecraft:login_request("user", "password") of + {ok, {{_, 200, _}, _, Body}} -> + [_,_,_,SessionID] = string:tokens(Body, ":") + end, + gen_server:cast(?MODULE, {handshake_user, "spanx75", SessionID}). + +connect_local() -> + connect("127.0.0.1", 25565). + +%% ------------------------------------------------------------------ +%% gen_server Function Definitions +%% ------------------------------------------------------------------ + +init([]) -> + process_flag(trap_exit, true), + %%io:format("Connecting to ~s~n", [MinecraftHost]), + %%{ok, Socket} = gen_tcp:connect(MinecraftHost, 25565, [binary, {packet, 0}]), + %%o:format("Socket: ~p~n", [Socket]), + {ok, #state{listeners = []}}. + +handle_call({connect, IP, Port}, _From, State) -> + case gen_tcp:connect(IP, Port, [binary, {active, once}]) of + {ok, Socket} -> + %gen_tcp:controlling_process(Socket, ?MODULE), + NewState = State#state{socket=Socket}, + io:format("Socket ar: ~p~n", [Socket]), + {reply, ok, NewState}; + {error, Reason} -> + {reply, {error, Reason}, State} + end; + +handle_call(_Request, _From, State) -> + {noreply, ok, State}. + +handle_cast({handshake_user, User, SessionID}, State) -> + gen_tcp:send(State#state.socket, ?HANDSHAKE_MESSAGE(User)), + {noreply, State#state{session_id = SessionID, user = User}}; + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({tcp, _, ?HANDSHAKE_PATTERN}, State = #state{session_id = SessionID, user = User}) -> + StringHash = minecraft:utf16_binary_to_list(Hash), + io:format("Handshake hash: ~p~n", [StringHash]), + case httpc:request(get, + {lists:flatten(io_lib:format("http://session.minecraft.net/game/joinserver.jsp?user=~s&sessionId=~s&serverId=~s", [User, SessionID, StringHash])), []}, [], []) of + {ok, {{_, 200, _}, _, Body}} -> + io:format("Body: ~p~n", [Body]) + end, + {noreply, State}; + +handle_info(Info, State) -> + io:format("Received info: ~p~n", [Info]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}.