package de.blinkt.openvpn;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Locale;
import java.util.Vector;
//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
// And rember, this is valid :)
// --
// bar
//
public class ConfigParser {
private HashMap>> options = new HashMap>>();
public void parseConfig(InputStream inputStream) throws IOException, ConfigParseError {
InputStreamReader fr = new InputStreamReader(inputStream);
BufferedReader br =new BufferedReader(fr);
@SuppressWarnings("unused")
int lineno=0;
while (true){
String line = br.readLine();
if(line==null)
break;
lineno++;
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);
}
}
private void checkinlinefile(Vector args, BufferedReader br) throws IOException, ConfigParseError {
String arg0 = args.get(0);
// 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.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",
"connection",
"proto-force",
"remote-random",
"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",
"auth-user-pass-verify",
"dhcp-release",
"dhcp-renew",
"dh",
"management-hold",
"management",
"management-query-passwords",
"pause-exit",
"persist-key",
"register-dns",
"route-delay",
"route-gateway",
"route-metric",
"route-method",
"script-security",
"show-net-up",
"status",
"suppress-timestamps",
"tmp-dir",
"topology",
"win-sys"
};
// This method is far too long
VpnProfile convertProfile() throws ConfigParseError{
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 = "";
for(Vector route:routes){
String netmask = "255.255.255.255";
if(route.size() >= 3)
netmask = route.get(2);
String net = route.get(1);
try {
CIDRIP cidr = new CIDRIP(net, netmask);
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;
}
// 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);
if(getAllOption("redirect-gateway", 0, 5) != null)
np.mUseDefaultRoute=true;
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 mode =getOption("mode",1,1);
if (mode != null){
if(!mode.get(1).equals("p2p"))
throw new ConfigParseError("Invalid mode for --mode specified, need p2p");
}
// Parse remote config
Vector remote = getOption("remote",1,3);
if(remote != null){
switch (remote.size()) {
case 4:
String proto = remote.get(3);
if(proto.equals("udp"))
np.mUseUdp=true;
else if (proto.equals("tcp"))
np.mUseUdp=false;
else
throw new ConfigParseError("remote protocol must be tcp or udp");
case 3:
np.mServerPort = remote.get(2);
case 2:
np.mServerName = remote.get(1);
}
}
Vector port = getOption("port", 1,1);
if(port!=null){
np.mServerPort = port.get(1);
}
Vector proto = getOption("proto", 1,1);
if(proto!=null){
if(proto.get(1).equals("udp") || proto.get(1).equals("udp6"))
np.mUseUdp=true;
else if (proto.get(1).equals("tcp-client") ||
proto.get(1).equals("tcp") ||
proto.get(1).equals("tcp6") ||
proto.get(1).endsWith("tcp6-client"))
np.mUseUdp=false;
else
throw new ConfigParseError("Unsupported option to --proto " + proto.get(1));
}
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) {
CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2));
np.mIPv4Address=cidr.toString();
}
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 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 tlsremote = getOption("tls-remote",1,1);
if(tlsremote!=null){
np.mRemoteCN = tlsremote.get(1);
np.mCheckRemoteCN=true;
}
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 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) {
np.mUsername=authuser.get(1);
}
}
// Check the other options
checkIgnoreAndInvalidOptions(np);
fixup(np);
return np;
}
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) {
String custom = "# These Options were found in the config file do not map to config settings:\n";
for(Vector> option:options.values()) {
for(Vector optionsline: option) {
for (String arg : optionsline)
custom+= VpnProfile.openVpnEscape(arg) + " ";
}
custom+="\n";
}
np.mCustomConfigOptions = custom;
np.mUseCustomConfig=true;
}
}
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;
}
}