package org.activityinfo.server.endpoint.gwtrpc; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import com.google.common.io.InputSupplier; import com.google.gwt.user.server.rpc.SerializationPolicy; import com.google.gwt.user.server.rpc.SerializationPolicyLoader; import com.google.inject.Inject; import org.activityinfo.server.util.blob.BlobService; import javax.servlet.ServletContext; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.logging.Level; import java.util.logging.Logger; /** * Provides SerializationPolicy from archives in Google Cloud Storage. * <p/> * <p/> * GWT uses a file called STRONGNAME.gwt.rpc to determine which types are * allowed to be deserialized on the server. This is to stop bad actors from * being able to instantiate whatever java objects they want via GWT-RPC. * <p/> * <p/> * STRONGNAME is the MD5 hash of the compiled javascript, unique to a specific * version and browser/language combination. * <p/> * <p/> * Because we use the AppCache to aggressively cache the client on the user's * browser, it is very common that when the user opens the client after a few * days away, the server has a new version (we are pushing out updates several * times a week). * <p/> * <p/> * Unfortunately, this means that the server will not be able to find the * permutation's corresponding .gwt.rpc file, and the RPC command will fail with * a IncompatibleRemoteException. This forces the user to download the new * version of the app before continuing. * <p/> * <p/> * Ideally what we want is the user to be able to use the application while the * AppCache fetches the new version in the background. * <p/> * <p/> * To accomplish this, we archive any gwt.rpc file that ever gets requested in * Google Cloud Storage, and then check this archive each time a gwt.rpc file is * requested. This way, even */ public class PersistentPolicyProvider { private static final Logger LOGGER = Logger.getLogger(PersistentPolicyProvider.class.getName()); private ServletContext servletContext; private BlobService blobService; @Inject public PersistentPolicyProvider(BlobService blobService, ServletContext servletContext) { this.blobService = blobService; this.servletContext = servletContext; } public SerializationPolicy getSerializationPolicy(String moduleBaseURL, String strongName) { LOGGER.info("Loading serialization policy " + strongName); SerializationPolicy policy = readFromDeployment(moduleBaseURL, strongName); if (policy == null) { policy = tryFetchFromBlobService(keyName(strongName)); } if (policy == null) { policy = tryFetchFromBlobService(legacyKeyName(strongName)); } return policy; } private String legacyKeyName(String strongName) { return "/gs/activityinfo-gwt-rpc/" + strongName + ".gwt.rpc"; } private SerializationPolicy tryFetchFromBlobService(String key) { try { LOGGER.fine("Trying to read serialization policy from blobservice at " + key); InputSupplier<? extends InputStream> inputSupplier = blobService.get(key); InputStream in = inputSupplier.getInput(); try { SerializationPolicy policy = SerializationPolicyLoader.loadFromStream(in, null); LOGGER.info("Read serialization policy from blob service at " + key); return policy; } finally { Closeables.closeQuietly(in); } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not load serialization policy from cache", e); } return null; } private String keyName(String strongName) { return "/gwt-rpc/" + strongName; } private SerializationPolicy readFromDeployment(String moduleBaseURL, String strongName) { // Read the serialization policy from the // deployed application files byte[] bytes; SerializationPolicy policy; try { String file = deploymentPath(moduleBaseURL, strongName); InputStream in = servletContext.getResourceAsStream(file); bytes = ByteStreams.toByteArray(in); in.close(); policy = SerializationPolicyLoader.loadFromStream(new ByteArrayInputStream(bytes), null); LOGGER.info("Read serialization policy " + strongName + " from deployment"); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to read serialization policy from deployment", e); return null; } // cache to blob service for later try { blobService.put(keyName(strongName), ByteStreams.newInputStreamSupplier(bytes)); LOGGER.info("Cached policy " + strongName + " to blob service"); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Could not cache serialization policy", e); } return policy; } private String deploymentPath(String moduleBaseURL, String strongName) throws MalformedURLException { String modulePath = new URL(moduleBaseURL).getPath(); return modulePath + strongName + ".gwt.rpc"; } }