package com.thinkminimo.golf; import com.thinkminimo.getopt.GetOpt; import org.json.*; import java.io.*; import java.util.*; import java.util.regex.Pattern; import java.net.URL; import java.net.URI; import java.net.URISyntaxException; import java.net.URLClassLoader; import java.rmi.server.UID; import java.security.NoSuchAlgorithmException; import java.lang.reflect.Method; import java.lang.reflect.Constructor; import net.sourceforge.htmlunit.corejs.javascript.ErrorReporter; import net.sourceforge.htmlunit.corejs.javascript.EvaluatorException; import com.yahoo.platform.yui.compressor.CssCompressor; import com.yahoo.platform.yui.compressor.JavaScriptCompressor; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.jets3t.service.CloudFrontService; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import org.jets3t.service.acl.AccessControlList; import org.jets3t.service.acl.GroupGrantee; import org.jets3t.service.acl.Permission; import org.jets3t.service.model.S3Bucket; import org.jets3t.service.model.S3Object; import org.jets3t.service.model.cloudfront.Distribution; import org.jets3t.service.model.cloudfront.DistributionConfig; import org.jets3t.service.impl.rest.httpclient.RestS3Service; import org.jets3t.service.security.AWSCredentials; import org.apache.tools.ant.*; import org.apache.tools.ant.taskdefs.*; import org.apache.tools.ant.types.resources.*; import org.mortbay.log.Log; import org.mortbay.jetty.Handler; import org.mortbay.jetty.HandlerContainer; import org.mortbay.jetty.Server; import org.mortbay.jetty.handler.HandlerList; import org.mortbay.jetty.handler.ContextHandlerCollection; import org.mortbay.jetty.handler.DefaultHandler; import org.mortbay.jetty.handler.ResourceHandler; import org.mortbay.jetty.handler.RequestLogHandler; import org.mortbay.jetty.servlet.Context; import org.mortbay.jetty.servlet.ServletHolder; import org.mortbay.jetty.servlet.DefaultServlet; import org.mortbay.jetty.webapp.WebAppContext; import org.mortbay.thread.QueuedThreadPool; public class Main { public static class RingList extends ArrayList { private int next = 0; public T next() { return get(next++ % size()); } public void reset() { next = 0; } } public static final String AWS_URL = "s3.amazonaws.com"; public static final int NUM_CFDOMAINS = 1; public static final int NUM_VMPOOL = 20; public static final int NUM_VMEXPIRE = 10; public static final int JETTY_PORT = 4653; private static final int BUF_SIZE = 1024; public static final String NEW_HTML = "new.html"; public static final String NEW_FC_HTML = "new.fc.html"; public static final String NEW_ST_HTML = "new.static.html"; public static final String ERROR_HTML = "error.html"; public static final String HEAD_HTML = "head.html"; public static final String JSDETECT_HTML = "jsdetect.html"; public static final String COMPONENTS_JS = "components.js"; public static final String CONTROLLER_JS = "controller.js"; public static final String JQUERY_JS = "jquery.js"; public static final String JQUERY_GOLF_JS = "jquery.golf.js"; public static final String JQUERY_HIST_JS = "jquery.address.js"; public static final String FORCEPROXY_TXT = "forceproxy.txt"; public static final String FORCECLIENT_TXT = "forceclient.txt"; public static final String FORCEBOT_TXT = "forcebot.txt"; public static final String NOSCRIPT_HTML = "noscript.html"; public static final String NOSCRIPT_FC_HTML= "noscript.forceclient.html"; public static final String NOSCRIPT_ST_HTML= "noscript.static.html"; public static final String LOADING_GIF = "loading.gif"; public static final String DIR_COMPONENTS = "components"; public static final String DIR_MODULES = "plugins"; public static final String DIR_SCRIPTS = "scripts"; public static final String DIR_STYLES = "styles"; private static GetOpt o = null; private static RingList mCfDomains = null; private static String mNewHtml = null; private static String mAppName = null; private static String mAppVersion = null; private static HashMap mApps = null; private static HashMap mBackends = null; private AWSCredentials mAwsKeys = null; private RestS3Service mS3svc = null; private CloudFrontService mCfsvc = null; private S3Bucket mBucket = null; private AccessControlList mAcl = null; public Main(String[] argv) throws Exception { mApps = new HashMap(); mBackends = new HashMap(); mCfDomains = new RingList(); mAppVersion = getResourceAsString("version").replaceFirst("\n", ""); // process single flag command lines if (argv[0].equals("--version")) { System.out.println(mAppVersion); System.exit(0); } // Command line parser setup o = new GetOpt("golf", argv); o.addFlag( "version", "Display golf application server version info and exit." ).addSection( "GENERAL OPTIONS", "General configuration of the golf application server. These options "+ "will be rolled into the war file (where appropriate) if deploying to "+ "production, or used in the built-in servlet container for devmode "+ "operation." ).addOpt( "port", "Set the port the server will listen on (optional, devmode only)." ).addOpt( "displayname", "The display name to use for deploying as a war file into a servlet "+ "container (optional)." ).addOpt( "description", "Description of app when deploying as a war file into a servlet "+ "container (optional)." ).addOpt( "pool-size", "How many concurrent proxymode client virtual machines to allow." ).addOpt( "pool-expire", "Minimum idle time (seconds) before a proxymode client virtual "+ "machine can be scavenged." ).addOpt( "static", "Destination directory for a static app deployment. Static apps are "+ "compiled into pure HTML+JS. There is no proxy support with this option." ).addSection( "AMAZON WEB SERVICES CONFIGURATION OPTIONS", "The awspublic and awsprivate options provide the golf server with "+ "your AWS credentials. This enables it to automatically upload the "+ "application to CloudFront when deploying to production. AWS is not "+ "used in devmode." ).addOpt( "awspublic", "The amazon aws access key ID to use for cloudfront caching (required "+ "when using AWS)." ).addOpt( "awsprivate", "The amazon aws secret access key corresponding to the aws access "+ "key ID specified with the --awspublic option (required when using AWS)." ).addOpt( "cloudfronts", "How many CloudFront distributions to create (optional). This may be "+ "useful for getting browsers to load things in parallel rather than "+ "one at a time. On the other hand, it may be useless." ).addSection( "HTTP PROXY CONFIGURATION OPTIONS", "The golf application server ships with a built-in HTTP proxy servlet "+ "that can be used to provide access to backend web services without "+ "needing to resort to using JSONP or the 'window.name' hack." ).addOpt( "proxyhost", "The remote URI that the HTTP proxy will relay requests to. This will "+ "produce a war file containing the configured proxy servlet and exit, "+ "instead of starting the embedded servlet container." ).addOpt( "proxyparams", "Parameters to add to the query string of every request sent by the "+ "HTTP proxy to the remote host. This can be used to pass tokens that "+ "the client shouldn't have access to, and things like that." ).addOpt( "proxymaxupload", "The maximum file upload size for HTTP proxy requests (optional, in "+ "bytes)." ).addSection( "WAR FILE CONFIGURATION OPTIONS", "The golf application server jar file is able to roll a golf "+ "application into a war file for deployment to a servlet container "+ "for production. These options govern how this is done." ).addFlag( "compress-js", "Whether to yuicompress javascript resource files (production only)." ).addFlag( "compress-css", "Whether to yuicompress css resource files (production only)." ).addFlag( "war", "If present, create war file instead of starting embedded servlet "+ "container." ).addArg( "approot|proxypath", "The location of the golf app root directory, or when building HTTP "+ "proxy, the desired name of the resulting war file (.war extension "+ "will be added). When this argument specifies a golf approot, the "+ "application context path (or war file name, in the case of the --war "+ "option) is derived from the basename of the specified directory." ).addVarArg( "backend [backend...]", "The backend webapp war file or approot (not used when building HTTP "+ "proxy war files). Zero or more war files or directories may be "+ "specified here. The context path they are deployed to is taken from "+ "the basename of the war file or directory." ).addExample( "RUN LOCAL DEVMODE SERVER WITH NO BACKEND", "java -jar golf.jar ./apps/demo", "This starts the golf application server, running the golf application "+ "locally from the embedded servlet container. The application will be "+ "accessible at the URL http://localhost:4653/demo/, and the approot is "+ "set to the ./apps/demo/ directory." ).addExample( "RUN LOCAL DEVMODE SERVER WITH BACKEND", "java -jar golf.jar ./apps/demo data1.war data2.war", "This starts the golf application server, locally, as in the previous "+ "example, accessible at the URL http://localhost:4653/demo/, etc. "+ "Additionally, data1.war and data2.war (backend applications) will be "+ "deployed to the /data1/ and /data2/ context paths." ).addExample( "PREPARE WAR FILE FOR DEPLOYMENT TO PRODUCTION", "java -jar golf.jar --war ./apps/demo", "Instead of starting the local golf application server, a war file is "+ "produced containing the golf application, in this case 'demo.war' is "+ "the resulting file. This war file can then be deployed to the "+ "production servlet container." ).addExample( "PREPARE WAR FILE FOR DEPLOYMENT TO PRODUCTION WITH AWS", "java -jar golf.jar --displayname 'My Golf App' \\\n"+ " --awspublic GKI69AJ344JLNT92X1QQ \\\n"+ " --awsprivate ke9S3CwVzLW9B21/HrkLiQfXEpoeGHwNDTlfBW5J \\\n"+ " --war ./apps/demo", "As in the previous example, a war file is produced instead of starting "+ "the local golf application server. Additionally, the golf application "+ "is uploaded to Amazon's s3 service, and a CloudFront distribution is "+ "created. The golf app in the resulting war file will automatically "+ "load the frontend from CloudFront, rather than from the golf server." ).addExample( "CREATE A HTTP PROXY", "java -jar golf.jar --proxyhost www.example.com:8080/doit/ \\\n"+ " --proxyparams 'user=myname&pass=secret' data", "This produces a HTTP proxy servlet configured to proxy HTTP requests "+ "to the specified remote URI, instead of starting the local embedded "+ "servlet container. The resulting war file will be saved to 'data.war'." ); // default values for command line options o.setOpt("port", String.valueOf(JETTY_PORT)); o.setOpt("displayname", "untitled web application"); o.setOpt("description", "powered by golf: http://thinkminimo.com"); o.setOpt("devmode", "false"); o.setOpt("awspublic", null); o.setOpt("awsprivate", null); o.setOpt("proxyhost", null); o.setOpt("proxyparams", ""); o.setOpt("proxymaxupload",String.valueOf(10*1024*1024)); o.setOpt("pool-size", String.valueOf(NUM_VMPOOL)); o.setOpt("pool-expire", String.valueOf(NUM_VMEXPIRE)); o.setOpt("cloudfronts", String.valueOf(NUM_CFDOMAINS)); o.setOpt("cfdomains", "[]"); o.setOpt("compress-js", "false"); o.setOpt("compress-css", "false"); o.setOpt("war", "false"); // parse command line try { o.go(); } catch (Exception e) { System.exit(1); } // command line option validation if (o.getOpt("port") != null) { try { Integer.parseInt(o.getOpt("port")); } catch (NumberFormatException e) { usage(e.getMessage()); } } // start work mAppName = (new File(o.getOpt("approot|proxypath"))) .getCanonicalFile().getName().replaceFirst("\\.war$", ""); mApps.put(mAppName, o.getOpt("approot|proxypath")); while (o.getExtra().size() > 0) { String path = o.getExtra().remove(0); File f = new File(path); String name = f.getCanonicalFile().getName().replaceFirst("\\.war$", ""); mBackends.put(name, path); } try { if (o.getOpt("proxyhost") != null) doProxyWarfile(); else if (o.getFlag("war")) doWarfile(); else if (o.getOpt("static") != null) doStatic(); else doServer(); } catch (Exception e) { System.err.println("golf: "+e.getMessage()); System.exit(1); } System.exit(0); } public static void main(String[] argv) { try { Main m = new Main(argv); } catch (Exception e) { System.exit(1); } } private void prepareAws() throws Exception { mAwsKeys = new AWSCredentials(o.getOpt("awspublic"), o.getOpt("awsprivate")); mS3svc = new RestS3Service(mAwsKeys); while (true) { mBucket = mS3svc.getOrCreateBucket(randName(mAppName)); long nowTime = (new Date()).getTime(); long bktTime = mBucket.getCreationDate().getTime(); long oneMin = 1L * 60L * 1000L; if (nowTime - bktTime < oneMin) break; } mAcl = mS3svc.getBucketAcl(mBucket); mAcl.grantPermission( GroupGrantee.ALL_USERS, Permission.PERMISSION_READ ); mBucket.setAcl(mAcl); mS3svc.putBucketAcl(mBucket); } private void doCloudFront() throws Exception { mCfsvc = new CloudFrontService(mAwsKeys); String orig = mBucket.getName() + "." + AWS_URL; String cmnt; if (o.getOpt("description") != null) cmnt = o.getOpt("description"); else if (o.getOpt("displayname") != null) cmnt = o.getOpt("displayname"); else cmnt = mAppName; JSONArray json = new JSONArray(); for (int i=0; i\n"+headStr+ " \n"+noscriptStr+ "