From 9ba7e25d64efbc22397f8b39ce853704100e5faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sat, 18 Sep 2010 22:53:15 +0200 Subject: [PATCH] egs_login_server: Abstract the login server. Share the code for login server and game auth. --- ebin/egs.app | 2 +- src/egs_char_select.erl | 2 +- src/egs_game.erl | 2 +- src/egs_game_server.erl | 5 +- src/egs_login.erl | 75 ++++++++++++++---- src/egs_login_server.erl | 38 +++++++++ src/egs_network.erl | 11 ++- src/egs_sup.erl | 6 +- src/psu/psu_login.erl | 165 --------------------------------------- src/psu/psu_npc.erl | 2 +- 10 files changed, 117 insertions(+), 191 deletions(-) create mode 100644 src/egs_login_server.erl delete mode 100644 src/psu/psu_login.erl diff --git a/ebin/egs.app b/ebin/egs.app index 06b8eea..0a8a1b0 100644 --- a/ebin/egs.app +++ b/ebin/egs.app @@ -7,6 +7,7 @@ egs_app, egs_sup, egs_game_server, + egs_login_server, egs_exit_mon, egs_user_model, egs_network, @@ -15,7 +16,6 @@ egs_game, reloader, psu_game, - psu_login, psu_patch, psu_instance, psu_proto, diff --git a/src/egs_char_select.erl b/src/egs_char_select.erl index defa911..82a06f7 100644 --- a/src/egs_char_select.erl +++ b/src/egs_char_select.erl @@ -1,6 +1,6 @@ %% @author Loïc Hoguin %% @copyright 2010 Loïc Hoguin. -%% @doc Game server's character selection callback module. +%% @doc Character selection callback module. %% %% This file is part of EGS. %% diff --git a/src/egs_game.erl b/src/egs_game.erl index e7d77fb..84b44f8 100644 --- a/src/egs_game.erl +++ b/src/egs_game.erl @@ -1,6 +1,6 @@ %% @author Loïc Hoguin %% @copyright 2010 Loïc Hoguin. -%% @doc Game server's client authentication callback module. +%% @doc Game callback module. %% %% This file is part of EGS. %% diff --git a/src/egs_game_server.erl b/src/egs_game_server.erl index 1ef3f34..58bfca7 100644 --- a/src/egs_game_server.erl +++ b/src/egs_game_server.erl @@ -58,6 +58,7 @@ on_exit(Pid) -> %% @todo Handle keepalive messages globally? init(Socket) -> egs_game_server_exit_mon ! {link, self()}, - psu_proto:send_0202(Socket, 0), timer:send_interval(5000, {egs, keepalive}), - egs_network:recv(<< >>, egs_login, {state, Socket}). + TmpGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1), + psu_proto:send_0202(Socket, TmpGID), + egs_network:recv(<< >>, egs_login, {state, Socket, TmpGID}). diff --git a/src/egs_login.erl b/src/egs_login.erl index 2f4064a..3c0c467 100644 --- a/src/egs_login.erl +++ b/src/egs_login.erl @@ -1,6 +1,6 @@ %% @author Loïc Hoguin %% @copyright 2010 Loïc Hoguin. -%% @doc Game server's client authentication callback module. +%% @doc Log in and authentication callback module. %% %% This file is part of EGS. %% @@ -22,17 +22,9 @@ %% @todo This header is only included because of egs_user_model. We don't want that here. -include("include/records.hrl"). +-include("include/network.hrl"). --record(state, {socket}). - -%% @doc Initialize the game state and start receiving messages. -%% @todo Link against egs_exit_mon. -%% @todo Handle keepalive messages globally. -%% @todo Probably have init where start_link is. -init(Socket) -> - psu_proto:send_0202(Socket, 0), - timer:send_interval(5000, {egs, keepalive}), - egs_network:recv(<< >>, ?MODULE, #state{socket=Socket}). +-record(state, {socket, gid}). %% @doc Don't keep alive here, authentication should go fast. keepalive(_State) -> @@ -48,6 +40,46 @@ info(_Msg, _State) -> cast(_Command, _Data, _State) -> ok. +%% Raw commands. +%% @todo Move all of them to events. + +%% Game server info request handler. +%% @todo Remove the dependency on network.hrl through configuration files. +raw(16#0217, _Data, #state{socket=Socket, gid=GID}) -> + IP = ?GAME_IP, + Port = ?GAME_PORT, + Packet = << 16#02160300:32, 0:192, GID:32/little-unsigned-integer, 0:64, IP/binary, Port:32/little-unsigned-integer >>, + psu_proto:packet_send(Socket, Packet), + ssl:close(Socket), + closed; + +%% @doc Authentication request handler. Currently always succeed. +%% Use the temporary session ID as the GID for now. +%% Use username and password as a folder name for saving character data. +%% @todo Handle real GIDs whenever there's real authentication. GID is the second SessionID in the reply. +%% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP. +raw(16#0219, << _:352, UsernameBlob:192/bits, PasswordBlob:192/bits, _/bits >>, #state{socket=Socket}) -> + Username = re:replace(UsernameBlob, "\\0", "", [global, {return, binary}]), + Password = re:replace(PasswordBlob, "\\0", "", [global, {return, binary}]), + io:format("auth success for ~s ~s~n", [Username, Password]), + RealGID = 10000000 + mnesia:dirty_update_counter(counters, gid, 1), + Auth = crypto:rand_bytes(4), + Folder = << Username/binary, "-", Password/binary >>, + Time = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), + egs_user_model:write(#egs_user_model{id=RealGID, pid=self(), socket=Socket, state={wait_for_authentication, Auth}, time=Time, folder=Folder}), + Packet = << 16#02230300:32, 0:192, RealGID:32/little-unsigned-integer, 0:64, RealGID:32/little-unsigned-integer, Auth:32/bits >>, + psu_proto:packet_send(Socket, Packet); + +%% @doc MOTD request handler. Handles both forms of MOTD requests, US and JP. Page number starts at 0. +%% @todo Currently ignore the language and send the same MOTD file to everyone. Language is 8 bits next to Page. +raw(Command, << _:352, Page:8, _/bits >>, #state{socket=Socket}) when Command =:= 16#0226; Command =:= 16#023f -> + {ok, File} = file:read_file("priv/psu_login/motd.txt"), + Tokens = re:split(File, "\n."), + MOTD = << << Line/binary, "\n", 0 >> || Line <- lists:sublist(Tokens, 1 + Page * 15, 15) >>, + NbPages = 1 + length(Tokens) div 15, + Packet = << 16#0225:16, 0:304, NbPages:8, Page:8, 16#8200:16/unsigned-integer, MOTD/binary, 0:16 >>, + psu_proto:packet_send(Socket, Packet); + %% @doc Dismiss all raw commands with a log notice. %% @todo Have a log event handler instead. raw(Command, _Data, _State) -> @@ -55,9 +87,24 @@ raw(Command, _Data, _State) -> %% Events. -%% @todo Check the client version info here too. Not just on login. -event({system_client_version_info, _Language, _Platform, _Version}, _State) -> - ok; +%% @doc Reject version < 2.0009.2. +%% @todo Reject wrong platforms too. +event({system_client_version_info, _Language, _Platform, Version}, #state{socket=Socket, gid=GID}) -> + if Version >= 2009002 -> ignore; true -> + Website = << "http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1" >>, + Size = byte_size(Website), + WebsiteLen = Size + 1, + PaddingSize = 8 * (512 - Size), + WebsitePacket = << 16#02310300:32, 16#ffff0000:32, 16#00000f00:32, GID:32/little, 0:64, 16#00000f00:32, GID:32/little, 0:64, + WebsiteLen:32/little, Website/binary, 0:PaddingSize >>, + psu_proto:packet_send(Socket, WebsitePacket), + {ok, File} = file:read_file("priv/psu_login/error_version.txt"), + ErrorLen = byte_size(File) div 2 + 2, + ErrorPacket = << 16#02230300:32, 0:160, 16#00000f00:32, GID:32/little, 0:96, 16#f9dbce73:32, 3:32/little, 0:48, ErrorLen:16/little, File/binary, 0:16 >>, + psu_proto:packet_send(Socket, ErrorPacket), + ssl:close(Socket), + closed + end; %% @doc Authenticate the user by pattern matching its saved state against the key received. %% If the user is authenticated, send him the character flags list. diff --git a/src/egs_login_server.erl b/src/egs_login_server.erl new file mode 100644 index 0000000..8e14310 --- /dev/null +++ b/src/egs_login_server.erl @@ -0,0 +1,38 @@ +%% @author Loïc Hoguin +%% @copyright 2010 Loïc Hoguin. +%% @doc Login server module. +%% +%% This file is part of EGS. +%% +%% EGS is free software: you can redistribute it and/or modify +%% it under the terms of the GNU Affero General Public License as +%% published by the Free Software Foundation, either version 3 of the +%% License, or (at your option) any later version. +%% +%% EGS is distributed in the hope that it will be useful, +%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +%% GNU Affero General Public License for more details. +%% +%% You should have received a copy of the GNU Affero General Public License +%% along with EGS. If not, see . + +-module(egs_login_server). +-export([start_link/1, on_exit/1, init/1]). + +%% @spec start_link(Port) -> {ok,Pid::pid()} +%% @doc Start the login server. +start_link(Port) -> + Pid = spawn(egs_network, listen, [Port, ?MODULE]), + {ok, Pid}. + +%% @spec on_exit(Pid) -> ok +%% @doc Nothing to do for the login server. +on_exit(_Pid) -> + ok. + +%% @doc Initialize the game state and start receiving messages. +init(Socket) -> + TmpGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1), + psu_proto:send_0202(Socket, TmpGID), + egs_network:recv(<< >>, egs_login, {state, Socket, TmpGID}). diff --git a/src/egs_network.erl b/src/egs_network.erl index c381cad..3f5981d 100644 --- a/src/egs_network.erl +++ b/src/egs_network.erl @@ -25,7 +25,7 @@ %% @doc Listen for connections. listen(Port, CallbackMod) -> - error_logger:info_report(io_lib:format("started a listener for ~p on port ~b", [CallbackMod, Port])), + error_logger:info_report(io_lib:format("listener started for ~p on port ~b", [CallbackMod, Port])), {ok, LSocket} = ssl:listen(Port, ?OPTIONS), ?MODULE:accept(LSocket, CallbackMod). @@ -50,8 +50,11 @@ recv(SoFar, CallbackMod, State) -> receive {ssl, _Any, Data} -> {Commands, Rest} = split(<< SoFar/bits, Data/bits >>, []), - {ok, NextCallbackMod, NewState} = dispatch(Commands, CallbackMod, CallbackMod, State), - ?MODULE:recv(Rest, NextCallbackMod, NewState); + case dispatch(Commands, CallbackMod, CallbackMod, State) of + {ok, NextCallbackMod, NewState} -> + ?MODULE:recv(Rest, NextCallbackMod, NewState); + closed -> closed + end; {ssl_closed, _} -> ssl_closed; %% exit {ssl_error, _, _} -> @@ -88,6 +91,8 @@ dispatch([Data|Tail], CallbackMod, NextMod, State) -> dispatch(Tail, CallbackMod, NewMod, NewState); {ok, NewState} -> dispatch(Tail, CallbackMod, NextMod, NewState); + closed -> + closed; _Any -> dispatch(Tail, CallbackMod, NextMod, State) end. diff --git a/src/egs_sup.erl b/src/egs_sup.erl index 2d3b644..fa789b9 100644 --- a/src/egs_sup.erl +++ b/src/egs_sup.erl @@ -51,9 +51,9 @@ init([]) -> %% Start egs_cron, egs_game, egs_login, egs_patch. To be replaced by configurable modules. Processes = [{egs_user_model, {egs_user_model, start_link, []}, permanent, 5000, worker, dynamic}, {egs_game_server, {egs_game_server, start_link, [?GAME_PORT]}, permanent, 5000, worker, dynamic}, - {psu_login_jp1, {psu_login, start_link, [?LOGIN_PORT_JP_ONE, 10000001]}, permanent, 5000, worker, dynamic}, - {psu_login_jp2, {psu_login, start_link, [?LOGIN_PORT_JP_TWO, 20000001]}, permanent, 5000, worker, dynamic}, - {psu_login_us, {psu_login, start_link, [?LOGIN_PORT_US, 30000001]}, permanent, 5000, worker, dynamic}, + {egs_login_server_jp1, {egs_login_server, start_link, [?LOGIN_PORT_JP_ONE]}, permanent, 5000, worker, dynamic}, + {egs_login_server_jp2, {egs_login_server, start_link, [?LOGIN_PORT_JP_TWO]}, permanent, 5000, worker, dynamic}, + {egs_login_server_us, {egs_login_server, start_link, [?LOGIN_PORT_US]}, permanent, 5000, worker, dynamic}, {psu_patch_jp, {psu_patch, start_link, [?PATCH_PORT_JP]}, permanent, 5000, worker, dynamic}, {psu_patch_us, {psu_patch, start_link, [?PATCH_PORT_US]}, permanent, 5000, worker, dynamic}], {ok, {{one_for_one, 10, 10}, Processes}}. diff --git a/src/psu/psu_login.erl b/src/psu/psu_login.erl deleted file mode 100644 index 548ea9c..0000000 --- a/src/psu/psu_login.erl +++ /dev/null @@ -1,165 +0,0 @@ -%% @author Loïc Hoguin -%% @copyright 2010 Loïc Hoguin. -%% @doc Process login requests. -%% -%% This file is part of EGS. -%% -%% EGS is free software: you can redistribute it and/or modify -%% it under the terms of the GNU Affero General Public License as -%% published by the Free Software Foundation, either version 3 of the -%% License, or (at your option) any later version. -%% -%% EGS is distributed in the hope that it will be useful, -%% but WITHOUT ANY WARRANTY; without even the implied warranty of -%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -%% GNU Affero General Public License for more details. -%% -%% You should have received a copy of the GNU Affero General Public License -%% along with EGS. If not, see . - --module(psu_login). --export([start_link/2]). %% External. --export([listen/2, accept/2, process/2, loop/2]). %% Internal. - --include("include/records.hrl"). --include("include/network.hrl"). - --define(OPTIONS, [binary, {active, false}, {reuseaddr, true}, {certfile, "priv/ssl/servercert.pem"}, {keyfile, "priv/ssl/serverkey.pem"}, {password, "alpha"}]). - -%% @spec start_link(Port) -> {ok,Pid::pid()} -%% @doc Start the PSU login server for inclusion in a supervisor tree. -start_link(Port, SessionID) -> - Pid = spawn(?MODULE, listen, [Port, SessionID]), - {ok, Pid}. - -%% @spec listen(Port, SessionID) -> ok -%% @doc Listen for connections. -listen(Port, SessionID) -> - error_logger:info_report(io_lib:format("psu_login listening on port ~b, with a sessionid of ~b", [Port, SessionID])), - {ok, LSocket} = ssl:listen(Port, ?OPTIONS), - ?MODULE:accept(LSocket, SessionID). - -%% @spec accept(LSocket, SessionID) -> ok -%% @doc Accept connections. -accept(LSocket, SessionID) -> - case ssl:transport_accept(LSocket, 5000) of - {ok, CSocket} -> - %% in the future, modulo to avoid conflicts future with real GIDs - %% NextID = (SessionID + 1) rem 1000000, - NextID = SessionID + 1, - ssl:ssl_accept(CSocket), - spawn(?MODULE, process, [CSocket, SessionID]); - {error, _Reason} -> - NextID = SessionID, - reload - end, - ?MODULE:accept(LSocket, NextID). - -%% @spec process(CSocket, SessionID) -> ok -%% @doc Process the new connections. Send an hello packet and start the loop. -process(CSocket, SessionID) -> - log(SessionID, "hello"), - psu_proto:send_0202(CSocket, SessionID), - ?MODULE:loop(CSocket, SessionID). - -%% @spec loop(CSocket, SessionID) -> ok -%% @doc Main loop for the login server. -loop(CSocket, SessionID) -> - case psu_proto:packet_recv(CSocket, 5000) of - {ok, Orig} -> - << _:32, Command:16/unsigned-integer, _/bits >> = Orig, - case handle(Command, CSocket, SessionID, Orig) of - closed -> ok; - _ -> ?MODULE:loop(CSocket, SessionID) - end; - {error, timeout} -> - reload, - ?MODULE:loop(CSocket, SessionID); - {error, closed} -> - log(SessionID, "quit"), - egs_user_model:delete(SessionID) - end. - -%% @spec handle(Command, CSocket, SessionID, Orig) -> ok | closed -%% @doc Login server client commands handler. - -%% Game server info request handler. -%% @todo Remove the dependency on network.hrl -handle(16#0217, CSocket, SessionID, _) -> - log(SessionID, "forward to game server"), - IP = ?GAME_IP, - Port = ?GAME_PORT, - Packet = << 16#02160300:32, 0:192, SessionID:32/little-unsigned-integer, 0:64, IP/binary, Port:32/little-unsigned-integer >>, - psu_proto:packet_send(CSocket, Packet), - ssl:close(CSocket), - closed; - -%% Authentication request handler. Currently always succeed. -%% Use the temporary session ID as the GID for now. -%% Use username and password as a folder name for saving character data. -%% @todo Handle real GIDs whenever there's real authentication. GID is the second SessionID in the reply. -%% @todo Apparently it's possible to ask a question in the reply here. Used for free course on JP. -handle(16#0219, CSocket, SessionID, Orig) -> - << _:352, UsernameBlob:192/bits, PasswordBlob:192/bits, _/bits >> = Orig, - Username = re:replace(UsernameBlob, "\\0", "", [global, {return, binary}]), - Password = re:replace(PasswordBlob, "\\0", "", [global, {return, binary}]), - log(SessionID, "auth success for ~s ~s", [Username, Password]), - Auth = crypto:rand_bytes(4), - Folder = << Username/binary, "-", Password/binary >>, - Time = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), - egs_user_model:write(#egs_user_model{id=SessionID, pid=self(), socket=CSocket, state={wait_for_authentication, Auth}, time=Time, folder=Folder}), - Packet = << 16#02230300:32, 0:192, SessionID:32/little-unsigned-integer, 0:64, SessionID:32/little-unsigned-integer, Auth:32/bits >>, - psu_proto:packet_send(CSocket, Packet); - -%% MOTD request handler. Handles both forms of MOTD requests, US and JP. Page number starts at 0. -%% @todo Currently ignore the language and send the same MOTD file to everyone. Language is 8 bits next to Page. -%% @todo Use a normal ASCII file rather than an UCS2 one? -handle(Command, CSocket, SessionID, Orig) when Command =:= 16#0226; Command =:= 16#023f -> - << _:352, Page:8/little-unsigned-integer, _/bits >> = Orig, - log(SessionID, "send MOTD page ~.10b", [Page + 1]), - {ok, File} = file:read_file("priv/psu_login/motd.txt"), - Tokens = re:split(File, "\n."), - MOTD = << << Line/binary, "\n", 0 >> || Line <- lists:sublist(Tokens, 1 + Page * 15, 15) >>, - NbPages = 1 + length(Tokens) div 15, - Packet = << 16#0225:16, 0:304, NbPages:8, Page:8, 16#8200:16/unsigned-integer, MOTD/binary, 0:16 >>, - psu_proto:packet_send(CSocket, Packet); - -%% @doc Platform information handler. Reject versions < 2.0009.2. -handle(16#080e, CSocket, SessionID, << _:352, _:64, Revision:8, Minor:4, _:12, Major:4, _:4, _/bits >>) -> - Version = Major * 1000000 + Minor * 1000 + Revision, - if Version < 2009002 -> - log(SessionID, "reject outdated version ~b.~b.~b", [Major, Minor, Revision]), - Website = << "http://psumods.co.uk/forums/comments.php?DiscussionID=40#Item_1" >>, - Size = byte_size(Website), - WebsiteLen = Size + 1, - PaddingSize = 8 * (512 - Size), - WebsitePacket = << 16#02310300:32, 16#ffff0000:32, 16#00000f00:32, SessionID:32/little, 0:64, 16#00000f00:32, SessionID:32/little, 0:64, - WebsiteLen:32/little, Website/binary, 0:PaddingSize >>, - psu_proto:packet_send(CSocket, WebsitePacket), - {ok, File} = file:read_file("priv/psu_login/error_version.txt"), - ErrorLen = byte_size(File) div 2 + 2, - ErrorPacket = << 16#02230300:32, 0:160, 16#00000f00:32, SessionID:32/little, 0:96, 16#f9dbce73:32, 3:32/little, 0:48, ErrorLen:16/little, File/binary, 0:16 >>, - psu_proto:packet_send(CSocket, ErrorPacket), - closed; - true -> - ignore - end; - -%% Silently ignore command 0227. -handle(16#0227, _, _, _) -> - ignore; - -%% Unknown command handler. Print a log message about it. -handle(Command, _, SessionID, _) -> - log(SessionID, "dismissed packet ~4.16.0b", [Command]). - -%% @spec log(SessionID, Message) -> ok -%% @doc Log message to the console. -log(SessionID, Message) -> - io:format("login (~.10b): ~s~n", [SessionID, Message]). - -%% @spec log(SessionID, Message, Format) -> ok -%% @doc Format a message and log it to the console. -log(SessionID, Message, Format) -> - RealMessage = io_lib:format(Message, Format), - log(SessionID, RealMessage). diff --git a/src/psu/psu_npc.erl b/src/psu/psu_npc.erl index 37aa3c0..6621a7b 100644 --- a/src/psu/psu_npc.erl +++ b/src/psu/psu_npc.erl @@ -27,7 +27,7 @@ %% @todo Handle stats, experience, based on level. %% @todo Level shouldn't go below 1 or above 200. user_init(NPCid, BaseLevel) -> - NPCGID = 16#ff000000 + mnesia:dirty_update_counter(counters, npcgid, 1), + NPCGID = 16#ff000000 + mnesia:dirty_update_counter(counters, tmpgid, 1), Settings = proplists:get_value(NPCid, ?NPC), TmpUCS2Name = << << X:8, 0:8 >> || X <- Settings#psu_npc.name >>, PaddingSize = 8 * (64 - byte_size(TmpUCS2Name)),