package de.blinkt.openvpn.core; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; 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>(); private boolean extraRemotesAsCustom=false; public void parseConfig(Reader reader) throws IOException, ConfigParseError { BufferedReader br =new BufferedReader(reader); int lineno=0; 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); } } 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("",argname); do { String line = br.readLine(); if(line==null){ throw new ConfigParseError(String.format("No endtag 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", "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", "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"} }; 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{ 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 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 port = getOption("port", 1,1); if(port!=null){ np.mServerPort = port.get(1); } Vector rport = getOption("rport", 1,1); if(rport!=null){ np.mServerPort = rport.get(1); } Vector proto = getOption("proto", 1,1); if(proto!=null){ np.mUseUdp=isUdpProto(proto.get(1)); } // Parse remote config Vector> remotes = getAllOption("remote",1,3); if(remotes!=null && remotes.size()>=1 ) { Vector remote = remotes.get(0); switch (remote.size()) { case 4: np.mUseUdp=isUdpProto(remote.get(3)); case 3: np.mServerPort = remote.get(2); case 2: np.mServerName = remote.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) { 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 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 get cance to embed later. np.mUsername=null; np.mPassword=authuser.get(1); useEmbbedUserAuth(np,authuser.get(1)); } } // 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); // Check the other options if(remotes !=null && remotes.size()>1 && extraRemotesAsCustom) { // first is already added remotes.remove(0); np.mCustomConfigOptions += getOptionStrings(remotes); np.mUseCustomConfig=true; } checkIgnoreAndInvalidOptions(np); fixup(np); return np; } private void checkRedirectParameters(VpnProfile np, Vector> defgw) { for (Vector redirect: defgw) for (int i=1;i= 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; } }