/** * Copyright 2010 The University of Nottingham * * This file is part of lobbyservice. * * lobbyservice 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. * * lobbyservice 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 lobbyservice. If not, see <http://www.gnu.org/licenses/>. * */ package uk.ac.horizon.ug.lobby.browser; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.users.User; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.TreeSet; import java.util.logging.Logger; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Query; import javax.servlet.http.*; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONWriter; import uk.ac.horizon.ug.lobby.ConfigurationUtils; import uk.ac.horizon.ug.lobby.Constants; import uk.ac.horizon.ug.lobby.HttpUtils; import uk.ac.horizon.ug.lobby.RequestException; import uk.ac.horizon.ug.lobby.browser.JoinUtils.JoinAuthInfo; import uk.ac.horizon.ug.lobby.model.Account; import uk.ac.horizon.ug.lobby.model.EMF; import uk.ac.horizon.ug.lobby.model.GUIDFactory; import uk.ac.horizon.ug.lobby.model.GameClient; import uk.ac.horizon.ug.lobby.model.GameClientTemplate; import uk.ac.horizon.ug.lobby.model.GameIndex; import uk.ac.horizon.ug.lobby.model.GameInstance; import uk.ac.horizon.ug.lobby.model.GameInstanceFactory; import uk.ac.horizon.ug.lobby.model.GameInstanceFactoryStatus; import uk.ac.horizon.ug.lobby.model.GameInstanceFactoryType; import uk.ac.horizon.ug.lobby.model.GameInstanceNominalStatus; import uk.ac.horizon.ug.lobby.model.GameInstanceSlot; import uk.ac.horizon.ug.lobby.model.GameInstanceSlotStatus; import uk.ac.horizon.ug.lobby.model.GameServer; import uk.ac.horizon.ug.lobby.model.GameServerStatus; import uk.ac.horizon.ug.lobby.model.GameTemplate; import uk.ac.horizon.ug.lobby.model.GameTemplateVisibility; import uk.ac.horizon.ug.lobby.model.ServerConfiguration; import uk.ac.horizon.ug.lobby.protocol.GameJoinRequest; import uk.ac.horizon.ug.lobby.protocol.GameJoinRequestType; import uk.ac.horizon.ug.lobby.protocol.GameJoinResponse; import uk.ac.horizon.ug.lobby.protocol.GameJoinResponseStatus; import uk.ac.horizon.ug.lobby.protocol.GameQuery; import uk.ac.horizon.ug.lobby.protocol.GameTemplateInfo; import uk.ac.horizon.ug.lobby.protocol.JSONUtils; import uk.ac.horizon.ug.lobby.protocol.LocationConstraint; import uk.ac.horizon.ug.lobby.protocol.TimeConstraint; import uk.ac.horizon.ug.lobby.server.CronExpressionException; import uk.ac.horizon.ug.lobby.server.FactoryTasks; import uk.ac.horizon.ug.lobby.server.FactoryUtils; import uk.ac.horizon.ug.lobby.server.ServerProtocol; import uk.ac.horizon.ug.lobby.user.UserGameTemplateServlet; import uk.me.jstott.jcoord.LatLng; /** * Handle request to create new GameInstance from Factory. * * @author cmg * */ @SuppressWarnings("serial") public class NewGameInstanceServlet extends HttpServlet implements Constants { static Logger logger = Logger.getLogger(NewGameInstanceServlet.class.getName()); private GameInstanceFactory getGameInstanceFactory(HttpServletRequest req) throws RequestException { String id = HttpUtils.getIdFromPath(req); EntityManager em = EMF.get().createEntityManager(); try { Key key = KeyFactory.stringToKey(id); GameInstanceFactory gt = em.find(GameInstanceFactory.class, key); if (gt==null) throw new RequestException(HttpServletResponse.SC_NOT_FOUND, "GameInstanceFactory "+id+" not found"); return gt; } finally { em.close(); } } @Override public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { // parse request String line = null; String auth = null; GameJoinRequest gjreq = null; try { BufferedReader br = req.getReader(); line = br.readLine(); JSONObject json = new JSONObject(line); gjreq = JSONUtils.parseGameJoinRequest(json); // second line is digital signature (if given) auth = br.readLine(); logger.info("GameJoinRequest "+gjreq); // check type supported... if (gjreq.getType()!=GameJoinRequestType.NEW_INSTANCE) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request type must be NEW_INSTANCE ("+gjreq.getType()+")"); return; } } catch (JSONException e) { logger.warning(e.toString()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.toString()); return; } //ServerConfiguration sc = ConfigurationUtils.getServerConfiguration(); try { GameInstanceFactory gif = getGameInstanceFactory(req); GameJoinResponse gjresp = new GameJoinResponse(); gjresp.setTime(System.currentTimeMillis()); // validate gjresp.setType(gjreq.getType()); // authenticate client JoinUtils.ClientInfo clientInfo = new JoinUtils.ClientInfo(gjreq.getCharacteristicsJson()); JoinUtils.JoinAuthInfo jai = JoinUtils.authenticate(gjreq.getClientId(), gjreq.getDeviceId(), clientInfo, gif.isAllowAnonymousClients(), req.getRequestURI(), line, auth); GameClient gc = jai.gc; Account account = jai.account; boolean anonymous = jai.anonymous; gjresp.setClientId(gc.getId()); handleNewInstanceRequest(gjreq, gjresp, jai, gif, req.getRemoteAddr()); JSONUtils.sendGameJoinResponse(resp, gjresp); } catch (RequestException e) { resp.sendError(e.getErrorCode(), e.getMessage()); return; } catch (JoinException e) { GameJoinResponse gjresp = new GameJoinResponse(); gjresp.setTime(System.currentTimeMillis()); gjresp.setType(gjreq.getType()); gjresp.setStatus(e.getStatus()); gjresp.setMessage(e.getMessage()); logger.warning(e.toString()); JSONUtils.sendGameJoinResponse(resp, gjresp); return; } catch (JSONException e) { logger.warning(e.toString()); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.toString()); return; } } private void handleNewInstanceRequest(GameJoinRequest gjreq, GameJoinResponse gjresp, JoinAuthInfo jai, GameInstanceFactory gif, String clientAddr) throws JoinException, RequestException, JSONException, IOException { GameClient gc = jai.gc; Account account = jai.account; boolean anonymous = jai.anonymous; // we know it is a NEW_INSTANCE request... if (gif.getType()!=GameInstanceFactoryType.ON_DEMAND) { throw new JoinException(GameJoinResponseStatus.ERROR_SCHEDULED_ONLY, "This game does not support on-request instances"); } if (gif.getStatus()!=GameInstanceFactoryStatus.ACTIVE) { throw new JoinException(GameJoinResponseStatus.ERROR_NOT_PERMITTED, "This game factory is not active"); } if (gif.getStartTimeOptionsJson()==null) { throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_INVALID, "This game factory has no available start time(s)"); } if (gjreq.getNewInstanceStartTime()==null) { throw new RequestException(HttpServletResponse.SC_BAD_REQUEST, "NEW_INSTANCE request must have newInstanceStartTime"); } long newInstanceStartTime = gjreq.getNewInstanceStartTime(); if (newInstanceStartTime<gif.getMinTime() || newInstanceStartTime>gif.getMaxTime()) { throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_INVALID, "NewInstanceStartTime out of range for this game factory"); } // lee-way in timing expressed (1 minute?!) long START_TIME_RANGE_MS = 60000; // allow start-up time on server long earliest = System.currentTimeMillis(); if (gif.getServerCreateTimeOffsetMs()<0) earliest = earliest - gif.getServerCreateTimeOffsetMs(); if (newInstanceStartTime+START_TIME_RANGE_MS < earliest) { throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_TOO_SOON, "NewInstanceStartTime too soon (in "+(newInstanceStartTime-System.currentTimeMillis())+"ms)"); } else if (newInstanceStartTime < earliest) // leave enough time... newInstanceStartTime = earliest; // round up to allowed times try { TreeSet timeOptions[] = FactoryUtils.parseTimeOptionsJson(gif.getStartTimeOptionsJson()); newInstanceStartTime = FactoryUtils.getNextCronTime(gif.getStartTimeCron(), timeOptions, newInstanceStartTime, gif.getMaxTime()); } catch (CronExpressionException e) { logger.warning("Checking nextStartTime: "+e); throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_INVALID, "Problem with checking nextStartTime"); } if (newInstanceStartTime!=0 && newInstanceStartTime>gjreq.getNewInstanceStartTime()+START_TIME_RANGE_MS) { if (gjreq.getNewInstanceStartTime() < earliest) throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_TOO_SOON, "NewInstanceStartTime too soon (in "+(newInstanceStartTime-System.currentTimeMillis())+"ms)"); else // rounded up 'too' far to find a valid start time throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_INVALID, "Proposed startTime is not (close to) a valid startTime"); } if (newInstanceStartTime==0 || newInstanceStartTime>gif.getMaxTime()) { throw new JoinException(GameJoinResponseStatus.ERROR_START_TIME_INVALID, "NewInstanceStartTime out of range for this game factory once correctly for allowed starts ("+newInstanceStartTime+")"); } GameInstance gi = null; if (gjreq.getNewInstanceVisibility()!=null && gjreq.getNewInstanceVisibility()!=gif.getInstanceVisibility() && gjreq.getNewInstanceVisibility()==GameTemplateVisibility.HIDDEN) // can't throw new JoinException(GameJoinResponseStatus.ERROR_NOT_PERMITTED, "Cannot create a hidden (private) instance of this game"); if (gjreq.getNewInstanceVisibility()==null || gjreq.getNewInstanceVisibility()!=GameTemplateVisibility.HIDDEN) { // check if it already exists... EntityManager em = EMF.get().createEntityManager(); try { // does this instance exist already? Query q = em.createQuery("SELECT x FROM GameInstance x WHERE x."+GAME_INSTANCE_FACTORY_KEY+" = :"+GAME_INSTANCE_FACTORY_KEY+" AND x."+START_TIME+" = :"+START_TIME+" AND x."+VISIBILITY+" = '"+GameTemplateVisibility.PUBLIC.toString()+"' AND x."+FULL+" = FALSE"); q.setParameter(GAME_INSTANCE_FACTORY_KEY, gif.getKey()); q.setParameter(START_TIME, newInstanceStartTime); q.setMaxResults(1); List<GameInstance> gis = (List<GameInstance>)q.getResultList(); if (gis.size()>0) { // essentially we now treat this as a JOIN ?! gi = gis.get(0); //et.rollback(); logger.info("NewGameInstance request satisfied by existing instance "+gi); } else { // debug... logger.info("No existing GameInstance matches (startTime="+newInstanceStartTime+")"); // q = em.createQuery("SELECT x FROM GameInstance x WHERE x."+GAME_INSTANCE_FACTORY_KEY); // q.setParameter(GAME_INSTANCE_FACTORY_KEY, gif.getKey()); // //q.setParameter(START_TIME, newInstanceStartTime); // gis = (List<GameInstance>)q.getResultList(); // logger.info(" "+gis.size()+" GIs match factory:"); // for (GameInstance gi2 : gis) // logger.info(" "+gi2.getKey()+" startTime="+gi2.getStartTime()+", visibility="+gi2.getVisibility()+", full="+gi2.isFull()); } } finally { em.close(); } } else logger.info("NewGameInstance request cannot use existing: newInstanceVisibility="+gjreq.getNewInstanceVisibility()); // needed in a minute ServerConfiguration sc = ConfigurationUtils.getServerConfiguration(); if (gi==null) { // doesn't exist - perhaps we'll make it // is it far enough in advance? // TODO // create new instance?! if (anonymous && !gif.isCreateForAnonymousClient()) { throw new JoinException(GameJoinResponseStatus.ERROR_USER_AUTHENTICATION_REQUIRED, "This game factory will not create for anonymous players"); } // update quota... (doesn't do anything else for non-scheduled factories) FactoryTasks.checkGameInstanceFactory(sc, gif); // check quota... EntityManager em = EMF.get().createEntityManager(); EntityTransaction et = em.getTransaction(); // transaction et.begin(); GameInstanceFactory ngif = null; try { ngif = em.find(GameInstanceFactory.class, gif.getKey()); int tokenCache = ngif.getNewInstanceTokens(); if (tokenCache<=0) { throw new JoinException(GameJoinResponseStatus.ERROR_SYSTEM_QUOTA_EXCEEDED, "This game factory cannot create any more instances at present"); } } finally { et.rollback(); em.close(); } // create! (over-ride visibility) gi = FactoryTasks.createGameInstanceFactoryInstance(ngif, gjreq.getNewInstanceVisibility(), newInstanceStartTime, account, clientAddr, null); } // new or existing?! // follow-on info gjresp.setJoinUrl(QueryGameTemplateServlet.makeJoinUrl(sc, gi)); // now attempt a RESERVE on the identified game instance gjreq.setType(GameJoinRequestType.RESERVE); JoinGameInstanceServlet.handleJoinRequestInternal(gjreq, gjresp, jai, gi); } }