Add ID codes for positive user identification, and minor fixups.
Users can specify an identification code when they connect (8-100 characters), only if they are using HTTPS. This code is combined with their nickname and a server-side secret, hashed with SHA-256, and condensed down to 64 bits by XORing every 8th byte with each other, and finally converted to base64 (with the trailing = removed). This code is displayed in a tooltip when hovering over the user's chat (TODO: mobile way to view it). Sigils have been added to be displayed before the user's name in the chat. Admins get @, users with an ID code get +, and normal users get nothing. The IS_ADMIN field is now deprecated, as this can be determined from the user's sigil. It will be removed eventually, but is still being included in events even though the official client should not be using it anymore. Kicks and bans are now always displayed to all users, even if the server isn't transmitting quit events normally.
This commit is contained in:
parent
dc31b1f2ce
commit
7a24c652ac
|
@ -107,9 +107,14 @@ HttpSession hSession = request.getSession(true);
|
|||
The PAX panel sets have also been removed.</li>
|
||||
</ul>
|
||||
<div id="nickbox">
|
||||
Nickname:
|
||||
<label for="nickname">Nickname:</label>
|
||||
<input type="text" id="nickname" value="" maxlength="30" role="textbox"
|
||||
aria-label="Enter your nickname." data-lpignore="true" />
|
||||
<label for="idcode">
|
||||
<dfn title="Only available via HTTPS. Provide a secret identification code to positively identify yourself in the chat.">
|
||||
Optional identification code:</dfn></label>
|
||||
<input type="password" id="idcode" value="" maxlength="100" disabled="disabled"
|
||||
aria-label="Optionally enter and identification code." />
|
||||
<input type="button" id="nicknameconfirm" value="Set" />
|
||||
<span id="nickbox_error" class="error"></span>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -199,6 +199,17 @@ cah.ajax.Builder.prototype.withCardcastId = function(id) {
|
|||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string}
|
||||
* id The user's identification code.
|
||||
* @returns {cah.ajax.Builder} This object.
|
||||
*/
|
||||
cah.ajax.Builder.prototype.withIdCode = function(idCode) {
|
||||
this.assertNotExecuted_();
|
||||
this.data[cah.$.AjaxRequest.ID_CODE] = idCode;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert that the request from this builder has not already run. Throws an exception if it has.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -29,11 +29,13 @@
|
|||
|
||||
cah.ajax.SuccessHandlers[cah.$.AjaxOperation.REGISTER] = function(data) {
|
||||
cah.nickname = data[cah.$.AjaxResponse.NICKNAME];
|
||||
cah.idcode = data[cah.$.AjaxResponse.ID_CODE];
|
||||
cah.sigil = data[cah.$.AjaxResponse.SIGIL];
|
||||
if (!cah.noPersistentId) {
|
||||
cah.persistentId = data[cah.$.AjaxResponse.PERSISTENT_ID];
|
||||
cah.setCookie("persistent_id", cah.persistentId);
|
||||
}
|
||||
cah.log.status("You are connected as " + cah.nickname);
|
||||
cah.log.status("You are connected as " + cah.sigil + cah.nickname);
|
||||
$("#welcome").hide();
|
||||
$("#canvass").show();
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -40,8 +40,13 @@ $(document).ready(function() {
|
|||
$("#nickname").val($.cookie("nickname"));
|
||||
}
|
||||
$("#nicknameconfirm").click(nicknameconfirm_click);
|
||||
$("#nickname").keyup(nickbox_keyup);
|
||||
$("#nickname").keyup(nickname_keyup);
|
||||
$("#nickname").focus();
|
||||
if (document.location.protocol == "https:" || cah.INSECURE_ID_ALLOWED) {
|
||||
$("#idcode").prop("disabled", false);
|
||||
// re-use existing handler
|
||||
$("#idcode").keyup(nickname_keyup);
|
||||
}
|
||||
|
||||
$(".chat", $("#tab-global")).keyup(chat_keyup($(".chat_submit", $("#tab-global"))));
|
||||
$(".chat_submit", $("#tab-global")).click(chatsubmit_click(null, $("#tab-global")));
|
||||
|
@ -82,7 +87,7 @@ $(window).blur(function() {
|
|||
* @param {jQuery.Event}
|
||||
* e
|
||||
*/
|
||||
function nickbox_keyup(e) {
|
||||
function nickname_keyup(e) {
|
||||
if (e.which == 13) {
|
||||
$("#nicknameconfirm").click();
|
||||
e.preventDefault();
|
||||
|
@ -96,6 +101,10 @@ function nicknameconfirm_click() {
|
|||
var nickname = $.trim($("#nickname").val());
|
||||
cah.setCookie("nickname", nickname);
|
||||
var builder = cah.Ajax.build(cah.$.AjaxOperation.REGISTER).withNickname(nickname);
|
||||
var idCode = $.trim($("#idcode").val());
|
||||
if (idCode) {
|
||||
builder.withIdCode(idCode);
|
||||
}
|
||||
if (!cah.noPersistentId && cah.persistentId) {
|
||||
builder.withPersistentId(cah.persistentId);
|
||||
}
|
||||
|
@ -153,7 +162,12 @@ function chatsubmit_click(game_id, parent_element) {
|
|||
ajax = cah.Ajax.build(cah.$.AjaxOperation.CHAT);
|
||||
}
|
||||
ajax = ajax.withEmote(false).withMessage(text);
|
||||
cah.log.status_with_game(game_id, "<" + cah.nickname + "> " + text);
|
||||
var clazz = '';
|
||||
if (cah.sigil == cah.$.Sigil.ADMIN) {
|
||||
clazz = 'admin';
|
||||
}
|
||||
cah.log.status_with_game(game_id, "<" + cah.sigil + cah.nickname + "> " + text, clazz,
|
||||
false, cah.log.getTitleForIdCode(cah.idcode));
|
||||
break;
|
||||
case 'me':
|
||||
if (game_id !== null) {
|
||||
|
@ -162,7 +176,12 @@ function chatsubmit_click(game_id, parent_element) {
|
|||
ajax = cah.Ajax.build(cah.$.AjaxOperation.CHAT);
|
||||
}
|
||||
ajax = ajax.withEmote(true).withMessage(text);
|
||||
cah.log.status_with_game(game_id, "* " + cah.nickname + " " + text);
|
||||
var clazz = '';
|
||||
if (cah.sigil == cah.$.Sigil.ADMIN) {
|
||||
clazz = 'admin';
|
||||
}
|
||||
cah.log.status_with_game(game_id, "* " + cah.sigil + cah.nickname + " " + text, clazz,
|
||||
false, cah.log.getTitleForIdCode(cah.idcode));
|
||||
break;
|
||||
case 'wall':
|
||||
ajax = cah.Ajax.build(cah.$.AjaxOperation.CHAT).withWall(true).withMessage(text);
|
||||
|
|
|
@ -109,33 +109,43 @@ cah.$.WhiteCardData.TEXT = "T";
|
|||
cah.$.WhiteCardData.ID = "cid";
|
||||
cah.$.WhiteCardData.WATERMARK = "W";
|
||||
|
||||
cah.$.Sigil = function() {
|
||||
// Dummy constructor to make Eclipse auto-complete.
|
||||
};
|
||||
cah.$.Sigil.prototype.dummyForAutocomplete = undefined;
|
||||
cah.$.Sigil.NORMAL_USER = "";
|
||||
cah.$.Sigil.ADMIN = "@";
|
||||
cah.$.Sigil.ID_CODE = "+";
|
||||
|
||||
cah.$.LongPollResponse = function() {
|
||||
// Dummy constructor to make Eclipse auto-complete.
|
||||
};
|
||||
cah.$.LongPollResponse.prototype.dummyForAutocomplete = undefined;
|
||||
cah.$.LongPollResponse.WALL = "wall";
|
||||
cah.$.LongPollResponse.PLAY_TIMER = "Pt";
|
||||
cah.$.LongPollResponse.ROUND_WINNER = "rw";
|
||||
cah.$.LongPollResponse.EMOTE = "me";
|
||||
cah.$.LongPollResponse.CARDCAST_DECK_INFO = "cdi";
|
||||
cah.$.LongPollResponse.PLAYER_INFO = "pi";
|
||||
cah.$.LongPollResponse.FROM = "f";
|
||||
cah.$.LongPollResponse.GAME_ID = "gid";
|
||||
cah.$.LongPollResponse.WHITE_CARDS = "wc";
|
||||
cah.$.LongPollResponse.EVENT = "E";
|
||||
cah.$.LongPollResponse.HAND = "h";
|
||||
cah.$.LongPollResponse.ERROR_CODE = "ec";
|
||||
cah.$.LongPollResponse.MESSAGE = "m";
|
||||
cah.$.LongPollResponse.WINNING_CARD = "WC";
|
||||
cah.$.LongPollResponse.NICKNAME = "n";
|
||||
cah.$.LongPollResponse.BLACK_CARD = "bc";
|
||||
cah.$.LongPollResponse.FROM_ADMIN = "fa";
|
||||
cah.$.LongPollResponse.TIMESTAMP = "ts";
|
||||
cah.$.LongPollResponse.GAME_STATE = "gs";
|
||||
cah.$.LongPollResponse.GAME_INFO = "gi";
|
||||
cah.$.LongPollResponse.ERROR = "e";
|
||||
cah.$.LongPollResponse.INTERMISSION = "i";
|
||||
cah.$.LongPollResponse.ID_CODE = "idc";
|
||||
cah.$.LongPollResponse.REASON = "qr";
|
||||
cah.$.LongPollResponse.WALL = "wall";
|
||||
cah.$.LongPollResponse.ROUND_WINNER = "rw";
|
||||
cah.$.LongPollResponse.SIGIL = "?";
|
||||
cah.$.LongPollResponse.EMOTE = "me";
|
||||
cah.$.LongPollResponse.CARDCAST_DECK_INFO = "cdi";
|
||||
cah.$.LongPollResponse.GAME_ID = "gid";
|
||||
cah.$.LongPollResponse.NICKNAME = "n";
|
||||
cah.$.LongPollResponse.BLACK_CARD = "bc";
|
||||
cah.$.LongPollResponse.GAME_STATE = "gs";
|
||||
cah.$.LongPollResponse.INTERMISSION = "i";
|
||||
|
||||
cah.$.LongPollEvent = function() {
|
||||
// Dummy constructor to make Eclipse auto-complete.
|
||||
|
@ -181,6 +191,7 @@ cah.$.ErrorCode.BANNED = "B&";
|
|||
cah.$.ErrorCode.WRONG_PASSWORD = "wp";
|
||||
cah.$.ErrorCode.RESERVED_NICK = "rn";
|
||||
cah.$.ErrorCode.TOO_MANY_GAMES = "tmg";
|
||||
cah.$.ErrorCode.INVALID_ID_CODE = "iid";
|
||||
cah.$.ErrorCode.CANNOT_JOIN_ANOTHER_GAME = "cjag";
|
||||
cah.$.ErrorCode.NO_MSG_SPECIFIED = "nms";
|
||||
cah.$.ErrorCode.ALREADY_STARTED = "as";
|
||||
|
@ -213,6 +224,7 @@ cah.$.ErrorCode.INVALID_CARD = "ic";
|
|||
cah.$.ErrorCode_msg = {};
|
||||
cah.$.ErrorCode_msg['cii'] = "Invalid Cardcast ID. Must be exactly 5 characters.";
|
||||
cah.$.ErrorCode_msg['nr'] = "Not registered. Refresh the page.";
|
||||
cah.$.ErrorCode_msg['iid'] = "Identification code, if provided, must be between 8 and 100 characters, inclusive.";
|
||||
cah.$.ErrorCode_msg['ns'] = "Session not detected. Make sure you have cookies enabled.";
|
||||
cah.$.ErrorCode_msg['ccf'] = "Cannot find Cardcast deck with given ID. If you just added this deck to Cardcast, wait a few minutes and try again.";
|
||||
cah.$.ErrorCode_msg['nyt'] = "It is not your turn to play a card.";
|
||||
|
@ -267,6 +279,7 @@ cah.$.AjaxResponse.SERIAL = "s";
|
|||
cah.$.AjaxResponse.NAMES = "nl";
|
||||
cah.$.AjaxResponse.PERSISTENT_ID = "pid";
|
||||
cah.$.AjaxResponse.GAMES = "gl";
|
||||
cah.$.AjaxResponse.SIGIL = "?";
|
||||
cah.$.AjaxResponse.PLAYER_INFO = "pi";
|
||||
cah.$.AjaxResponse.GAME_ID = "gid";
|
||||
cah.$.AjaxResponse.WHITE_CARDS = "wc";
|
||||
|
@ -281,6 +294,7 @@ cah.$.AjaxResponse.NEXT = "next";
|
|||
cah.$.AjaxResponse.GAME_INFO = "gi";
|
||||
cah.$.AjaxResponse.CARD_ID = "cid";
|
||||
cah.$.AjaxResponse.ERROR = "e";
|
||||
cah.$.AjaxResponse.ID_CODE = "idc";
|
||||
cah.$.AjaxResponse.CARD_SETS = "css";
|
||||
|
||||
cah.$.AjaxRequest = function() {
|
||||
|
@ -288,17 +302,18 @@ cah.$.AjaxRequest = function() {
|
|||
};
|
||||
cah.$.AjaxRequest.prototype.dummyForAutocomplete = undefined;
|
||||
cah.$.AjaxRequest.SERIAL = "s";
|
||||
cah.$.AjaxRequest.GAME_OPTIONS = "go";
|
||||
cah.$.AjaxRequest.MESSAGE = "m";
|
||||
cah.$.AjaxRequest.OP = "o";
|
||||
cah.$.AjaxRequest.NICKNAME = "n";
|
||||
cah.$.AjaxRequest.WALL = "wall";
|
||||
cah.$.AjaxRequest.PASSWORD = "pw";
|
||||
cah.$.AjaxRequest.PERSISTENT_ID = "pid";
|
||||
cah.$.AjaxRequest.EMOTE = "me";
|
||||
cah.$.AjaxRequest.CARD_ID = "cid";
|
||||
cah.$.AjaxRequest.CARDCAST_ID = "cci";
|
||||
cah.$.AjaxRequest.GAME_ID = "gid";
|
||||
cah.$.AjaxRequest.GAME_OPTIONS = "go";
|
||||
cah.$.AjaxRequest.MESSAGE = "m";
|
||||
cah.$.AjaxRequest.NICKNAME = "n";
|
||||
cah.$.AjaxRequest.PASSWORD = "pw";
|
||||
cah.$.AjaxRequest.CARD_ID = "cid";
|
||||
cah.$.AjaxRequest.ID_CODE = "idc";
|
||||
|
||||
cah.$.AjaxOperation = function() {
|
||||
// Dummy constructor to make Eclipse auto-complete.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -70,8 +70,10 @@ cah.log.status = function(text, opt_class) {
|
|||
* opt_class Optional CSS class to use for this message.
|
||||
* @param {boolean}
|
||||
* opt_allow_html Allow HTML to be used.
|
||||
* @param {string}
|
||||
* opt_title Optional title text for span.
|
||||
*/
|
||||
cah.log.status_with_game = function(game_or_id, text, opt_class, opt_allow_html) {
|
||||
cah.log.status_with_game = function(game_or_id, text, opt_class, opt_allow_html, opt_title) {
|
||||
var logElement;
|
||||
if (game_or_id === null) {
|
||||
logElement = cah.log.log;
|
||||
|
@ -89,7 +91,12 @@ cah.log.status_with_game = function(game_or_id, text, opt_class, opt_allow_html)
|
|||
var scroll = (logElement.prop("scrollHeight") - logElement.height() - logElement
|
||||
.prop("scrollTop")) <= 5;
|
||||
|
||||
var node = $("<span></span><br/>");
|
||||
var node;
|
||||
if (opt_title) {
|
||||
node = $("<span title ='" + opt_title + "'></span><br/>");
|
||||
} else {
|
||||
node = $("<span></span><br/>");
|
||||
}
|
||||
var full_msg = "[" + new Date().toLocaleTimeString() + "] " + text + "\n";
|
||||
if (opt_allow_html) {
|
||||
$(node[0]).html(full_msg);
|
||||
|
@ -200,3 +207,17 @@ cah.log.debug = function(text, opt_obj) {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the title text to use for the given idcode, or a null if there is no idcode.
|
||||
*
|
||||
* @param {string}
|
||||
* idcode ID code, or logical false to not have a title.
|
||||
*/
|
||||
cah.log.getTitleForIdCode = function(idcode) {
|
||||
if (idcode) {
|
||||
return "Identification code: " + idcode;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2012, Andy Janata All rights reserved.
|
||||
* Copyright (c) 2012-2018, Andy Janata All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
* provided that the following conditions are met:
|
||||
|
@ -93,17 +93,20 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.BANNED] = function() {
|
|||
|
||||
cah.longpoll.EventHandlers[cah.$.LongPollEvent.CHAT] = function(data) {
|
||||
var clazz = undefined;
|
||||
var idcode = data[cah.$.LongPollResponse.ID_CODE];
|
||||
var title = cah.log.getTitleForIdCode(idcode);
|
||||
var sigil = data[cah.$.LongPollResponse.SIGIL];
|
||||
var from = data[cah.$.LongPollResponse.FROM];
|
||||
var show = !cah.ignoreList[from];
|
||||
var game = null;
|
||||
if (data[cah.$.LongPollResponse.FROM_ADMIN]) {
|
||||
if (sigil == cah.$.Sigil.ADMIN) {
|
||||
clazz = "admin";
|
||||
show = true;
|
||||
}
|
||||
if (data[cah.$.LongPollResponse.WALL]) {
|
||||
// treat these specially
|
||||
cah.log.everyWindow(
|
||||
"Global message from " + from + ": " + data[cah.$.LongPollResponse.MESSAGE], clazz);
|
||||
cah.log.everyWindow("Global message from " + sigil + from + ": "
|
||||
+ data[cah.$.LongPollResponse.MESSAGE], clazz, false, title);
|
||||
} else {
|
||||
if (cah.$.LongPollResponse.GAME_ID in data) {
|
||||
game = data[cah.$.LongPollResponse.GAME_ID];
|
||||
|
@ -113,9 +116,9 @@ cah.longpoll.EventHandlers[cah.$.LongPollEvent.CHAT] = function(data) {
|
|||
if (from != cah.nickname && show) {
|
||||
var message = data[cah.$.LongPollResponse.MESSAGE];
|
||||
if (data[cah.$.LongPollResponse.EMOTE]) {
|
||||
cah.log.status_with_game(game, "* " + from + " " + message, clazz);
|
||||
cah.log.status_with_game(game, "* " + sigil + from + " " + message, clazz, false, title);
|
||||
} else {
|
||||
cah.log.status_with_game(game, "<" + from + "> " + message, clazz);
|
||||
cah.log.status_with_game(game, "<" + sigil + from + "> " + message, clazz, false, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@ pyx.max_games=25
|
|||
pyx.include_inactive_cardsets=true
|
||||
pyx.broadcast_connects_and_disconnects=true
|
||||
pyx.global_chat_enabled=true
|
||||
# allow identification codes to be used without HTTPS
|
||||
pyx.insecure_id_allowed=true
|
||||
# set this to some secure random value, and never change it, unless you want to break all codes
|
||||
pyx.id_code_salt=
|
||||
|
||||
# for production use, use postgres
|
||||
#hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
|
|
|
@ -4,6 +4,7 @@ pyx.server.max_users=${pyx.max_users}
|
|||
pyx.server.max_games=${pyx.max_games}
|
||||
pyx.server.broadcast_connects_and_disconnects=${pyx.broadcast_connects_and_disconnects}
|
||||
pyx.server.global_chat_enabled=${pyx.global_chat_enabled}
|
||||
pyx.server.id_code_salt=${pyx.id_code_salt}
|
||||
pyx.build=${buildNumber}
|
||||
|
||||
# this is NOT allowed to be changed during a reload, as metrics depend on previous events
|
||||
|
|
|
@ -168,6 +168,23 @@ public class CahModule extends AbstractModule {
|
|||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@InsecureIdAllowed
|
||||
Boolean provideInsecureIdAllowed() {
|
||||
synchronized (properties) {
|
||||
return Boolean.valueOf(properties.getProperty(
|
||||
"pyx.server.insecure_id_allowed", "true"));
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IdCodeSalt
|
||||
String provideIdCodeSalt() {
|
||||
synchronized (properties) {
|
||||
return properties.getProperty("pyx.server.id_code_salt", "");
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@CookieDomain
|
||||
String getCookieDomain() {
|
||||
|
@ -218,6 +235,16 @@ public class CahModule extends AbstractModule {
|
|||
public @interface GlobalChatEnabled {
|
||||
}
|
||||
|
||||
@BindingAnnotation
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface InsecureIdAllowed {
|
||||
}
|
||||
|
||||
@BindingAnnotation
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface IdCodeSalt {
|
||||
}
|
||||
|
||||
@BindingAnnotation
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CookieDomain {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -215,6 +215,7 @@ public class Constants {
|
|||
EMOTE("me"),
|
||||
GAME_ID("gid"),
|
||||
GAME_OPTIONS("go"),
|
||||
ID_CODE("idc"),
|
||||
MESSAGE("m"),
|
||||
NICKNAME("n"),
|
||||
OP("o"),
|
||||
|
@ -252,6 +253,8 @@ public class Constants {
|
|||
GAME_OPTIONS(AjaxRequest.GAME_OPTIONS),
|
||||
GAMES("gl"),
|
||||
HAND("h"),
|
||||
@DuplicationAllowed
|
||||
ID_CODE(AjaxRequest.ID_CODE),
|
||||
/**
|
||||
* Whether this client is reconnecting or not.
|
||||
*/
|
||||
|
@ -267,6 +270,10 @@ public class Constants {
|
|||
@DuplicationAllowed
|
||||
PERSISTENT_ID(AjaxRequest.PERSISTENT_ID),
|
||||
PLAYER_INFO("pi"),
|
||||
/**
|
||||
* Sigil to display next to user's name.
|
||||
*/
|
||||
SIGIL("?"),
|
||||
@DuplicationAllowed
|
||||
SERIAL(AjaxRequest.SERIAL),
|
||||
WHITE_CARDS("wc");
|
||||
|
@ -324,6 +331,8 @@ public class Constants {
|
|||
GAME_FULL("gf", "That game is full. Join another."),
|
||||
INVALID_CARD("ic", "Invalid card specified."),
|
||||
INVALID_GAME("ig", "Invalid game specified."),
|
||||
INVALID_ID_CODE("iid", "Identification code, if provided, must be between 8 and 100 characters,"
|
||||
+ " inclusive."),
|
||||
/**
|
||||
* TODO this probably should be pulled in from a static inside the RegisterHandler.
|
||||
*/
|
||||
|
@ -468,7 +477,9 @@ public class Constants {
|
|||
FROM("f"),
|
||||
/**
|
||||
* A chat message is from an admin. This is going to be done with IP addresses for now.
|
||||
* @deprecated Compare the SIGIL field to Sigil.ADMIN.
|
||||
*/
|
||||
@Deprecated
|
||||
FROM_ADMIN("fa"),
|
||||
@DuplicationAllowed
|
||||
GAME_ID(AjaxResponse.GAME_ID),
|
||||
|
@ -477,6 +488,8 @@ public class Constants {
|
|||
GAME_STATE("gs"),
|
||||
@DuplicationAllowed
|
||||
HAND(AjaxResponse.HAND),
|
||||
@DuplicationAllowed
|
||||
ID_CODE(AjaxRequest.ID_CODE),
|
||||
/**
|
||||
* The delay until the next game round begins.
|
||||
*/
|
||||
|
@ -493,6 +506,11 @@ public class Constants {
|
|||
*/
|
||||
REASON("qr"),
|
||||
ROUND_WINNER("rw"),
|
||||
/**
|
||||
* Sigil to display next to user's name.
|
||||
*/
|
||||
@DuplicationAllowed
|
||||
SIGIL(AjaxResponse.SIGIL),
|
||||
TIMESTAMP("ts"),
|
||||
@DuplicationAllowed
|
||||
WALL(AjaxRequest.WALL),
|
||||
|
@ -516,6 +534,24 @@ public class Constants {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User sigils. Displayed before the user's name.
|
||||
*/
|
||||
public enum Sigil {
|
||||
ADMIN("@"), ID_CODE("+"), NORMAL_USER("");
|
||||
|
||||
private final String sigil;
|
||||
|
||||
Sigil(final String sigil) {
|
||||
this.sigil = sigil;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return sigil;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data fields for white cards.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -35,6 +35,13 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
|
||||
import net.socialgamer.cah.CahModule.BroadcastConnectsAndDisconnects;
|
||||
import net.socialgamer.cah.CahModule.MaxUsers;
|
||||
import net.socialgamer.cah.Constants.DisconnectReason;
|
||||
|
@ -46,13 +53,6 @@ import net.socialgamer.cah.data.QueuedMessage.MessageType;
|
|||
import net.socialgamer.cah.metrics.GeoIP;
|
||||
import net.socialgamer.cah.metrics.Metrics;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import com.maxmind.geoip2.model.CityResponse;
|
||||
|
||||
|
||||
/**
|
||||
* Class that holds all users connected to the server, and provides functions to operate on said
|
||||
|
@ -124,8 +124,8 @@ public class ConnectedUsers {
|
|||
user.toString(), users.size(), maxUsers));
|
||||
return ErrorCode.TOO_MANY_USERS;
|
||||
} else {
|
||||
logger.info(String.format("New user %s from %s (admin=%b)", user.toString(),
|
||||
user.getHostname(), user.isAdmin()));
|
||||
logger.info(String.format("New user %s from %s (admin=%b, id=%s)", user.toString(),
|
||||
user.getHostname(), user.isAdmin(), user.getIdCode()));
|
||||
users.put(user.getNickname().toLowerCase(), user);
|
||||
if (broadcastConnectsAndDisconnectsProvider.get()) {
|
||||
final HashMap<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
|
||||
|
@ -191,7 +191,8 @@ public class ConnectedUsers {
|
|||
*/
|
||||
private void notifyRemoveUser(final User user, final DisconnectReason reason) {
|
||||
// Games are informed about the user leaving when the user object is marked invalid.
|
||||
if (broadcastConnectsAndDisconnectsProvider.get()) {
|
||||
if (broadcastConnectsAndDisconnectsProvider.get() || reason == DisconnectReason.BANNED
|
||||
|| reason == DisconnectReason.KICKED) {
|
||||
final HashMap<ReturnableData, Object> data = new HashMap<ReturnableData, Object>();
|
||||
data.put(LongPollResponse.EVENT, LongPollEvent.PLAYER_LEAVE.toString());
|
||||
data.put(LongPollResponse.NICKNAME, user.getNickname());
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -32,12 +32,13 @@ import java.util.concurrent.PriorityBlockingQueue;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
|
||||
import net.sf.uadetector.ReadableUserAgent;
|
||||
import net.sf.uadetector.service.UADetectorServiceFactory;
|
||||
import net.socialgamer.cah.CahModule.UniqueId;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
import net.socialgamer.cah.Constants.Sigil;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -49,6 +50,8 @@ public class User {
|
|||
|
||||
private final String nickname;
|
||||
|
||||
private final String idCode;
|
||||
|
||||
private final PriorityBlockingQueue<QueuedMessage> queuedMessages;
|
||||
|
||||
private final Object queuedMessageSynchronization = new Object();
|
||||
|
@ -83,6 +86,9 @@ public class User {
|
|||
*
|
||||
* @param nickname
|
||||
* The user's nickname.
|
||||
* @param idCode
|
||||
* The user's ID code, after hashing with salt and their name, or the empty string if
|
||||
* none provided.
|
||||
* @param hostname
|
||||
* The user's Internet hostname (which will likely just be their IP address).
|
||||
* @param isAdmin
|
||||
|
@ -91,9 +97,14 @@ public class User {
|
|||
* This user's persistent (cross-session) ID.
|
||||
* @param sessionId
|
||||
* The unique ID of this session for this server instance.
|
||||
* @param clientLanguage
|
||||
* The language of the user's web browser/client.
|
||||
* @param clientAgent
|
||||
* The name of the user's web browser/client.
|
||||
*/
|
||||
@Inject
|
||||
public User(@Assisted("nickname") final String nickname,
|
||||
@Assisted("idCode") final String idCode,
|
||||
@Assisted("hostname") final String hostname,
|
||||
@Assisted final boolean isAdmin,
|
||||
@Assisted("persistentId") final String persistentId,
|
||||
|
@ -101,6 +112,7 @@ public class User {
|
|||
@Nullable @Assisted("clientLanguage") final String clientLanguage,
|
||||
@Nullable @Assisted("clientAgent") final String clientAgent) {
|
||||
this.nickname = nickname;
|
||||
this.idCode = idCode;
|
||||
this.hostname = hostname;
|
||||
this.isAdmin = isAdmin;
|
||||
this.persistentId = persistentId;
|
||||
|
@ -111,10 +123,11 @@ public class User {
|
|||
}
|
||||
|
||||
public interface Factory {
|
||||
User create(@Assisted("nickname") String nickname, @Assisted("hostname") String hostname,
|
||||
boolean isAdmin, @Assisted("persistentId") String persistentId,
|
||||
@Assisted("clientLanguage") String clientLanguage,
|
||||
@Assisted("clientAgent") String clientAgent);
|
||||
User create(@Assisted("nickname") String nickname, @Assisted("idCode") String idCode,
|
||||
@Assisted("hostname") String hostname, boolean isAdmin,
|
||||
@Assisted("persistentId") String persistentId,
|
||||
@Nullable @Assisted("clientLanguage") String clientLanguage,
|
||||
@Nullable @Assisted("clientAgent") String clientAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -185,6 +198,20 @@ public class User {
|
|||
return isAdmin;
|
||||
}
|
||||
|
||||
public String getIdCode() {
|
||||
return idCode;
|
||||
}
|
||||
|
||||
public Sigil getSigil() {
|
||||
if (isAdmin) {
|
||||
return Sigil.ADMIN;
|
||||
} else if (!idCode.isEmpty()) {
|
||||
return Sigil.ID_CODE;
|
||||
} else {
|
||||
return Sigil.NORMAL_USER;
|
||||
}
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
|
|
@ -106,6 +106,8 @@ public class ChatHandler extends Handler {
|
|||
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.CHAT.toString());
|
||||
broadcastData.put(LongPollResponse.FROM, user.getNickname());
|
||||
broadcastData.put(LongPollResponse.MESSAGE, message);
|
||||
broadcastData.put(LongPollResponse.ID_CODE, user.getIdCode());
|
||||
broadcastData.put(LongPollResponse.SIGIL, user.getSigil().toString());
|
||||
if (user.isAdmin()) {
|
||||
broadcastData.put(LongPollResponse.FROM_ADMIN, true);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -28,6 +28,8 @@ import java.util.Map;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import net.socialgamer.cah.Constants;
|
||||
import net.socialgamer.cah.Constants.AjaxOperation;
|
||||
import net.socialgamer.cah.Constants.AjaxRequest;
|
||||
|
@ -41,8 +43,6 @@ import net.socialgamer.cah.data.GameManager;
|
|||
import net.socialgamer.cah.data.QueuedMessage.MessageType;
|
||||
import net.socialgamer.cah.data.User;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
|
||||
/**
|
||||
* Handler for chat messages.
|
||||
|
@ -91,6 +91,8 @@ public class GameChatHandler extends GameWithPlayerHandler {
|
|||
broadcastData.put(LongPollResponse.FROM, user.getNickname());
|
||||
broadcastData.put(LongPollResponse.MESSAGE, message);
|
||||
broadcastData.put(LongPollResponse.FROM_ADMIN, user.isAdmin());
|
||||
broadcastData.put(LongPollResponse.ID_CODE, user.getIdCode());
|
||||
broadcastData.put(LongPollResponse.SIGIL, user.getSigil().toString());
|
||||
broadcastData.put(LongPollResponse.GAME_ID, game.getId());
|
||||
broadcastData.put(LongPollResponse.EMOTE, emote);
|
||||
game.broadcastToPlayers(MessageType.CHAT, broadcastData);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -31,6 +31,8 @@ import java.util.Map;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import net.socialgamer.cah.Constants.AjaxOperation;
|
||||
import net.socialgamer.cah.Constants.AjaxResponse;
|
||||
import net.socialgamer.cah.Constants.ReturnableData;
|
||||
|
@ -38,8 +40,6 @@ import net.socialgamer.cah.RequestWrapper;
|
|||
import net.socialgamer.cah.data.ConnectedUsers;
|
||||
import net.socialgamer.cah.data.User;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
|
||||
/**
|
||||
* Handler to get the names of all players connected to the server.
|
||||
|
@ -61,11 +61,10 @@ public class NamesHandler extends Handler {
|
|||
public Map<ReturnableData, Object> handle(final RequestWrapper request,
|
||||
final HttpSession session) {
|
||||
final Map<ReturnableData, Object> ret = new HashMap<ReturnableData, Object>();
|
||||
// TODO once there are multiple rooms, we need which one was asked for
|
||||
final Collection<User> userList = users.getUsers();
|
||||
final List<String> names = new ArrayList<String>(userList.size());
|
||||
for (final User u : userList) {
|
||||
names.add(u.getNickname());
|
||||
names.add(u.getSigil() + u.getNickname());
|
||||
}
|
||||
ret.put(AjaxResponse.NAMES, names);
|
||||
return ret;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -30,6 +30,12 @@ import java.util.regex.Pattern;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.HttpHeaders;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import net.socialgamer.cah.CahModule.BanList;
|
||||
import net.socialgamer.cah.CahModule.UserPersistentId;
|
||||
import net.socialgamer.cah.Constants;
|
||||
|
@ -42,12 +48,7 @@ import net.socialgamer.cah.Constants.SessionAttribute;
|
|||
import net.socialgamer.cah.RequestWrapper;
|
||||
import net.socialgamer.cah.data.ConnectedUsers;
|
||||
import net.socialgamer.cah.data.User;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.http.HttpHeaders;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import net.socialgamer.cah.util.IdCodeMangler;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -59,21 +60,25 @@ public class RegisterHandler extends Handler {
|
|||
|
||||
public static final String OP = AjaxOperation.REGISTER.toString();
|
||||
|
||||
private static final Pattern validName = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]{2,29}");
|
||||
private static final Pattern VALID_NAME = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]{2,29}");
|
||||
private static final int ID_CODE_MIN_LENGTH = 8;
|
||||
private static final int ID_CODE_MAX_LENGTH = 100;
|
||||
|
||||
private final ConnectedUsers users;
|
||||
private final Set<String> banList;
|
||||
private final User.Factory userFactory;
|
||||
private final Provider<String> persistentIdProvider;
|
||||
private final IdCodeMangler idCodeManger;
|
||||
|
||||
@Inject
|
||||
public RegisterHandler(final ConnectedUsers users, @BanList final Set<String> banList,
|
||||
final User.Factory userFactory,
|
||||
final User.Factory userFactory, final IdCodeMangler idCodeMangler,
|
||||
@UserPersistentId final Provider<String> persistentIdProvider) {
|
||||
this.users = users;
|
||||
this.banList = banList;
|
||||
this.userFactory = userFactory;
|
||||
this.persistentIdProvider = persistentIdProvider;
|
||||
this.idCodeManger = idCodeMangler;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,9 +92,13 @@ public class RegisterHandler extends Handler {
|
|||
|
||||
if (request.getParameter(AjaxRequest.NICKNAME) == null) {
|
||||
return error(ErrorCode.NO_NICK_SPECIFIED);
|
||||
} else if (request.getParameter(AjaxRequest.ID_CODE) != null
|
||||
&& (request.getParameter(AjaxRequest.ID_CODE).trim().length() < ID_CODE_MIN_LENGTH
|
||||
|| request.getParameter(AjaxRequest.ID_CODE).trim().length() > ID_CODE_MAX_LENGTH)) {
|
||||
return error(ErrorCode.INVALID_ID_CODE);
|
||||
} else {
|
||||
final String nick = request.getParameter(AjaxRequest.NICKNAME).trim();
|
||||
if (!validName.matcher(nick).matches()) {
|
||||
if (!VALID_NAME.matcher(nick).matches()) {
|
||||
return error(ErrorCode.INVALID_NICK);
|
||||
} else if ("xyzzy".equalsIgnoreCase(nick)) {
|
||||
return error(ErrorCode.RESERVED_NICK);
|
||||
|
@ -99,7 +108,10 @@ public class RegisterHandler extends Handler {
|
|||
persistentId = persistentIdProvider.get();
|
||||
}
|
||||
|
||||
final User user = userFactory.create(nick, request.getRemoteAddr(),
|
||||
final String mangledIdCode = idCodeManger.mangle(nick,
|
||||
request.getParameter(AjaxRequest.ID_CODE));
|
||||
|
||||
final User user = userFactory.create(nick, mangledIdCode, request.getRemoteAddr(),
|
||||
Constants.ADMIN_IP_ADDRESSES.contains(request.getRemoteAddr()), persistentId,
|
||||
request.getHeader(HttpHeaders.ACCEPT_LANGUAGE),
|
||||
request.getHeader(HttpHeaders.USER_AGENT));
|
||||
|
@ -116,6 +128,8 @@ public class RegisterHandler extends Handler {
|
|||
|
||||
data.put(AjaxResponse.NICKNAME, nick);
|
||||
data.put(AjaxResponse.PERSISTENT_ID, persistentId);
|
||||
data.put(AjaxResponse.ID_CODE, user.getIdCode());
|
||||
data.put(AjaxResponse.SIGIL, user.getSigil().toString());
|
||||
} else {
|
||||
return error(errorCode);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.google.inject.Key;
|
|||
|
||||
import net.socialgamer.cah.CahModule.CookieDomain;
|
||||
import net.socialgamer.cah.CahModule.GlobalChatEnabled;
|
||||
import net.socialgamer.cah.CahModule.InsecureIdAllowed;
|
||||
import net.socialgamer.cah.StartupUtils;
|
||||
|
||||
|
||||
|
@ -77,8 +78,11 @@ public class JavascriptConfigServlet extends HttpServlet {
|
|||
final Injector injector = (Injector) getServletContext().getAttribute(StartupUtils.INJECTOR);
|
||||
final String cookieDomain = injector.getInstance(Key.get(String.class, CookieDomain.class));
|
||||
final Boolean globalChatEnabled = injector.getInstance(Key.get(Boolean.class, GlobalChatEnabled.class));
|
||||
final Boolean insecureIdAllowed = injector
|
||||
.getInstance(Key.get(Boolean.class, InsecureIdAllowed.class));
|
||||
builder.append(String.format("cah.COOKIE_DOMAIN = '%s';\n", cookieDomain));
|
||||
builder.append(String.format("cah.GLOBAL_CHAT_ENABLED = %b;\n", globalChatEnabled));
|
||||
builder.append(String.format("cah.INSECURE_ID_ALLOWED = %b;\n", insecureIdAllowed));
|
||||
|
||||
resp.setContentType("text/javascript");
|
||||
final PrintWriter out = resp.getWriter();
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Copyright (c) 2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
* provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this list of conditions
|
||||
* and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright notice, this list of
|
||||
* conditions and the following disclaimer in the documentation and/or other materials provided
|
||||
* with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
||||
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
|
||||
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package net.socialgamer.cah.util;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import net.socialgamer.cah.CahModule.IdCodeSalt;
|
||||
|
||||
|
||||
public class IdCodeMangler {
|
||||
private static final Logger LOG = Logger.getLogger(IdCodeMangler.class);
|
||||
|
||||
private final String salt;
|
||||
private final Base64.Encoder encoder = Base64.getEncoder();
|
||||
|
||||
@Inject
|
||||
public IdCodeMangler(@IdCodeSalt final String salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public String mangle(final String username, final String idCode) {
|
||||
if (null == idCode || idCode.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
final byte[] plaintext = (salt + username + idCode.trim()).getBytes(Charset.forName("UTF-8"));
|
||||
// 32 byte output
|
||||
final byte[] digest = md.digest(plaintext);
|
||||
final byte[] condensed = new byte[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
condensed[i] = (byte) (digest[i] ^ digest[i + 8] ^ digest[i + 16] ^ digest[i + 24]);
|
||||
}
|
||||
return encoder.encodeToString(condensed).substring(0, 11);
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
LOG.error("Unable to mangle ID code.", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (c) 2012-2017, Andy Janata
|
||||
* Copyright (c) 2012-2018, Andy Janata
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
|
@ -38,13 +38,13 @@ import java.util.Collection;
|
|||
import java.util.HashMap;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import net.socialgamer.cah.data.Game.TooManyPlayersException;
|
||||
import net.socialgamer.cah.data.QueuedMessage.MessageType;
|
||||
import net.socialgamer.cah.metrics.Metrics;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@code Game}.
|
||||
|
@ -78,8 +78,8 @@ public class GameTest {
|
|||
expectLastCall().once();
|
||||
replay(gmMock);
|
||||
|
||||
final User user1 = new User("test1", "test.lan", false, "1", "1", "en-US", "JUnit");
|
||||
final User user2 = new User("test2", "test.lan", false, "2", "2", "en-US", "JUnit");
|
||||
final User user1 = new User("test1", null, "test.lan", false, "1", "1", "en-US", "JUnit");
|
||||
final User user2 = new User("test2", null, "test.lan", false, "2", "2", "en-US", "JUnit");
|
||||
game.addPlayer(user1);
|
||||
game.addPlayer(user2);
|
||||
|
||||
|
|
Loading…
Reference in New Issue