Save cookies in the credentials file, not passwords.

I'm not willing to put my password in a plain text file, so I changed how the
credentials work. The save_credentials.sh script will prompt for your gwtproject.org
password, authenticate, and store the cookie. (It doesn't work with OTP.)

The cookie expires after a day, so you'll have to re-run the script to update the
credentials file. The expiration time is an App Engine setting, so we could change
it to a week if that's terribly inconvenient.

After saving your credentials, run 'upload.sh credentials' to deploy to production.
You can also run 'upload.sh localhost' to deploy to a local dev appengine running
on port 8080. Deploying locally doesn't use your credentials file, so you don't
have to edit it.

Also removed the Credentials class since it seemed redundant after this change.

Change-Id: I6caa5bd1572fdd2df69725fea22c7ec7c61abd5c
diff --git a/pom.xml b/pom.xml
index 4d114a7..50a4e09 100644
--- a/pom.xml
+++ b/pom.xml
@@ -94,21 +94,7 @@
 				<groupId>org.codehaus.mojo</groupId>
 				<artifactId>exec-maven-plugin</artifactId>
 				<version>1.1.1</version>
-				<executions>
-					<execution>
-						<phase>prepare-package</phase>
-						<goals>
-							<goal>java</goal>
-						</goals>
-						<configuration>
-							<mainClass>com.google.gwt.site.uploader.Uploader</mainClass>
-							<arguments>
-								<argument>${project.build.directory}/gwt-site-unpack</argument>
-								<argument>${basedir}/credentials</argument>
-							</arguments>
-						</configuration>
-					</execution>
-				</executions>
+
 			</plugin>
 
 		</plugins>
diff --git a/save_credentials.sh b/save_credentials.sh
new file mode 100755
index 0000000..278851c
--- /dev/null
+++ b/save_credentials.sh
@@ -0,0 +1,6 @@
+#!/bin/bash -e
+#
+# Run this command to save your account's cookie to 'credentials'
+
+mvn -q compile
+mvn -q exec:java -Dexec.mainClass=com.google.gwt.site.uploader.SaveCredentials
diff --git a/src/main/java/com/google/gwt/site/uploader/CredentialsProvider.java b/src/main/java/com/google/gwt/site/uploader/CredentialsProvider.java
index 37ddcdf..1f9e154 100644
--- a/src/main/java/com/google/gwt/site/uploader/CredentialsProvider.java
+++ b/src/main/java/com/google/gwt/site/uploader/CredentialsProvider.java
@@ -14,75 +14,76 @@
 
 package com.google.gwt.site.uploader;
 
+import com.google.appengine.tools.remoteapi.RemoteApiOptions;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.util.Properties;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import org.apache.commons.io.IOUtils;
-
-import com.google.gwt.site.uploader.model.Credentials;
-
 public class CredentialsProvider {
 
   private static final Logger logger = Logger.getLogger(CredentialsProvider.class.getName());
 
-  public Credentials readCredentialsFromFile(String credentialsFile) {
-    FileInputStream inputStream = null;
+  public RemoteApiOptions readCredentialsFromFile(String credentialsFile) throws IOException {
 
+    String serialized = Files.toString(new File(credentialsFile), Charsets.UTF_8);
+    Map<String, List<String>> props = parseProperties(serialized);
+
+    checkOneProperty(props, "host");
+    checkOneProperty(props, "email");
+
+    String host = props.get("host").get(0);
+    String email = props.get("email").get(0);
+
+    int port = 443;
     try {
-      inputStream = new FileInputStream(new File(credentialsFile));
-      Properties properties = new Properties();
-      properties.load(inputStream);
-      String username = properties.getProperty("username");
-      if (username == null) {
-
-        logger.log(Level.SEVERE, "No username found in credentials file, are you missing username=something");
-
-        throw new RuntimeException(
-            "No username found in credentials file, are you missing username=something");
+      if (props.containsKey("port")) {
+        checkOneProperty(props, "port");
+        port = Integer.parseInt(props.get("port").get(0));
       }
-
-      String password = properties.getProperty("password");
-      if (password == null) {
-
-        logger.log(Level.SEVERE, "No password found in credentials file, are you missing password=something");
-
-        throw new RuntimeException(
-            "No password found in credentials file, are you missing password=something");
-      }
-
-      String host = properties.getProperty("host");
-      if (host == null) {
-        logger.log(Level.SEVERE, "No host found in credentials file, are you missing host=something");
-
-        throw new RuntimeException(
-            "No host found in credentials file, are you missing host=something");
-      }
-
-      String portString = properties.getProperty("port");
-      if (portString == null) {
-        logger.log(Level.SEVERE, "No port found in credentials file, are you missing port=something");
-
-        throw new RuntimeException(
-            "No port found in credentials file, are you missing port=something");
-      }
-      try {
-        int port = Integer.parseInt(portString);
-        return new Credentials(host, port, username, password);
-      } catch (NumberFormatException e) {
-        logger.log(Level.SEVERE, "error while parsing port", e);
-        throw new RuntimeException("error while parsing port");
-      }
-
-    } catch (IOException e) {
-      logger.log(Level.SEVERE, "can not load credential files", e);
-
-      throw new RuntimeException("can not load credential files", e);
-    } finally {
-      IOUtils.closeQuietly(inputStream);
+    } catch (NumberFormatException e) {
+      logger.log(Level.SEVERE, "error while parsing port", e);
+      throw new RuntimeException("error while parsing port");
     }
+
+    return new RemoteApiOptions()
+        .server(host, port)
+        .reuseCredentials(email, serialized);
+  }
+
+  // taken from RemoteApiInstaller
+  private static void checkOneProperty(Map<String, List<String>> props, String key)
+      throws IOException {
+    if (props.get(key).size() != 1) {
+      String message = "invalid credential file (should have one property named '" + key + "')";
+      throw new IOException(message);
+    }
+  }
+
+  // taken from RemoteApiInstaller
+  private static Map<String, List<String>> parseProperties(String serializedCredentials) {
+    Map<String, List<String>> props = new HashMap<String, List<String>>();
+    for (String line : serializedCredentials.split("\n")) {
+      line = line.trim();
+      if (!line.startsWith("#") && line.contains("=")) {
+        int firstEqual = line.indexOf('=');
+        String key = line.substring(0, firstEqual);
+        String value = line.substring(firstEqual + 1);
+        List<String> values = props.get(key);
+        if (values == null) {
+          values = new ArrayList<String>();
+          props.put(key, values);
+        }
+        values.add(value);
+      }
+    }
+    return props;
   }
 }
diff --git a/src/main/java/com/google/gwt/site/uploader/ResourceUploaderAppEngineImpl.java b/src/main/java/com/google/gwt/site/uploader/ResourceUploaderAppEngineImpl.java
index 93b017c..a8fc602 100644
--- a/src/main/java/com/google/gwt/site/uploader/ResourceUploaderAppEngineImpl.java
+++ b/src/main/java/com/google/gwt/site/uploader/ResourceUploaderAppEngineImpl.java
@@ -14,17 +14,11 @@
 
 package com.google.gwt.site.uploader;
 
-import com.google.appengine.api.datastore.DatastoreService;
-import com.google.appengine.api.datastore.Entity;
-import com.google.appengine.api.datastore.Key;
-import com.google.appengine.api.datastore.KeyFactory;
-import com.google.appengine.api.datastore.Text;
+import com.google.appengine.api.datastore.*;
 import com.google.appengine.repackaged.com.google.api.client.util.Base64;
 import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
 import com.google.appengine.tools.remoteapi.RemoteApiOptions;
-import com.google.gwt.site.uploader.model.Credentials;
 import com.google.gwt.site.uploader.model.Resource;
-
 import org.apache.commons.io.IOUtils;
 import org.json.JSONArray;
 import org.json.JSONException;
@@ -61,13 +55,13 @@
   private static final String DOC_MODEL = "DocModel";
   private static final Logger logger = Logger.getLogger(ResourceUploaderAppEngineImpl.class
       .getName());
-  private final Credentials credentials;
+  private final RemoteApiOptions credentials;
   private final DatastoreService ds;
   private final RemoteApiInstaller installer;
   private boolean isInitialized;
   private final KeyProvider keyProvider;
 
-  public ResourceUploaderAppEngineImpl(DatastoreService ds, Credentials credentials,
+  public ResourceUploaderAppEngineImpl(DatastoreService ds, RemoteApiOptions credentials,
       RemoteApiInstaller installer, KeyProvider keyProvider) {
     this.ds = ds;
     this.credentials = credentials;
@@ -198,7 +192,7 @@
       protocol = "https://";
     }
 
-    return protocol + credentials.getHost() + ":" + credentials.getPort() + "/hash?count=" + count;
+    return protocol + credentials.getHostname() + ":" + credentials.getPort() + "/hash?count=" + count;
   }
 
   @Override
@@ -206,16 +200,8 @@
     if (isInitialized) {
       throw new IllegalStateException("app engine remote api was already initialized");
     }
-    String username = credentials.getUsername();
-    String password = credentials.getPassword();
-    String host = credentials.getHost();
-    int port = credentials.getPort();
-
-    RemoteApiOptions options =
-        new RemoteApiOptions().server(host, port).credentials(username, password);
-
     try {
-      installer.install(options);
+      installer.install(credentials);
       isInitialized = true;
     } catch (IOException e) {
       logger.log(Level.SEVERE, "can not initialize app engine", e);
diff --git a/src/main/java/com/google/gwt/site/uploader/SaveCredentials.java b/src/main/java/com/google/gwt/site/uploader/SaveCredentials.java
new file mode 100644
index 0000000..713905a
--- /dev/null
+++ b/src/main/java/com/google/gwt/site/uploader/SaveCredentials.java
@@ -0,0 +1,63 @@
+package com.google.gwt.site.uploader;
+
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.repackaged.com.google.common.io.Files;
+import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
+import com.google.appengine.tools.remoteapi.RemoteApiOptions;
+import com.google.common.base.Charsets;
+
+import java.io.File;
+import java.io.IOException;
+
+public class SaveCredentials {
+
+  public static void main(String[] args) {
+    try {
+      run();
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+
+  public static void run() throws IOException {
+    if (System.console() == null) {
+      System.out.println("System.console not available. Please re-run from a shell.");
+      System.exit(1);
+    }
+
+    File f = new File("credentials");
+    if (!f.exists()) {
+      p("Expected a file named 'credentials' to exist.");
+      p("It should be in gwt-site-uploader.");
+      p("Please make sure you're in the right directory and touch the file if it's not there.");
+      System.exit(1);
+    }
+    if (!f.canWrite()) {
+      p("Can't write to credentials.");
+      System.exit(1);
+    }
+
+    p("Please enter your gwtproject.org credentials.");
+
+    String email = System.console().readLine("Email: ");
+    char[] password = System.console().readPassword("Password: ");
+
+    RemoteApiOptions options =
+        new RemoteApiOptions().server("gwtproject-demo.appspot.com", 443).credentials(email, new String(password));
+    RemoteApiInstaller installer = new RemoteApiInstaller();
+    installer.install(options);
+    // test the connection
+    DatastoreServiceFactory.getDatastoreService().allocateIds("foo", 1);
+
+    String creds = installer.serializeCredentials();
+    Files.write(creds, f, Charsets.UTF_8);
+    System.out.println("Credentials saved.");
+
+    installer.uninstall();
+  }
+
+
+  private static void p(String s) {
+    System.out.println(s);
+  }
+}
diff --git a/src/main/java/com/google/gwt/site/uploader/Uploader.java b/src/main/java/com/google/gwt/site/uploader/Uploader.java
index dab1d47..9bfedf1 100644
--- a/src/main/java/com/google/gwt/site/uploader/Uploader.java
+++ b/src/main/java/com/google/gwt/site/uploader/Uploader.java
@@ -17,33 +17,29 @@
 import com.google.appengine.api.datastore.DatastoreService;
 import com.google.appengine.api.datastore.DatastoreServiceFactory;
 import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
-import com.google.gwt.site.uploader.model.Credentials;
+import com.google.appengine.tools.remoteapi.RemoteApiOptions;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.logging.Logger;
 
 public class Uploader {
 
   private static final Logger logger = Logger.getLogger(Uploader.class.getName());
 
-  public static void main(String[] args) {
+  public static void main(String[] args) throws IOException {
 
     if (args.length != 2) {
-      System.out.println("Usage Uploader <filesDir> <credentialsFile>");
-      throw new IllegalArgumentException("Usage Uploader <filesDir> <credentialsFile>");
+      System.out.println("Usage Uploader <filesDir> <credentialsFile>|localhost");
+      throw new IllegalArgumentException("Usage Uploader <filesDir> <credentialsFile>|localhost");
     }
 
     String filesDir = args[0];
     logger.info("files directory: '" + filesDir + "'");
 
-    String credentialsFile = args[1];
-    logger.info("credentials file: '" + credentialsFile + "'");
-
-    CredentialsProvider credentialsProvider = new CredentialsProvider();
-    Credentials credentials = credentialsProvider.readCredentialsFromFile(credentialsFile);
+    RemoteApiOptions credentials = loadCredentials(args[1]);
 
     DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
-
     RemoteApiInstaller installer = new RemoteApiInstaller();
 
     HashCalculator hashCalculator = new HashCalculatorSha1Impl();
@@ -56,4 +52,14 @@
     UploadController uploadController = new UploadController(fileTraverser, fileUploader);
     uploadController.uploadOutdatedFiles();
   }
+
+  private static RemoteApiOptions loadCredentials(String fileOrLocalhost) throws IOException {
+    if (fileOrLocalhost.equals("localhost")) {
+      // special case for dev server
+      return new RemoteApiOptions().server("localhost", 8080).credentials("nobody@google.com", "ignored");
+    } else {
+      logger.info("credentials file: '" + fileOrLocalhost + "'");
+      return new CredentialsProvider().readCredentialsFromFile(fileOrLocalhost);
+    }
+  }
 }
diff --git a/src/main/java/com/google/gwt/site/uploader/model/Credentials.java b/src/main/java/com/google/gwt/site/uploader/model/Credentials.java
deleted file mode 100644
index 2438983..0000000
--- a/src/main/java/com/google/gwt/site/uploader/model/Credentials.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2013 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.site.uploader.model;
-
-public class Credentials {
-
-  private final String host;
-  private final String username;
-  private final String password;
-  private final int port;
-
-  public Credentials(String host, int port, String username, String password) {
-    this.host = host;
-    this.port = port;
-    this.username = username;
-    this.password = password;
-  }
-
-  public String getHost() {
-    return host;
-  }
-
-  public String getUsername() {
-    return username;
-  }
-
-  public String getPassword() {
-    return password;
-  }
-
-  public int getPort() {
-    return port;
-  }
-}
diff --git a/src/test/java/com/google/gwt/site/uploader/ResourceUploaderTest.java b/src/test/java/com/google/gwt/site/uploader/ResourceUploaderTest.java
index 7541b16..ec7fd10 100644
--- a/src/test/java/com/google/gwt/site/uploader/ResourceUploaderTest.java
+++ b/src/test/java/com/google/gwt/site/uploader/ResourceUploaderTest.java
@@ -21,7 +21,6 @@
 import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
 import com.google.appengine.tools.remoteapi.RemoteApiOptions;
 import com.google.gwt.site.uploader.ResourceUploaderAppEngineImpl.KeyProvider;
-import com.google.gwt.site.uploader.model.Credentials;
 
 import org.apache.commons.io.IOUtils;
 import org.junit.Assert;
@@ -43,14 +42,13 @@
 public class ResourceUploaderTest {
 
   private DatastoreService ds;
-  private Credentials credentials;
   private RemoteApiInstaller remoteApiInstaller;
   private ResourceUploaderAppEngineImpl resourceUploader;
   private KeyProvider keyProvider;
 
   @Before
   public void setup() {
-    credentials = new Credentials("host", 80, "user", "pw");
+    RemoteApiOptions credentials = new RemoteApiOptions();
     ds = Mockito.mock(DatastoreService.class);
 
     remoteApiInstaller = Mockito.mock(RemoteApiInstaller.class);
diff --git a/upload.sh b/upload.sh
new file mode 100755
index 0000000..74015d8
--- /dev/null
+++ b/upload.sh
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+#
+# Run with 'localhost' to deploy locally.
+# Run with 'credentials' (after running save_credentials.sh) to deploy to prod.
+
+mvn -q compile generate-resources
+mvn -q exec:java -Dexec.mainClass=com.google.gwt.site.uploader.Uploader -Dexec.args="target/gwt-site-unpack $1"