/*
* Copyright (c) 2012-2014 Arne Schwabe
* Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
*/
package de.blinkt.openvpn.core;
import android.text.TextUtils;
import android.util.Pair;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
// And remember, this is valid :)
// --
// bar
//
public class ConfigParser {
public static final String CONVERTED_PROFILE = "converted Profile";
private HashMap>> options = new HashMap>>();
private HashMap> meta = new HashMap>();
public void parseConfig(Reader reader) throws IOException, ConfigParseError {
BufferedReader br = new BufferedReader(reader);
int lineno = 0;
try {
while (true) {
String line = br.readLine();
lineno++;
if (line == null)
break;
if (lineno == 1 && (line.startsWith("PK\003\004")
|| (line.startsWith("PK\007\008"))))
throw new ConfigParseError("Input looks like a ZIP Archive. Import is only possible for OpenVPN config files (.ovpn/.conf)");
// Check for OpenVPN Access Server Meta information
if (line.startsWith("# OVPN_ACCESS_SERVER_")) {
Vector metaarg = parsemeta(line);
meta.put(metaarg.get(0), metaarg);
continue;
}
Vector args = parseline(line);
if (args.size() == 0)
continue;
if (args.get(0).startsWith("--"))
args.set(0, args.get(0).substring(2));
checkinlinefile(args, br);
String optionname = args.get(0);
if (!options.containsKey(optionname)) {
options.put(optionname, new Vector>());
}
options.get(optionname).add(args);
}
} catch (java.lang.OutOfMemoryError memoryError) {
throw new ConfigParseError("File too large to parse: " + memoryError.getLocalizedMessage());
}
}
private Vector parsemeta(String line) {
String meta = line.split("#\\sOVPN_ACCESS_SERVER_", 2)[1];
String[] parts = meta.split("=", 2);
Vector rval = new Vector();
Collections.addAll(rval, parts);
return rval;
}
private void checkinlinefile(Vector args, BufferedReader br) throws IOException, ConfigParseError {
String arg0 = args.get(0).trim();
// CHeck for
if (arg0.startsWith("<") && arg0.endsWith(">")) {
String argname = arg0.substring(1, arg0.length() - 1);
String inlinefile = VpnProfile.INLINE_TAG;
String endtag = String.format("%s>", argname);
do {
String line = br.readLine();
if (line == null) {
throw new ConfigParseError(String.format("No endtag %s> for starttag <%s> found", argname, argname));
}
if (line.trim().equals(endtag))
break;
else {
inlinefile += line;
inlinefile += "\n";
}
} while (true);
args.clear();
args.add(argname);
args.add(inlinefile);
}
}
enum linestate {
initial,
readin_single_quote, reading_quoted, reading_unquoted, done
}
private boolean space(char c) {
// I really hope nobody is using zero bytes inside his/her config file
// to sperate parameter but here we go:
return Character.isWhitespace(c) || c == '\0';
}
public class ConfigParseError extends Exception {
private static final long serialVersionUID = -60L;
public ConfigParseError(String msg) {
super(msg);
}
}
// adapted openvpn's parse function to java
private Vector parseline(String line) throws ConfigParseError {
Vector parameters = new Vector();
if (line.length() == 0)
return parameters;
linestate state = linestate.initial;
boolean backslash = false;
char out = 0;
int pos = 0;
String currentarg = "";
do {
// Emulate the c parsing ...
char in;
if (pos < line.length())
in = line.charAt(pos);
else
in = '\0';
if (!backslash && in == '\\' && state != linestate.readin_single_quote) {
backslash = true;
} else {
if (state == linestate.initial) {
if (!space(in)) {
if (in == ';' || in == '#') /* comment */
break;
if (!backslash && in == '\"')
state = linestate.reading_quoted;
else if (!backslash && in == '\'')
state = linestate.readin_single_quote;
else {
out = in;
state = linestate.reading_unquoted;
}
}
} else if (state == linestate.reading_unquoted) {
if (!backslash && space(in))
state = linestate.done;
else
out = in;
} else if (state == linestate.reading_quoted) {
if (!backslash && in == '\"')
state = linestate.done;
else
out = in;
} else if (state == linestate.readin_single_quote) {
if (in == '\'')
state = linestate.done;
else
out = in;
}
if (state == linestate.done) {
/* ASSERT (parm_len > 0); */
state = linestate.initial;
parameters.add(currentarg);
currentarg = "";
out = 0;
}
if (backslash && out != 0) {
if (!(out == '\\' || out == '\"' || space(out))) {
throw new ConfigParseError("Options warning: Bad backslash ('\\') usage");
}
}
backslash = false;
}
/* store parameter character */
if (out != 0) {
currentarg += out;
}
} while (pos++ < line.length());
return parameters;
}
final String[] unsupportedOptions = {"config",
"tls-server"
};
// Ignore all scripts
// in most cases these won't work and user who wish to execute scripts will
// figure out themselves
final String[] ignoreOptions = {"tls-client",
"askpass",
"auth-nocache",
"up",
"down",
"route-up",
"ipchange",
"route-up",
"route-pre-down",
"auth-user-pass-verify",
"dhcp-release",
"dhcp-renew",
"dh",
"group",
"ip-win32",
"management-hold",
"management",
"management-client",
"management-query-remote",
"management-query-passwords",
"management-query-proxy",
"management-external-key",
"management-forget-disconnect",
"management-signal",
"management-log-cache",
"management-up-down",
"management-client-user",
"management-client-group",
"pause-exit",
"plugin",
"machine-readable-output",
"persist-key",
"register-dns",
"route-delay",
"route-gateway",
"route-metric",
"route-method",
"status",
"script-security",
"show-net-up",
"suppress-timestamps",
"tmp-dir",
"tun-ipv6",
"topology",
"user",
"win-sys",
};
final String[][] ignoreOptionsWithArg =
{
{"setenv", "IV_GUI_VER"},
{"setenv", "IV_OPENVPN_GUI_VERSION"},
{"engine", "dynamic"}
};
final String[] connectionOptions = {
"local",
"remote",
"float",
"port",
"connect-retry",
"connect-timeout",
"connect-retry-max",
"link-mtu",
"tun-mtu",
"tun-mtu-extra",
"fragment",
"mtu-disc",
"local-port",
"remote-port",
"bind",
"nobind",
"proto",
"http-proxy",
"http-proxy-retry",
"http-proxy-timeout",
"http-proxy-option",
"socks-proxy",
"socks-proxy-retry",
"explicit-exit-notify",
"mssfix"
};
// This method is far too long
@SuppressWarnings("ConstantConditions")
public VpnProfile convertProfile() throws ConfigParseError, IOException {
boolean noauthtypeset = true;
VpnProfile np = new VpnProfile(CONVERTED_PROFILE);
// Pull, client, tls-client
np.clearDefaults();
if (options.containsKey("client") || options.containsKey("pull")) {
np.mUsePull = true;
options.remove("pull");
options.remove("client");
}
Vector secret = getOption("secret", 1, 2);
if (secret != null) {
np.mAuthenticationType = VpnProfile.TYPE_STATICKEYS;
noauthtypeset = false;
np.mUseTLSAuth = true;
np.mTLSAuthFilename = secret.get(1);
if (secret.size() == 3)
np.mTLSAuthDirection = secret.get(2);
}
Vector> routes = getAllOption("route", 1, 4);
if (routes != null) {
String routeopt = "";
String routeExcluded = "";
for (Vector route : routes) {
String netmask = "255.255.255.255";
String gateway = "vpn_gateway";
if (route.size() >= 3)
netmask = route.get(2);
if (route.size() >= 4)
gateway = route.get(3);
String net = route.get(1);
try {
CIDRIP cidr = new CIDRIP(net, netmask);
if (gateway.equals("net_gateway"))
routeExcluded += cidr.toString() + " ";
else
routeopt += cidr.toString() + " ";
} catch (ArrayIndexOutOfBoundsException aioob) {
throw new ConfigParseError("Could not parse netmask of route " + netmask);
} catch (NumberFormatException ne) {
throw new ConfigParseError("Could not parse netmask of route " + netmask);
}
}
np.mCustomRoutes = routeopt;
np.mExcludedRoutes = routeExcluded;
}
Vector> routesV6 = getAllOption("route-ipv6", 1, 4);
if (routesV6 != null) {
String customIPv6Routes = "";
for (Vector route : routesV6) {
customIPv6Routes += route.get(1) + " ";
}
np.mCustomRoutesv6 = customIPv6Routes;
}
// Also recognize tls-auth [inline] direction ...
Vector> tlsauthoptions = getAllOption("tls-auth", 1, 2);
if (tlsauthoptions != null) {
for (Vector tlsauth : tlsauthoptions) {
if (tlsauth != null) {
if (!tlsauth.get(1).equals("[inline]")) {
np.mTLSAuthFilename = tlsauth.get(1);
np.mUseTLSAuth = true;
}
if (tlsauth.size() == 3)
np.mTLSAuthDirection = tlsauth.get(2);
}
}
}
Vector direction = getOption("key-direction", 1, 1);
if (direction != null)
np.mTLSAuthDirection = direction.get(1);
Vector> defgw = getAllOption("redirect-gateway", 0, 5);
if (defgw != null) {
np.mUseDefaultRoute = true;
checkRedirectParameters(np, defgw);
}
Vector> redirectPrivate = getAllOption("redirect-private", 0, 5);
if (redirectPrivate != null) {
checkRedirectParameters(np, redirectPrivate);
}
Vector dev = getOption("dev", 1, 1);
Vector devtype = getOption("dev-type", 1, 1);
if ((devtype != null && devtype.get(1).equals("tun")) ||
(dev != null && dev.get(1).startsWith("tun")) ||
(devtype == null && dev == null)) {
//everything okay
} else {
throw new ConfigParseError("Sorry. Only tun mode is supported. See the FAQ for more detail");
}
Vector mssfix = getOption("mssfix", 0, 1);
if (mssfix != null) {
if (mssfix.size() >= 2) {
try {
np.mMssFix = Integer.parseInt(mssfix.get(1));
} catch (NumberFormatException e) {
throw new ConfigParseError("Argument to --mssfix has to be an integer");
}
} else {
np.mMssFix = VpnProfile.DEFAULT_MSSFIX_SIZE;
}
}
Vector mode = getOption("mode", 1, 1);
if (mode != null) {
if (!mode.get(1).equals("p2p"))
throw new ConfigParseError("Invalid mode for --mode specified, need p2p");
}
Vector> dhcpoptions = getAllOption("dhcp-option", 2, 2);
if (dhcpoptions != null) {
for (Vector dhcpoption : dhcpoptions) {
String type = dhcpoption.get(1);
String arg = dhcpoption.get(2);
if (type.equals("DOMAIN")) {
np.mSearchDomain = dhcpoption.get(2);
} else if (type.equals("DNS")) {
np.mOverrideDNS = true;
if (np.mDNS1.equals(VpnProfile.DEFAULT_DNS1))
np.mDNS1 = arg;
else
np.mDNS2 = arg;
}
}
}
Vector ifconfig = getOption("ifconfig", 2, 2);
if (ifconfig != null) {
try {
CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2));
np.mIPv4Address = cidr.toString();
} catch (NumberFormatException nfe) {
throw new ConfigParseError("Could not pase ifconfig IP address: " + nfe.getLocalizedMessage());
}
}
if (getOption("remote-random-hostname", 0, 0) != null)
np.mUseRandomHostname = true;
if (getOption("float", 0, 0) != null)
np.mUseFloat = true;
if (getOption("comp-lzo", 0, 1) != null)
np.mUseLzo = true;
Vector cipher = getOption("cipher", 1, 1);
if (cipher != null)
np.mCipher = cipher.get(1);
Vector auth = getOption("auth", 1, 1);
if (auth != null)
np.mAuth = auth.get(1);
Vector ca = getOption("ca", 1, 1);
if (ca != null) {
np.mCaFilename = ca.get(1);
}
Vector cert = getOption("cert", 1, 1);
if (cert != null) {
np.mClientCertFilename = cert.get(1);
np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES;
noauthtypeset = false;
}
Vector key = getOption("key", 1, 1);
if (key != null)
np.mClientKeyFilename = key.get(1);
Vector pkcs12 = getOption("pkcs12", 1, 1);
if (pkcs12 != null) {
np.mPKCS12Filename = pkcs12.get(1);
np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE;
noauthtypeset = false;
}
Vector cryptoapicert = getOption("cryptoapicert", 1, 1);
if (cryptoapicert != null) {
np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE;
noauthtypeset = false;
}
Vector compatnames = getOption("compat-names", 1, 2);
Vector nonameremapping = getOption("no-name-remapping", 1, 1);
Vector tlsremote = getOption("tls-remote", 1, 1);
if (tlsremote != null) {
np.mRemoteCN = tlsremote.get(1);
np.mCheckRemoteCN = true;
np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE;
if ((compatnames != null && compatnames.size() > 2) ||
(nonameremapping != null))
np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING;
}
Vector verifyx509name = getOption("verify-x509-name", 1, 2);
if (verifyx509name != null) {
np.mRemoteCN = verifyx509name.get(1);
np.mCheckRemoteCN = true;
if (verifyx509name.size() > 2) {
if (verifyx509name.get(2).equals("name"))
np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_RDN;
else if (verifyx509name.get(2).equals("name-prefix"))
np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX;
else
throw new ConfigParseError("Unknown parameter to x509-verify-name: " + verifyx509name.get(2));
} else {
np.mX509AuthType = VpnProfile.X509_VERIFY_TLSREMOTE_DN;
}
}
Vector verb = getOption("verb", 1, 1);
if (verb != null) {
np.mVerb = verb.get(1);
}
if (getOption("nobind", 0, 0) != null)
np.mNobind = true;
if (getOption("persist-tun", 0, 0) != null)
np.mPersistTun = true;
Vector connectretry = getOption("connect-retry", 1, 1);
if (connectretry != null)
np.mConnectRetry = connectretry.get(1);
Vector connectretrymax = getOption("connect-retry-max", 1, 1);
if (connectretrymax != null)
np.mConnectRetryMax = connectretrymax.get(1);
Vector> remotetls = getAllOption("remote-cert-tls", 1, 1);
if (remotetls != null)
if (remotetls.get(0).get(1).equals("server"))
np.mExpectTLSCert = true;
else
options.put("remotetls", remotetls);
Vector authuser = getOption("auth-user-pass", 0, 1);
if (authuser != null) {
if (noauthtypeset) {
np.mAuthenticationType = VpnProfile.TYPE_USERPASS;
} else if (np.mAuthenticationType == VpnProfile.TYPE_CERTIFICATES) {
np.mAuthenticationType = VpnProfile.TYPE_USERPASS_CERTIFICATES;
} else if (np.mAuthenticationType == VpnProfile.TYPE_KEYSTORE) {
np.mAuthenticationType = VpnProfile.TYPE_USERPASS_KEYSTORE;
}
if (authuser.size() > 1) {
// Set option value to password get to embed later.
np.mUsername = null;
useEmbbedUserAuth(np, authuser.get(1));
}
}
Pair conns = parseConnectionOptions(null);
np.mConnections = conns.second;
Vector> connectionBlocks = getAllOption("connection", 1, 1);
if (np.mConnections.length > 0 && connectionBlocks != null) {
throw new ConfigParseError("Using a block and --remote is not allowed.");
}
if (connectionBlocks != null) {
np.mConnections = new Connection[connectionBlocks.size()];
int connIndex = 0;
for (Vector conn : connectionBlocks) {
Pair connectionBlockConnection =
parseConnection(conn.get(1), conns.first);
if (connectionBlockConnection.second.length != 1)
throw new ConfigParseError("A block must have exactly one remote");
np.mConnections[connIndex] = connectionBlockConnection.second[0];
connIndex++;
}
}
if (getOption("remote-random", 0, 0) != null)
np.mRemoteRandom = true;
Vector protoforce = getOption("proto-force", 1, 1);
if (protoforce != null) {
boolean disableUDP;
String protoToDisable = protoforce.get(1);
if (protoToDisable.equals("udp"))
disableUDP = true;
else if (protoToDisable.equals("tcp"))
disableUDP = false;
else
throw new ConfigParseError(String.format("Unknown protocol %s in proto-force", protoToDisable));
for (Connection conn : np.mConnections)
if (conn.mUseUdp == disableUDP)
conn.mEnabled = false;
}
// Parse OpenVPN Access Server extra
Vector friendlyname = meta.get("FRIENDLY_NAME");
if (friendlyname != null && friendlyname.size() > 1)
np.mName = friendlyname.get(1);
Vector ocusername = meta.get("USERNAME");
if (ocusername != null && ocusername.size() > 1)
np.mUsername = ocusername.get(1);
checkIgnoreAndInvalidOptions(np);
fixup(np);
return np;
}
private Pair parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError {
// Parse a connection Block as a new configuration file
ConfigParser connectionParser = new ConfigParser();
StringReader reader = new StringReader(connection.substring(VpnProfile.INLINE_TAG.length()));
connectionParser.parseConfig(reader);
Pair conn = connectionParser.parseConnectionOptions(defaultValues);
return conn;
}
private Pair parseConnectionOptions(Connection connDefault) throws ConfigParseError {
Connection conn;
if (connDefault != null)
try {
conn = connDefault.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
else
conn = new Connection();
Vector port = getOption("port", 1, 1);
if (port != null) {
conn.mServerPort = port.get(1);
}
Vector rport = getOption("rport", 1, 1);
if (rport != null) {
conn.mServerPort = rport.get(1);
}
Vector proto = getOption("proto", 1, 1);
if (proto != null) {
conn.mUseUdp = isUdpProto(proto.get(1));
}
// Parse remote config
Vector> remotes = getAllOption("remote", 1, 3);
// Assume that we need custom options if connectionDefault are set
if (connDefault != null) {
for (Vector> option : options.values()) {
conn.mCustomConfiguration += getOptionStrings(option);
}
if (!TextUtils.isEmpty(conn.mCustomConfiguration))
conn.mUseCustomConfig = true;
}
// Make remotes empty to simplify code
if (remotes == null)
remotes = new Vector>();
Connection[] connections = new Connection[remotes.size()];
int i = 0;
for (Vector remote : remotes) {
try {
connections[i] = conn.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
switch (remote.size()) {
case 4:
connections[i].mUseUdp = isUdpProto(remote.get(3));
case 3:
connections[i].mServerPort = remote.get(2);
case 2:
connections[i].mServerName = remote.get(1);
}
i++;
}
return Pair.create(conn, connections);
}
private void checkRedirectParameters(VpnProfile np, Vector> defgw) {
for (Vector redirect : defgw)
for (int i = 1; i < redirect.size(); i++) {
if (redirect.get(i).equals("block-local"))
np.mAllowLocalLAN = false;
else if (redirect.get(i).equals("unblock-local"))
np.mAllowLocalLAN = true;
}
}
private boolean isUdpProto(String proto) throws ConfigParseError {
boolean isudp;
if (proto.equals("udp") || proto.equals("udp6"))
isudp = true;
else if (proto.equals("tcp-client") ||
proto.equals("tcp") ||
proto.equals("tcp6") ||
proto.endsWith("tcp6-client"))
isudp = false;
else
throw new ConfigParseError("Unsupported option to --proto " + proto);
return isudp;
}
static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) {
String data = VpnProfile.getEmbeddedContent(inlinedata);
String[] parts = data.split("\n");
if (parts.length >= 2) {
np.mUsername = parts[0];
np.mPassword = parts[1];
}
}
private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError {
for (String option : unsupportedOptions)
if (options.containsKey(option))
throw new ConfigParseError(String.format("Unsupported Option %s encountered in config file. Aborting", option));
for (String option : ignoreOptions)
// removing an item which is not in the map is no error
options.remove(option);
if (options.size() > 0) {
np.mCustomConfigOptions += "# These Options were found in the config file do not map to config settings:\n";
for (Vector> option : options.values()) {
np.mCustomConfigOptions += getOptionStrings(option);
}
np.mUseCustomConfig = true;
}
}
boolean ignoreThisOption(Vector option) {
for (String[] ignoreOption : ignoreOptionsWithArg) {
if (option.size() < ignoreOption.length)
continue;
boolean ignore = true;
for (int i = 0; i < ignoreOption.length; i++) {
if (!ignoreOption[i].equals(option.get(i)))
ignore = false;
}
if (ignore)
return true;
}
return false;
}
private String getOptionStrings(Vector> option) {
String custom = "";
for (Vector optionsline : option) {
if (!ignoreThisOption(optionsline)) {
for (String arg : optionsline)
custom += VpnProfile.openVpnEscape(arg) + " ";
custom += "\n";
}
}
return custom;
}
private void fixup(VpnProfile np) {
if (np.mRemoteCN.equals(np.mServerName)) {
np.mRemoteCN = "";
}
}
private Vector getOption(String option, int minarg, int maxarg) throws ConfigParseError {
Vector> alloptions = getAllOption(option, minarg, maxarg);
if (alloptions == null)
return null;
else
return alloptions.lastElement();
}
private Vector> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError {
Vector> args = options.get(option);
if (args == null)
return null;
for (Vector optionline : args)
if (optionline.size() < (minarg + 1) || optionline.size() > maxarg + 1) {
String err = String.format(Locale.getDefault(), "Option %s has %d parameters, expected between %d and %d",
option, optionline.size() - 1, minarg, maxarg);
throw new ConfigParseError(err);
}
options.remove(option);
return args;
}
}