Initial Cardcast support. Hard-coded to pull the Reject Pack for testing.

WARNING: This disables the JVM's SSL certificate checking, and only allows 'api.cardcastgame.com' when checking hosts. The proper solution would be to ensure that the JVM's trust store will accept that certificate.
This commit is contained in:
Andy Janata 2014-07-19 18:29:37 -07:00
parent f5060113db
commit 8638dcadd4
9 changed files with 471 additions and 8 deletions

View File

@ -33,6 +33,9 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextEvent;
import net.socialgamer.cah.cardcast.CardcastModule;
import net.socialgamer.cah.cardcast.CardcastService;
import org.apache.log4j.PropertyConfigurator; import org.apache.log4j.PropertyConfigurator;
import com.google.inject.Guice; import com.google.inject.Guice;
@ -108,6 +111,7 @@ public class StartupUtils extends GuiceServletContextListener {
reconfigureLogging(contextEvent.getServletContext()); reconfigureLogging(contextEvent.getServletContext());
reloadProperties(contextEvent.getServletContext()); reloadProperties(contextEvent.getServletContext());
CardcastService.hackSslVerifier();
} }
public static void reloadProperties(final ServletContext context) { public static void reloadProperties(final ServletContext context) {
@ -130,6 +134,6 @@ public class StartupUtils extends GuiceServletContextListener {
@Override @Override
protected Injector getInjector() { protected Injector getInjector() {
return Guice.createInjector(new CahModule()); return Guice.createInjector(new CahModule(), new CardcastModule());
} }
} }

View File

@ -0,0 +1,44 @@
package net.socialgamer.cah.cardcast;
import net.socialgamer.cah.data.BlackCard;
public class CardcastBlackCard extends BlackCard {
private final int id;
private final String text;
private final int draw;
private final int pick;
public CardcastBlackCard(final int id, final String text, final int draw, final int pick) {
this.id = id;
this.text = text;
this.draw = draw;
this.pick = pick;
}
@Override
public int getId() {
return id;
}
@Override
public String getText() {
return text;
}
@Override
public String getWatermark() {
return "CC";
}
@Override
public int getDraw() {
return draw;
}
@Override
public int getPick() {
return pick;
}
}

View File

@ -0,0 +1,61 @@
package net.socialgamer.cah.cardcast;
import java.util.HashSet;
import java.util.Set;
import net.socialgamer.cah.data.CardSet;
public class CardcastDeck extends CardSet {
private final String name;
private final String code;
private final String description;
private final Set<CardcastBlackCard> blackCards = new HashSet<CardcastBlackCard>();
private final Set<CardcastWhiteCard> whiteCards = new HashSet<CardcastWhiteCard>();
public CardcastDeck(final String name, final String code, final String description) {
this.name = name;
this.code = code;
this.description = description;
}
@Override
public int getId() {
return -Integer.parseInt(code, 36);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean isActive() {
return true;
}
@Override
public boolean isBaseDeck() {
return false;
}
@Override
public int getWeight() {
return Integer.MAX_VALUE;
}
@Override
public Set<CardcastBlackCard> getBlackCards() {
return blackCards;
}
@Override
public Set<CardcastWhiteCard> getWhiteCards() {
return whiteCards;
}
}

View File

@ -0,0 +1,33 @@
package net.socialgamer.cah.cardcast;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.atomic.AtomicInteger;
import net.socialgamer.cah.data.GameOptions;
import com.google.inject.AbstractModule;
import com.google.inject.BindingAnnotation;
import com.google.inject.Provides;
public class CardcastModule extends AbstractModule {
AtomicInteger cardId = new AtomicInteger(-(GameOptions.MAX_BLANK_CARD_LIMIT + 1));
@Override
protected void configure() {
}
@Provides
@CardcastCardId
Integer provideCardId() {
return cardId.decrementAndGet();
}
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface CardcastCardId {
/**/
}
}

View File

@ -0,0 +1,271 @@
package net.socialgamer.cah.cardcast;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.SoftReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import net.socialgamer.cah.cardcast.CardcastModule.CardcastCardId;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class CardcastService {
private static final Logger LOG = Logger.getLogger(CardcastService.class);
private static final String HOSTNAME = "api.cardcastgame.com";
/**
* Base URL to the Cardcast API.
*/
private static final String BASE_URL = "https://" + HOSTNAME + "/v1/decks/";
/**
* URL to the Cardcast API for information about a card set. The only format replacement is the
* string deck ID.
*/
private static final String CARD_SET_INFO_URL_FORMAT_STRING = BASE_URL + "%s";
/**
* URL to the Cardcast API for cards in a card set. The only format replacement is the string
* deck ID.
*/
private static final String CARD_SET_CARDS_URL_FORMAT_STRING = CARD_SET_INFO_URL_FORMAT_STRING
+ "/cards";
private static final int GET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(3);
/**
* How long to cache nonexistent card sets, or after an error occurs while querying for the card
* set. We need to do this to prevent DoS attacks.
*/
private static final long INVALID_SET_CACHE_LIFETIME = TimeUnit.SECONDS.toMillis(30);
/**
* How long to cache valid card sets.
*/
private static final long VALID_SET_CACHE_LIFETIME = TimeUnit.MINUTES.toMillis(15);
private static final Map<String, SoftReference<CardcastCacheEntry>> cache = Collections
.synchronizedMap(new HashMap<String, SoftReference<CardcastCacheEntry>>());
private final Provider<Integer> cardIdProvider;
@Inject
public CardcastService(@CardcastCardId final Provider<Integer> cardIdProvider) {
this.cardIdProvider = cardIdProvider;
}
private class CardcastCacheEntry {
final long expires;
final CardcastDeck deck;
CardcastCacheEntry(final long cacheLifetime, final CardcastDeck deck) {
this.expires = System.currentTimeMillis() + cacheLifetime;
this.deck = deck;
}
}
private CardcastCacheEntry checkCache(final String setId) {
final SoftReference<CardcastCacheEntry> soft = cache.get(setId);
if (null == soft) {
return null;
}
return soft.get();
}
public CardcastDeck loadSet(final String setId) {
final CardcastCacheEntry cached = checkCache(setId);
if (null != cached && cached.expires < System.currentTimeMillis()) {
return cached.deck;
}
try {
final String infoContent = getUrlContent(String
.format(CARD_SET_INFO_URL_FORMAT_STRING, setId));
if (null == infoContent) {
// failed to load
cacheMissingSet(setId);
return null;
}
final JSONObject info = (JSONObject) JSONValue.parse(infoContent);
final String cardContent = getUrlContent(String.format(
CARD_SET_CARDS_URL_FORMAT_STRING, setId));
if (null == cardContent) {
// failed to load
cacheMissingSet(setId);
return null;
}
final JSONObject cards = (JSONObject) JSONValue.parse(cardContent);
final String name = (String) info.get("name");
final String description = (String) info.get("description");
if (null == name || null == description || name.isEmpty()) {
// We require a name. Blank description is acceptable, but cannot be null.
cacheMissingSet(setId);
return null;
}
final CardcastDeck deck = new CardcastDeck(name, setId, description);
// load up the cards
final JSONArray blacks = (JSONArray) cards.get("calls");
if (null != blacks) {
for (final Object black : blacks) {
final JSONArray texts = (JSONArray) ((JSONObject) black).get("text");
if (null != texts) {
// TODO this is going to need some work to look pretty.
final List<String> strs = new ArrayList<String>(texts.size());
for (final Object o : texts) {
strs.add((String) o);
}
final String text = StringUtils.join(strs, "____");
final int pick = strs.size() - 1;
final int draw = (pick >= 3 ? pick - 1 : 0);
final CardcastBlackCard card = new CardcastBlackCard(cardIdProvider.get(), text, draw,
pick);
deck.getBlackCards().add(card);
}
}
}
final JSONArray whites = (JSONArray) cards.get("responses");
if (null != whites) {
for (final Object white : whites) {
final JSONArray texts = (JSONArray) ((JSONObject) white).get("text");
if (null != texts) {
// The white cards should only ever have one element in text, but let's be safe.
final List<String> strs = new ArrayList<String>(texts.size());
for (final Object o : texts) {
strs.add((String) o);
}
final String text = StringUtils.join(strs, "");
final CardcastWhiteCard card = new CardcastWhiteCard(cardIdProvider.get(), text);
deck.getWhiteCards().add(card);
}
}
}
cacheSet(setId, deck);
return deck;
} catch (final Exception e) {
LOG.error(String.format("Unable to load deck %s from Cardcast", setId), e);
e.printStackTrace();
cacheMissingSet(setId);
return null;
}
}
private void cachePut(final String setId, final CardcastDeck deck, final long timeout) {
LOG.info(String.format("Caching %s=%s for %d ms", setId, deck, timeout));
cache.put(setId, new SoftReference<CardcastCacheEntry>(new CardcastCacheEntry(timeout, deck)));
}
private void cacheSet(final String setId, final CardcastDeck deck) {
cachePut(setId, deck, VALID_SET_CACHE_LIFETIME);
}
private void cacheMissingSet(final String setId) {
cachePut(setId, null, INVALID_SET_CACHE_LIFETIME);
}
private String getUrlContent(final String urlStr) throws IOException {
final URL url = new URL(urlStr);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(false);
conn.setRequestMethod("GET");
conn.setInstanceFollowRedirects(true);
conn.setReadTimeout(GET_TIMEOUT);
conn.setConnectTimeout(GET_TIMEOUT);
final int code = conn.getResponseCode();
if (HttpURLConnection.HTTP_OK != code) {
LOG.error(String.format("Got HTTP response code %d from Cardcast for %s", code, urlStr));
return null;
}
final String contentType = conn.getContentType();
if (!"application/json".equals(contentType)) {
LOG.error(String.format("Got content-type %s from Cardcast for %s", contentType, urlStr));
return null;
}
final InputStream is = conn.getInputStream();
final InputStreamReader isr = new InputStreamReader(is);
final BufferedReader reader = new BufferedReader(isr);
final StringBuilder builder = new StringBuilder(4096);
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
builder.append('\n');
}
reader.close();
isr.close();
is.close();
return builder.toString();
}
public static void hackSslVerifier() {
// FIXME: My JVM doesn't like the certificate. I should go add StartSSL's root certificate to
// its trust store, and document steps. For now, I'm going to disable SSL certificate checking.
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(final X509Certificate[] certs, final String authType) {
}
@Override
public void checkServerTrusted(final X509Certificate[] certs, final String authType) {
}
}
};
try {
final SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (final Exception e) {
LOG.error("Unable to install trust-all security manager", e);
}
// Create host name verifier that only trusts cardcast
final HostnameVerifier allHostsValid = new HostnameVerifier() {
@Override
public boolean verify(final String hostname, final SSLSession session) {
return HOSTNAME.equals(hostname);
}
};
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
}
}

