RuneHive-Tarnish
Neural OSRS Enhancement Framework
Loading...
Searching...
No Matches
LoginSession.java
1package com.osroyale.net.session;
2
3import com.jcabi.jdbc.JdbcSession;
4import com.osroyale.Config;
5import com.osroyale.OSRoyale;
6import com.osroyale.content.bot.BotUtility;
7import com.osroyale.game.service.ForumService;
8import com.osroyale.game.world.World;
9import com.osroyale.game.world.entity.mob.player.BannedPlayers;
10import com.osroyale.game.world.entity.mob.player.Player;
11import com.osroyale.game.world.entity.mob.player.persist.PlayerSerializer;
12import com.osroyale.game.world.entity.mob.player.profile.Profile;
13import com.osroyale.game.world.entity.mob.player.profile.ProfileRepository;
14import com.osroyale.net.codec.game.GamePacketDecoder;
15import com.osroyale.net.codec.game.GamePacketEncoder;
16import com.osroyale.net.codec.login.LoginDetailsPacket;
17import com.osroyale.net.codec.login.LoginResponse;
18import com.osroyale.net.codec.login.LoginResponsePacket;
19import com.osroyale.util.Stopwatch;
20import com.osroyale.util.Utility;
21import de.mkammerer.argon2.Argon2Factory.Argon2Types;
22import io.netty.channel.Channel;
23import io.netty.channel.ChannelFutureListener;
24import io.netty.channel.ChannelPipeline;
25import org.jire.tarnishps.Argon2;
26import org.mindrot.jbcrypt.BCrypt;
27import org.slf4j.Logger;
28import org.slf4j.LoggerFactory;
29
30import java.time.Instant;
31import java.util.Date;
32import java.util.concurrent.ConcurrentHashMap;
33import java.util.concurrent.ConcurrentMap;
34import java.util.concurrent.TimeUnit;
35import java.util.concurrent.atomic.AtomicInteger;
36
72
73public final class LoginSession extends Session {
74
75 private static final Logger logger = LoggerFactory.getLogger(LoginSession.class);
76
77 private static final ConcurrentMap<String, FailedLoginAttempt> failedLogins = new ConcurrentHashMap<>();
78
79 public LoginSession(Channel channel) {
80 super(channel);
81 }
82
83 @Override
84 public void handleClientPacket(Object o) {
85 if (o instanceof LoginDetailsPacket) {
87 OSRoyale.getInstance().getLoginExecutorService().execute(this, packet);
88 //handleUserLoginDetails(packet);
89 }
90 }
91
92 public void handleUserLoginDetails(final LoginDetailsPacket packet) {
93 final ConcurrentMap<String, FailedLoginAttempt> failedLogins = LoginSession.failedLogins;
94
95 final String username = packet.getUsername();
96
97 final FailedLoginAttempt attempt = failedLogins.get(username);
98 if (attempt != null) {
99 final Stopwatch stopwatch = attempt.getStopwatch();
100 final AtomicInteger atomicTime = attempt.getAttempt();
101 final int time = atomicTime.get();
102 if (time >= Config.FAILED_LOGIN_ATTEMPTS
103 && !stopwatch.elapsed(Config.FAILED_LOGIN_TIMEOUT, TimeUnit.MINUTES)) {
104 sendFailedResponse(channel, LoginResponse.LOGIN_ATTEMPTS_EXCEEDED);
105 return;
106 } else if (time >= Config.FAILED_LOGIN_ATTEMPTS
107 && stopwatch.elapsed(Config.FAILED_LOGIN_TIMEOUT, TimeUnit.MINUTES)) {
108 failedLogins.remove(username);
109 } else {
110 atomicTime.incrementAndGet();
111 }
112 }
113
114 final Player player = new Player(username);
115 final String password = packet.getPassword();
116 player.setPassword(password);
117
118 final LoginResponse response = evaluate(player);
119 if (response == LoginResponse.INVALID_CREDENTIALS) {
120 if (!failedLogins.containsKey(username)) {
121 failedLogins.put(username, new FailedLoginAttempt());
122 }
123 } else if (response == LoginResponse.NORMAL) {
124 failedLogins.remove(username);
125 }
126
127 final Channel channel = this.channel;
128
129 if (response != LoginResponse.NORMAL) {
130 sendFailedResponse(channel, response);
131 return;
132 }
133
134 final Argon2Types argon2Type = Argon2.argon2Type(player.getPassword());
135 if (argon2Type != Argon2.DEFAULT_TYPE) {
136 // needs rehashing (this should be moved onto another thread, as hashing is slow)
137 final String passwordHash = Argon2.getDefault().hash(
138 Argon2.DEFAULT_ITERATIONS,
139 Argon2.DEFAULT_MEMORY,
140 Argon2.DEFAULT_PARALLELISM,
141 password);
142 player.setPassword(passwordHash); // update password to hashed version
143 }
144
145 ProfileRepository.put(new Profile(username, player.lastHost, player.hostList, player.right));
146
147 channel.writeAndFlush(new LoginResponsePacket(response, player.right, false))
148 .addListener((ChannelFutureListener) sourceFuture -> {
149 try {
150 final ChannelPipeline pipeline = channel.pipeline();
151 pipeline.replace("login-decoder",
152 "game-decoder", new GamePacketDecoder(packet.getDecryptor()));
153 pipeline.replace("login-encoder",
154 "game-encoder", new GamePacketEncoder(packet.getEncryptor()));
155
156 final GameSession session = new GameSession(channel, player);
157 channel.attr(Config.SESSION_KEY).set(session);
158 player.setSession(session);
159
160 World.queueLogin(player);
161 } catch (final Exception e) {
162 logger.error("Failed to queue login for \"" + username + "\"", e);
163 }
164 });
165 }
166
167 private static void sendFailedResponse(final Channel channel, final LoginResponse response) {
168 channel.writeAndFlush(new LoginResponsePacket(response))
169 .addListener(ChannelFutureListener.CLOSE);
170 }
171
172 private LoginResponse evaluate(Player player) {
173 final String username = player.getUsername();
174 final String password = player.getPassword();
175 final boolean isEmail = username.indexOf('@') != -1;
176
177 // prevents users from logging in before the server is ready to accept connections
178 if (!OSRoyale.serverStarted.get()) {
179 return LoginResponse.SERVER_BEING_UPDATED;
180 }
181
182 // prevents users from using accounts with bot names
183 for (String botName : BotUtility.BOT_NAMES) {
184 if (username.equalsIgnoreCase(botName)) {
185 return LoginResponse.INSUFFICIENT_PERMSSION;
186 }
187 }
188
189 // the world is currently full
190 if (World.getPlayerCount() == Config.MAX_PLAYERS) {
191 return LoginResponse.WORLD_FULL;
192 }
193
194 // prevents users from logging in if the world is being updated
195 if (World.update.get()) {
196 return LoginResponse.SERVER_BEING_UPDATED;
197 }
198
199 if (BannedPlayers.bans.contains(username.toLowerCase())) {
200 return LoginResponse.ACCOUNT_DISABLED;
201 }
202
203 if (isEmail) {
204 if (!Config.FORUM_INTEGRATION) {
205 return LoginResponse.BAD_USERNAME;
206 }
207
208 if (username.length() > Config.EMAIL_MAX_CHARACTERS || username.length() < Config.EMAIL_MIN_CHARACTERS) {
209 return LoginResponse.INVALID_EMAIL;
210 }
211
212 // does email have illegal characters
213 if (!(username.matches("^[a-zA-Z0-9.@]{1," + Config.EMAIL_MAX_CHARACTERS + "}$"))) {
214 return LoginResponse.INVALID_CREDENTIALS;
215 }
216 } else if (username.length() < Config.USERNAME_MIN_CHARACTERS) {
217 return LoginResponse.SHORT_USERNAME;
218 } else if (username.length() > Config.USERNAME_MAX_CHARACTERS) {
219 return LoginResponse.BAD_USERNAME;
220 } else if (World.getPlayerByHash(Utility.nameToLong(username)).isPresent()) { // this user is already online
221 return LoginResponse.ACCOUNT_ONLINE;
222 } else if (!(username.matches("^[a-zA-Z0-9 ]{1," + Config.USERNAME_MAX_CHARACTERS + "}$"))) { // does username have illegal characters
223 return LoginResponse.INVALID_CREDENTIALS;
224 } else if (password.isEmpty()/* || password.length() > Config.PASSWORD_MAX_CHARACTERS*/) {
225 return LoginResponse.INVALID_CREDENTIALS;
226 }
227
228 if (World.search(username).isPresent()) {
229 return LoginResponse.ACCOUNT_ONLINE;
230 }
231
232 if (Config.FORUM_INTEGRATION) {
233 // check username and password from client with username and password from forum
234 final LoginResponse response = authenticatedForumUser(player, isEmail);
235 if (response != LoginResponse.NORMAL) {
236 return response;
237 }
238 }
239
240 LoginResponse response = PlayerSerializer.load(player, password);
241
242 if (World.searchAll(username).isPresent()) {
243 return LoginResponse.ACCOUNT_ONLINE;
244 }
245
246 return response;
247 }
248
249 private LoginResponse authenticatedForumUser(Player player, boolean isEmail) {
250 final String username = player.getUsername();
251 try {
252 final LoginResponse response = new JdbcSession(ForumService.getConnectionPool())
253 .sql(isEmail ? "SELECT member_id, members_pass_hash, name, temp_ban FROM core_members WHERE UPPER(email) = ?" : "SELECT member_id, members_pass_hash, temp_ban FROM core_members WHERE UPPER(name) = ?")
254 .set(username.toUpperCase())
255 .select((rset, stmt) -> {
256 if (rset.next()) {
257 final int memberId = rset.getInt(1);
258 final String passwordHash = rset.getString(2);
259 final String forumUsername = isEmail ? rset.getString(3) : username;
260 final long unixTime = rset.getLong(isEmail ? 4 : 3);
261
262 if (isBanned(unixTime)) {
263 return LoginResponse.ACCOUNT_DISABLED;
264 }
265
266 if (passwordHash.isEmpty()) {
267 return LoginResponse.INVALID_CREDENTIALS;
268 } else if (BCrypt.checkpw(player.getPassword(), passwordHash)) {
269 player.setMemberId(memberId);
270 player.setUsername(forumUsername);
271 player.setPassword(passwordHash);
272 return LoginResponse.NORMAL;
273 } else {
274 return LoginResponse.INVALID_CREDENTIALS;
275 }
276 }
277 return LoginResponse.FORUM_REGISTRATION;
278 });
279 return response;
280 } catch (Exception ex) {
281 ex.printStackTrace();
282 }
283 return LoginResponse.LOGIN_SERVER_OFFLINE;
284 }
285
286 private boolean isBanned(long unixTime) {
287 // not banned
288 if (unixTime == 0) {
289 return false;
290 } else if (unixTime == -1) { // perm ban
291 return true;
292 }
293
294 final Date date = Date.from(Instant.ofEpochSecond(unixTime));
295
296 final Date currentDate = Date.from(Instant.now());
297
298 return date.after(currentDate);
299 }
300
306 private static final class FailedLoginAttempt {
307
308 private final AtomicInteger attempt = new AtomicInteger(0);
309 private final Stopwatch stopwatch = Stopwatch.start();
310
311 public AtomicInteger getAttempt() {
312 return attempt;
313 }
314
315 public Stopwatch getStopwatch() {
316 return stopwatch;
317 }
318
319 }
320
321}