/*
*
* Paros and its related class files.
*
* Paros is an HTTP/HTTPS proxy for assessing web application security.
* Copyright (C) 2003-2004 Chinotec Technologies Company
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Clarified Artistic License
* as published by the Free Software Foundation.
*
* 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
* Clarified Artistic License for more details.
*
* You should have received a copy of the Clarified Artistic License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
// ZAP: 2011/04/16 Support for running ZAP as a daemon
// ZAP: 2012/03/15 Removed unnecessary castings from methods parse, getArgument and getHelp.
// Changed to use the class StringBuilder instead of the class StringBuffer in the method getHelp.
// ZAP: 2012/10/15 Issue 397: Support weekly builds
// ZAP: 2013/03/03 Issue 546: Remove all template Javadoc comments
// ZAP: 2013/03/20 Issue 568: Allow extensions to run from the command line
// ZAP: 2013/08/30 Issue 775: Allow host to be set via the command line
// ZAP: 2013/12/03 Issue 933: Automatically determine install dir
// ZAP: 2013/12/03 Issue 934: Handle files on the command line via extension
// ZAP: 2014/01/17 Issue 987: Allow arbitrary config file values to be set via the command line
// ZAP: 2014/05/20 Issue 1191: Cmdline session params have no effect
// ZAP: 2015/04/02 Issue 321: Support multiple databases and Issue 1582: Low memory option
// ZAP: 2015/10/06 Issue 1962: Install and update add-ons from the command line
// ZAP: 2016/08/19 Issue 2782: Support -configfile
// ZAP: 2016/09/22 JavaDoc tweaks
// ZAP: 2016/11/07 Allow to disable default standard output logging
// ZAP: 2017/03/26 Allow to obtain configs in the order specified
package org.parosproxy.paros;
import java.io.File;
import java.io.FileInputStream;
import java.text.MessageFormat;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.parosproxy.paros.extension.CommandLineArgument;
import org.parosproxy.paros.extension.CommandLineListener;
import org.parosproxy.paros.network.HttpSender;
import org.zaproxy.zap.ZAP;
public class CommandLine {
private static final Logger logger = Logger.getLogger(CommandLine.class);
// ZAP: Made public
public static final String SESSION = "-session";
public static final String NEW_SESSION = "-newsession";
public static final String DAEMON = "-daemon";
public static final String HELP = "-help";
public static final String HELP2 = "-h";
public static final String DIR = "-dir";
public static final String VERSION = "-version";
public static final String PORT = "-port";
public static final String HOST = "-host";
public static final String CMD = "-cmd";
public static final String INSTALL_DIR = "-installdir";
public static final String CONFIG = "-config";
public static final String CONFIG_FILE = "-configfile";
public static final String LOWMEM = "-lowmem";
public static final String EXPERIMENTALDB = "-experimentaldb";
/**
* Command line option to disable the default logging through standard output.
*
* @see #isNoStdOutLog()
* @since 2.6.0
*/
public static final String NOSTDOUT = "-nostdout";
static final String NO_USER_AGENT = "-nouseragent";
static final String SP = "-sp";
private boolean GUI = true;
private boolean daemon = false;
private boolean reportVersion = false;
private boolean lowMem = false;
private boolean experimentalDb = false;
private int port = -1;
private String host = null;
private String[] args = null;
private final Map<String, String> configs = new LinkedHashMap<>();
private final Hashtable<String, String> keywords = new Hashtable<>();
private List<CommandLineArgument[]> commandList = null;
/**
* Flag that indicates whether or not the default logging through standard output should be disabled.
*/
private boolean noStdOutLog;
public CommandLine(String[] args) throws Exception {
this.args = args;
parseFirst(this.args);
}
private boolean checkPair(String[] args, String paramName, int i) throws Exception {
String key = args[i];
String value = null;
if (key == null) {
return false;
}
if (key.equalsIgnoreCase(paramName)) {
value = args[i + 1];
if (value == null) {
throw new Exception();
}
keywords.put(paramName, value);
args[i] = null;
args[i + 1] = null;
return true;
}
return false;
}
private boolean checkSwitch(String[] args, String paramName, int i) throws Exception {
String key = args[i];
if (key == null) {
return false;
}
if (key.equalsIgnoreCase(paramName)) {
keywords.put(paramName, "");
args[i] = null;
return true;
}
return false;
}
private void parseFirst(String[] args) throws Exception {
for (int i = 0; i < args.length; i++) {
if (parseSwitchs(args, i)) {
continue;
}
if (parseKeywords(args, i)) {
continue;
}
}
}
public void parse(List<CommandLineArgument[]> commandList, Map<String, CommandLineListener> extMap) throws Exception {
this.commandList = commandList;
CommandLineArgument lastArg = null;
boolean found = false;
int remainingValueCount = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] == null) {
continue;
}
found = false;
for (int j = 0; j < commandList.size() && !found; j++) {
CommandLineArgument[] extArg = commandList.get(j);
for (int k = 0; k < extArg.length && !found; k++) {
if (args[i].compareToIgnoreCase(extArg[k].getName()) == 0) {
// check if previous keyword satisfied its required no. of parameters
if (remainingValueCount > 0) {
throw new Exception("Missing parameters for keyword '" + lastArg.getName() + "'.");
}
// process this keyword
lastArg = extArg[k];
lastArg.setEnabled(true);
found = true;
args[i] = null;
remainingValueCount = lastArg.getNumOfArguments();
}
}
}
// check if current string is a keyword preceded by '-'
if (args[i] != null && args[i].startsWith("-")) {
continue;
}
// check if there is no more expected param value
if (lastArg != null && remainingValueCount == 0) {
continue;
}
// check if consume remaining for last matched keywords
if (!found && lastArg != null) {
if (lastArg.getPattern() == null || lastArg.getPattern().matcher(args[i]).find()) {
lastArg.getArguments().add(args[i]);
if (remainingValueCount > 0) {
remainingValueCount--;
}
args[i] = null;
} else {
throw new Exception(lastArg.getErrorMessage());
}
}
}
// check if the last keyword satified its no. of parameters.
if (lastArg != null && remainingValueCount > 0) {
throw new Exception("Missing parameters for keyword '" + lastArg.getName() + "'.");
}
// check for supported extensions
for (int i = 0; i < args.length; i++) {
if (args[i] == null) {
continue;
}
int dotIndex = args[i].lastIndexOf(".");
if (dotIndex < 0) {
// Only support files with extensions
continue;
}
File file = new File(args[i]);
if (!file.exists() || !file.canRead()) {
// Not there or cant read .. move on
continue;
}
String ext = args[i].substring(dotIndex + 1);
CommandLineListener cll = extMap.get(ext);
if (cll != null) {
if (cll.handleFile(file)) {
found = true;
args[i] = null;
}
}
}
// check if there is some unknown keywords or parameters
for (String arg : args) {
if (arg != null) {
if (arg.startsWith("-")) {
throw new Exception(
MessageFormat.format(Constant.messages.getString("start.cmdline.badparam"), arg));
} else {
// Assume they were trying to specify a file
File f = new File(arg);
if (!f.exists()) {
throw new Exception(
MessageFormat.format(Constant.messages.getString("start.cmdline.nofile"), arg));
} else if (!f.canRead()) {
throw new Exception(
MessageFormat.format(Constant.messages.getString("start.cmdline.noread"), arg));
} else {
// We probably dont handle this sort of file
throw new Exception(
MessageFormat.format(Constant.messages.getString("start.cmdline.badfile"), arg));
}
}
}
}
}
private boolean parseSwitchs(String[] args, int i) throws Exception {
boolean result = false;
if (checkSwitch(args, NO_USER_AGENT, i)) {
HttpSender.setUserAgent("");
Constant.setEyeCatcher("");
result = true;
} else if (checkSwitch(args, SP, i)) {
Constant.setSP(true);
result = true;
} else if (checkSwitch(args, CMD, i)) {
setDaemon(false);
setGUI(false);
} else if (checkSwitch(args, DAEMON, i)) {
setDaemon(true);
setGUI(false);
} else if (checkSwitch(args, LOWMEM, i)) {
setLowMem(true);
} else if (checkSwitch(args, EXPERIMENTALDB, i)) {
setExperimentalDb(true);
} else if (checkSwitch(args, HELP, i)) {
result = true;
setGUI(false);
} else if (checkSwitch(args, HELP2, i)) {
result = true;
setGUI(false);
} else if (checkSwitch(args, VERSION, i)) {
reportVersion = true;
setDaemon(false);
setGUI(false);
} else if (checkSwitch(args, NOSTDOUT, i)) {
noStdOutLog = true;
}
return result;
}
private boolean parseKeywords(String[] args, int i) throws Exception {
boolean result = false;
if (checkPair(args, NEW_SESSION, i)) {
result = true;
} else if (checkPair(args, SESSION, i)) {
result = true;
} else if (checkPair(args, DIR, i)) {
Constant.setZapHome(keywords.get(DIR));
result = true;
} else if (checkPair(args, INSTALL_DIR, i)) {
Constant.setZapInstall(keywords.get(INSTALL_DIR));
result = true;
} else if (checkPair(args, HOST, i)) {
this.host = keywords.get(HOST);
result = true;
} else if (checkPair(args, PORT, i)) {
this.port = Integer.parseInt(keywords.get(PORT));
result = true;
} else if (checkPair(args, CONFIG, i)) {
String pair = keywords.get(CONFIG);
if (pair != null && pair.indexOf("=") > 0) {
int eqIndex = pair.indexOf("=");
this.configs.put(pair.substring(0, eqIndex), pair.substring(eqIndex + 1));
result = true;
}
} else if (checkPair(args, CONFIG_FILE, i)) {
String conf = keywords.get(CONFIG_FILE);
File confFile = new File(conf);
if (! confFile.isFile()) {
// We cant use i18n here as the messages wont have been loaded
String error = "No such file: " + confFile.getAbsolutePath();
System.out.println(error);
throw new Exception(error);
} else if (! confFile.canRead()) {
// We cant use i18n here as the messages wont have been loaded
String error = "File not readable: " + confFile.getAbsolutePath();
System.out.println(error);
throw new Exception(error);
}
Properties prop = new Properties();
try (FileInputStream inStream = new FileInputStream(confFile)) {
prop.load(inStream);
}
for (Entry<Object, Object> keyValue : prop.entrySet()) {
this.configs.put((String)keyValue.getKey(), (String)keyValue.getValue());
}
}
return result;
}
/**
* Tells whether or not ZAP was started with GUI.
*
* @return {@code true} if ZAP was started with GUI, {@code false} otherwise
*/
public boolean isGUI() {
return GUI;
}
/**
* Sets whether or not ZAP was started with GUI.
*
* @param GUI {@code true} if ZAP was started with GUI, {@code false} otherwise
*/
public void setGUI(boolean GUI) {
this.GUI = GUI;
}
public boolean isDaemon() {
return daemon;
}
public void setDaemon(boolean daemon) {
this.daemon = daemon;
}
public boolean isLowMem() {
return lowMem;
}
public void setLowMem(boolean lowMem) {
this.lowMem = lowMem;
}
public boolean isExperimentalDb() {
return experimentalDb;
}
public void setExperimentalDb(boolean experimentalDb) {
this.experimentalDb = experimentalDb;
}
public boolean isReportVersion() {
return this.reportVersion;
}
public int getPort() {
return this.port;
}
public String getHost() {
return host;
}
/**
* Gets the {@code config} command line arguments, in no specific order.
*
* @return the {@code config} command line arguments.
* @deprecated (2.6.0) Use {@link #getOrderedConfigs()} instead, which are in the order they were specified.
*/
@Deprecated
public Hashtable<String, String> getConfigs() {
return new Hashtable<>(configs);
}
/**
* Gets the {@code config} command line arguments, in the order they were specified.
*
* @return the {@code config} command line arguments.
* @since 2.6.0
*/
public Map<String, String> getOrderedConfigs() {
return configs;
}
public String getArgument(String keyword) {
return keywords.get(keyword);
}
public String getHelp() {
return CommandLine.getHelp(commandList);
}
/**
* Tells whether or not the default logging through standard output should be disabled.
*
* @return {@code true} if the default logging through standard output should be disabled, {@code false} otherwise.
* @since 2.6.0
*/
public boolean isNoStdOutLog() {
return noStdOutLog;
}
public static String getHelp(List<CommandLineArgument[]> cmdList) {
String zap;
if (Constant.isWindows()) {
zap = "zap.bat";
} else {
zap = "zap.sh";
}
StringBuilder sb = new StringBuilder();
sb.append(MessageFormat.format(
Constant.messages.getString("cmdline.help"), zap));
if (cmdList != null) {
for (CommandLineArgument[] extArgs : cmdList) {
for (CommandLineArgument extArg : extArgs) {
sb.append("\t");
sb.append(extArg.getHelpMessage()).append("\n");
}
}
}
return sb.toString();
}
public boolean isEnabled(String keyword) {
Object obj = keywords.get(keyword);
return (obj != null) && (obj instanceof String);
}
/**
* A method for reporting informational messages in {@link CommandLineListener#execute(CommandLineArgument[])}
* implementations. It ensures that messages are written to the log file and/or written to stdout as appropriate.
* @param str the informational message
*/
public static void info(String str) {
switch (ZAP.getProcessType()) {
case cmdline: System.out.println(str); break;
default: // Ignore
}
// Always write to the log
logger.info(str);
}
/**
* A method for reporting error messages in {@link CommandLineListener#execute(CommandLineArgument[])} implementations.
* It ensures that messages are written to the log file and/or written to stderr as appropriate.
* @param str the error message
*/
public static void error(String str) {
switch (ZAP.getProcessType()) {
case cmdline: System.err.println(str); break;
default: // Ignore
}
// Always write to the log
logger.error(str);
}
/**
* A method for reporting error messages in {@link CommandLineListener#execute(CommandLineArgument[])} implementations.
* It ensures that messages are written to the log file and/or written to stderr as appropriate.
* @param str the error message
* @param e the cause of the error
*/
public static void error(String str, Throwable e) {
switch (ZAP.getProcessType()) {
case cmdline: System.err.println(str); break;
default: // Ignore
}
// Always write to the log
logger.error(str, e);
}
}