View File

@ -0,0 +1,35 @@
package net.socialgamer.cah.cardcast;
import net.socialgamer.cah.data.WhiteCard;
public class CardcastWhiteCard extends WhiteCard {
private final int id;
private final String text;
public CardcastWhiteCard(final int id, final String text) {
this.id = id;
this.text = text;
}
@Override
public int getId() {
return id;
}
@Override
public String getText() {
return text;
}
@Override
public String getWatermark() {
return "CC";
}
@Override
public boolean isWriteIn() {
return false;
}
}

View File

@ -49,6 +49,7 @@ import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData; import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.Constants.WhiteCardData; import net.socialgamer.cah.Constants.WhiteCardData;
import net.socialgamer.cah.SafeTimerTask; import net.socialgamer.cah.SafeTimerTask;
import net.socialgamer.cah.cardcast.CardcastService;
import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.QueuedMessage.MessageType; import net.socialgamer.cah.data.QueuedMessage.MessageType;
@ -144,6 +145,7 @@ public class Game {
private final Object roundTimerLock = new Object(); private final Object roundTimerLock = new Object();
private volatile ScheduledFuture<?> lastScheduledFuture; private volatile ScheduledFuture<?> lastScheduledFuture;
private final ScheduledThreadPoolExecutor globalTimer; private final ScheduledThreadPoolExecutor globalTimer;
private final Provider<CardcastService> cardcastServiceProvider;
/** /**
* Create a new game. * Create a new game.
@ -161,12 +163,14 @@ public class Game {
@Inject @Inject
public Game(@GameId final Integer id, final ConnectedUsers connectedUsers, public Game(@GameId final Integer id, final ConnectedUsers connectedUsers,
final GameManager gameManager, final ScheduledThreadPoolExecutor globalTimer, final GameManager gameManager, final ScheduledThreadPoolExecutor globalTimer,
final Provider<Session> sessionProvider) { final Provider<Session> sessionProvider,
final Provider<CardcastService> cardcastServiceProvider) {
this.id = id; this.id = id;
this.connectedUsers = connectedUsers; this.connectedUsers = connectedUsers;
this.gameManager = gameManager; this.gameManager = gameManager;
this.globalTimer = globalTimer; this.globalTimer = globalTimer;
this.sessionProvider = sessionProvider; this.sessionProvider = sessionProvider;
this.cardcastServiceProvider = cardcastServiceProvider;
state = GameState.LOBBY; state = GameState.LOBBY;
} }
@ -648,6 +652,9 @@ public class Game {
final List<CardSet> cardSets = session.createQuery("from PyxCardSet where id in (:ids)") final List<CardSet> cardSets = session.createQuery("from PyxCardSet where id in (:ids)")
.setParameterList("ids", options.getPyxCardSetIds()).list(); .setParameterList("ids", options.getPyxCardSetIds()).list();
// FIXME hardcode hack for testing GBTXA
cardSets.add(cardcastServiceProvider.get().loadSet("GBTXA"));
blackDeck = new BlackDeck(cardSets); blackDeck = new BlackDeck(cardSets);
whiteDeck = new WhiteDeck(cardSets, options.blanksInDeck); whiteDeck = new WhiteDeck(cardSets, options.blanksInDeck);
} catch (final Exception e) { } catch (final Exception e) {

View File

@ -40,6 +40,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import net.socialgamer.cah.HibernateUtil; import net.socialgamer.cah.HibernateUtil;
import net.socialgamer.cah.cardcast.CardcastModule.CardcastCardId;
import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.GameManager.MaxGames; import net.socialgamer.cah.data.GameManager.MaxGames;
import net.socialgamer.cah.data.QueuedMessage.MessageType; import net.socialgamer.cah.data.QueuedMessage.MessageType;
@ -114,6 +115,13 @@ public class GameManagerTest {
Session provideSession() { Session provideSession() {
return HibernateUtil.instance.sessionFactory.openSession(); return HibernateUtil.instance.sessionFactory.openSession();
} }
@SuppressWarnings("unused")
@Provides
@CardcastCardId
Integer provideCardcastCardId() {
return 0;
}
}); });
gameManager = injector.getInstance(GameManager.class); gameManager = injector.getInstance(GameManager.class);
@ -135,11 +143,11 @@ public class GameManagerTest {
// fill it up with 3 games // fill it up with 3 games
assertEquals(0, gameManager.get().intValue()); assertEquals(0, gameManager.get().intValue());
gameManager.getGames().put(0, new Game(0, cuMock, gameManager, timer, null)); gameManager.getGames().put(0, new Game(0, cuMock, gameManager, timer, null, null));
assertEquals(1, gameManager.get().intValue()); assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1, new Game(1, cuMock, gameManager, timer, null)); gameManager.getGames().put(1, new Game(1, cuMock, gameManager, timer, null, null));
assertEquals(2, gameManager.get().intValue()); assertEquals(2, gameManager.get().intValue());
gameManager.getGames().put(2, new Game(2, cuMock, gameManager, timer, null)); gameManager.getGames().put(2, new Game(2, cuMock, gameManager, timer, null, null));
// make sure it says it can't make any more // make sure it says it can't make any more
assertEquals(-1, gameManager.get().intValue()); assertEquals(-1, gameManager.get().intValue());
@ -147,13 +155,13 @@ public class GameManagerTest {
gameManager.destroyGame(1); gameManager.destroyGame(1);
// make sure it re-uses that id // make sure it re-uses that id
assertEquals(1, gameManager.get().intValue()); assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1, new Game(1, cuMock, gameManager, timer, null)); gameManager.getGames().put(1, new Game(1, cuMock, gameManager, timer, null, null));
assertEquals(-1, gameManager.get().intValue()); assertEquals(-1, gameManager.get().intValue());
// remove game 1 out from under it, to make sure it'll fix itself // remove game 1 out from under it, to make sure it'll fix itself
gameManager.getGames().remove(1); gameManager.getGames().remove(1);
assertEquals(1, gameManager.get().intValue()); assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1, new Game(1, cuMock, gameManager, timer, null)); gameManager.getGames().put(1, new Game(1, cuMock, gameManager, timer, null, null));
assertEquals(-1, gameManager.get().intValue()); assertEquals(-1, gameManager.get().intValue());
gameManager.destroyGame(2); gameManager.destroyGame(2);

View File

@ -61,7 +61,7 @@ public class GameTest {
public void setUp() throws Exception { public void setUp() throws Exception {
cuMock = createMock(ConnectedUsers.class); cuMock = createMock(ConnectedUsers.class);
gmMock = createMock(GameManager.class); gmMock = createMock(GameManager.class);
game = new Game(0, cuMock, gmMock, timer, null); game = new Game(0, cuMock, gmMock, timer, null, null);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")