/**
*
* Copyright 2003-2004 The Apache Software Foundation
*
* 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 org.apache.geronimo.deployment.cli;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.JarFile;
import javax.enterprise.deploy.shared.factories.DeploymentFactoryManager;
import javax.enterprise.deploy.spi.DeploymentManager;
import javax.enterprise.deploy.spi.exceptions.DeploymentManagerCreationException;
import javax.enterprise.deploy.spi.factories.DeploymentFactory;
import org.apache.geronimo.common.DeploymentException;
import org.apache.geronimo.deployment.plugin.factories.AuthenticationFailedException;
import org.apache.geronimo.deployment.plugin.factories.DeploymentFactoryImpl;
import org.apache.geronimo.deployment.plugin.jmx.JMXDeploymentManager;
import org.apache.geronimo.util.SimpleEncryption;
/**
* Supports online connections to the server, via JSR-88, valid only
* when the server is online.
*
* @version $Rev$ $Date$
*/
public class ServerConnection {
private final static Map OPTION_HELP = new LinkedHashMap(9);
static {
OPTION_HELP.put("--uri", "A URI to contact the server. If not specified, the deployer defaults to " +
"operating on a Geronimo server running on the standard port on localhost.\n" +
"A URI to connect to Geronimo (including optional host and port parameters) has the form: " +
"deployer:geronimo:jmx[://host[:port]] (though you could also just use --host and --port instead).");
OPTION_HELP.put("--host", "The host name of a Geronimo server to deploy to. This option is " +
"not compatible with --uri, but is often used with --port.");
OPTION_HELP.put("--port", "The RMI listen port of a Geronimo server to deploy to. This option is " +
"not compatible with --uri, but is often used with --host. The default port is 1099.");
OPTION_HELP.put("--driver", "If you want to use this tool with a server other than Geronimo, " +
"then you must provide the path to its driver JAR. Currently, manifest " +
"Class-Path entries in that JAR are ignored.");
OPTION_HELP.put("--user", "If the deployment operation requires authentication, then you can " +
"specify the username to use to connect. If no password is specified, the " +
"deployer will attempt to connect to the server with no password, and if " +
"that fails, will prompt you for a password.");
OPTION_HELP.put("--password", "Specifies a password to use to authenticate to the server.");
OPTION_HELP.put("--syserr", "Enables error logging to syserr. Disabled by default.");
OPTION_HELP.put("--verbose", "Enables verbose execution mode. Disabled by default.");
}
public static Map getOptionHelp() {
return OPTION_HELP;
}
/**
* Checks whether the stated command-line argument is a general argument (which
* may be the general argument itself, or a required parameter after the general
* argument). For example, if the arguments were "--user bob foo" then
* this should return true for "--user" and "bob" and false for "foo".
*
* @param args The previous arguments on the command line
* @param option The argument we're checking at the moment
*
* @return True if the argument we're checking is part of a general argument
*/
public static boolean isGeneralOption(List args, String option) {
if(OPTION_HELP.containsKey(option) || option.equals("--url")) {
return true;
}
if(args.size() == 0) {
return false;
}
String last = (String) args.get(args.size()-1);
return last.equals("--uri") || last.equals("--url") || last.equals("--driver") || last.equals("--user") ||
last.equals("--password") || last.equals("--host") || last.equals("--port");
}
private final static String DEFAULT_URI = "deployer:geronimo:jmx";
private DeploymentManager manager;
private PrintWriter out;
private BufferedReader in;
private SavedAuthentication auth;
private boolean logToSysErr;
private boolean verboseMessages;
public ServerConnection(String[] args, PrintWriter out, BufferedReader in) throws DeploymentException {
String uri = null, driver = null, user = null, password = null, host = null;
Integer port = null;
this.out = out;
this.in = in;
for(int i = 0; i < args.length; i++) {
String arg = args[i];
if(arg.equals("--uri") || arg.equals("--url")) {
if(uri != null) {
throw new DeploymentSyntaxException("Cannot specify more than one URI");
} else if(i >= args.length-1) {
throw new DeploymentSyntaxException("Must specify a URI (e.g. --uri deployer:...)");
}
if(host != null || port != null) {
throw new DeploymentSyntaxException("Cannot specify a URI as well as a host/port");
}
uri = args[++i];
} else if(arg.equals("--host")) {
if(host != null) {
throw new DeploymentSyntaxException("Cannot specify more than one host");
} else if(i >= args.length-1) {
throw new DeploymentSyntaxException("Must specify a hostname (e.g. --host localhost)");
}
if(uri != null) {
throw new DeploymentSyntaxException("Cannot specify a URI as well as a host/port");
}
host = args[++i];
} else if(arg.equals("--port")) {
if(port != null) {
throw new DeploymentSyntaxException("Cannot specify more than one port");
} else if(i >= args.length-1) {
throw new DeploymentSyntaxException("Must specify a port (e.g. --port 1099)");
}
if(uri != null) {
throw new DeploymentSyntaxException("Cannot specify a URI as well as a host/port");
}
try {
port = new Integer(args[++i]);
} catch (NumberFormatException e) {
throw new DeploymentSyntaxException("Port must be a number ("+e.getMessage()+")");
}
} else if(arg.equals("--driver")) {
if(driver != null) {
throw new DeploymentSyntaxException("Cannot specify more than one driver");
} else if(i >= args.length-1) {
throw new DeploymentSyntaxException("Must specify a driver JAR (--driver jarfile)");
}
driver = args[++i];
} else if(arg.equals("--offline")) {
throw new DeploymentSyntaxException("This tool no longer handles offline deployment");
} else if(arg.equals("--user")) {
if(user != null) {
throw new DeploymentSyntaxException("Cannot specify more than one user name");
} else if(i >= args.length-1) {
throw new DeploymentSyntaxException("Must specify a username (--user username)");
}
user = args[++i];
} else if(arg.equals("--password")) {
if(password != null) {
throw new DeploymentSyntaxException("Cannot specify more than one password");
} else if(i >= args.length-1) {
throw new DeploymentSyntaxException("Must specify a password (--password password)");
}
password = args[++i];
} else if (arg.equals("--verbose")) {
verboseMessages = true;
} else if (arg.equals("--syserr")) {
logToSysErr = true;
} else {
throw new DeploymentException("Invalid option "+arg);
}
}
if((driver != null) && uri == null) {
throw new DeploymentSyntaxException("A custom driver requires a custom URI");
}
if(host != null || port != null) {
uri = DEFAULT_URI+"://"+(host == null ? "" : host)+(port == null ? "" : ":"+port);
}
tryToConnect(uri, driver, user, password, true);
if(manager == null) {
throw new DeploymentException("Unexpected error; connection failed.");
}
}
public void close() throws DeploymentException {
if(manager != null) {
manager.release();
}
}
Serializable getAuthentication() {
return auth;
}
String getServerURI() {
return auth.uri;
}
private void tryToConnect(String argURI, String driver, String user, String password, boolean authPrompt) throws DeploymentException {
DeploymentFactoryManager mgr = DeploymentFactoryManager.getInstance();
if(driver != null) {
loadDriver(driver, mgr);
} else {
mgr.registerDeploymentFactory(new DeploymentFactoryImpl());
}
String useURI = argURI == null ? DEFAULT_URI : argURI;
if(authPrompt && user == null && password == null) {
File authFile = new File(System.getProperty("user.home"), ".geronimo-deployer");
if(authFile.exists() && authFile.canRead()) {
try {
Properties props = new Properties();
InputStream in = new BufferedInputStream(new FileInputStream(authFile));
props.load(in);
in.close();
String encryped = props.getProperty("login."+useURI);
if(encryped != null) {
if(encryped.startsWith("{Standard}")) {
SavedAuthentication auth = (SavedAuthentication) SimpleEncryption.decrypt(encryped.substring(10));
if(auth.uri.equals(useURI)) {
user = auth.user;
password = new String(auth.password);
}
} else if(encryped.startsWith("{Plain}")) {
int pos = encryped.indexOf("/");
user = encryped.substring(7, pos);
password = encryped.substring(pos+1);
} else {
System.out.println(DeployUtils.reformat("Unknown encryption used in saved login file", 4, 72));
}
}
} catch (IOException e) {
System.out.println(DeployUtils.reformat("Unable to read authentication from saved login file: "+e.getMessage(), 4, 72));
}
}
}
if(authPrompt && !useURI.equals(DEFAULT_URI) && user == null && password == null) {
// Non-standard URI, but no authentication information
doAuthPromptAndRetry(useURI, user, password);
return;
} else { // Standard URI with no auth, Non-standard URI with auth, or else this is the 2nd try already
try {
manager = mgr.getDeploymentManager(useURI, user, password);
auth = new SavedAuthentication(useURI, user, password == null ? null : password.toCharArray());
} catch(AuthenticationFailedException e) { // server's there, you just can't talk to it
if(authPrompt) {
doAuthPromptAndRetry(useURI, user, password);
return;
} else {
throw new DeploymentException("Login Failed");
}
} catch(DeploymentManagerCreationException e) {
throw new DeploymentException("Unable to connect to server at "+useURI+" -- "+e.getMessage());
}
}
if (manager instanceof JMXDeploymentManager) {
JMXDeploymentManager deploymentManager = (JMXDeploymentManager) manager;
deploymentManager.setLogConfiguration(logToSysErr, verboseMessages);
}
}
private void loadDriver(String driver, DeploymentFactoryManager mgr) throws DeploymentException {
File file = new File(driver);
if(!file.exists() || !file.canRead() || !DeployUtils.isJarFile(file)) {
throw new DeploymentSyntaxException("Driver '"+file.getAbsolutePath()+"' is not a readable JAR file");
}
String className = null;
try {
JarFile jar = new JarFile(file);
className = jar.getManifest().getMainAttributes().getValue("J2EE-DeploymentFactory-Implementation-Class");
if(className == null) {
throw new DeploymentException("The driver JAR "+file.getAbsolutePath()+" does not specify a J2EE-DeploymentFactory-Implementation-Class; cannot load driver.");
}
jar.close();
DeploymentFactory factory = (DeploymentFactory) Class.forName(className).newInstance();
mgr.registerDeploymentFactory(factory);
} catch(DeploymentException e) {
throw e;
} catch(Exception e) {
throw new DeploymentSyntaxException("Unable to load driver class "+className+" from JAR "+file.getAbsolutePath(), e);
}
}
private void doAuthPromptAndRetry(String uri, String user, String password) throws DeploymentException {
try {
if(user == null) {
out.print("Username: ");
out.flush();
user = in.readLine();
}
if(password == null) {
password = new PasswordPrompt("Password: ", out).getPassword(in);
}
} catch(IOException e) {
throw new DeploymentException("Unable to prompt for login", e);
}
tryToConnect(uri, null, user, password, false);
}
public DeploymentManager getDeploymentManager() {
return manager;
}
public boolean isGeronimo() {
return manager.getClass().getName().startsWith("org.apache.geronimo.");
}
/**
* Prompts for and grabs a password, trying to suppress any console output
* along the way. Kind of heavy-handed, but we don't have access to any
* platform-specific APIs that might make this nicer.
*/
public static class PasswordPrompt implements Runnable {
private volatile boolean done = false;
private String prompt;
private PrintWriter out;
public PasswordPrompt(String prompt, PrintWriter out) {
this.prompt = prompt;
this.out = out;
}
/**
* Don't call this directly.
*/
public void run() {
int priority = Thread.currentThread().getPriority();
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
String fullPrompt = "\r"+prompt+" "+"\r"+prompt;
StringBuffer clearline = new StringBuffer();
clearline.append('\r');
for(int i=prompt.length()+10; i>=0; i--) {
clearline.append(' ');
}
while(!done) {
out.print(fullPrompt);
out.flush();
Thread.sleep(1);
}
out.print(clearline.toString());
out.flush();
out.println();
out.flush();
} catch (InterruptedException e) {
} finally {
Thread.currentThread().setPriority(priority);
}
prompt = null;
out = null;
}
/**
* Displays the prompt, grabs the password, cleans up, and returns
* the entered password. For this to make sense, the input reader
* here must be part of the same console as the output writer passed
* to the constructor.
*
* For higher security, should return a char[], but that will just
* be defeated by the JSR-88 call that takes a String anyway, so
* why bother?
*/
public String getPassword(BufferedReader in) throws IOException {
Thread t = new Thread(this, "Password hiding thread");
t.start();
String password = in.readLine();
done = true;
try {
t.join();
} catch (InterruptedException e) {
}
return password;
}
}
private final static class SavedAuthentication implements Serializable {
private String uri;
private String user;
private char[] password;
public SavedAuthentication(String uri, String user, char[] password) {
this.uri = uri;
this.user = user;
this.password = password;
}
}
}