/*
 * network-manager-openvpn - OpenVPN integration with NetworkManager
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright (C) 2008 - 2013 Dan Williams <dcbw@redhat.com>
 * Copyright (C) 2008 - 2018 Red Hat, Inc.
 */

#include "nm-default.h"

#include "import-export.h"

#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <stdio.h>

#include "utils.h"
#include "nm-utils/nm-shared-utils.h"


#define INLINE_BLOB_CA                  NMV_OVPN_TAG_CA
#define INLINE_BLOB_CERT                NMV_OVPN_TAG_CERT
#define INLINE_BLOB_EXTRA_CERTS         NMV_OVPN_TAG_EXTRA_CERTS
#define INLINE_BLOB_CRL_VERIFY          NMV_OVPN_TAG_CRL_VERIFY
#define INLINE_BLOB_KEY                 NMV_OVPN_TAG_KEY
#define INLINE_BLOB_PKCS12              NMV_OVPN_TAG_PKCS12
#define INLINE_BLOB_SECRET              NMV_OVPN_TAG_SECRET
#define INLINE_BLOB_TLS_AUTH            NMV_OVPN_TAG_TLS_AUTH
#define INLINE_BLOB_TLS_CRYPT           NMV_OVPN_TAG_TLS_CRYPT
#define INLINE_BLOB_TLS_CRYPT_V2        NMV_OVPN_TAG_TLS_CRYPT_V2

const char *_nmovpn_test_temp_path = NULL;

/*****************************************************************************/

static void
_auto_free_gstring_p (GString **ptr)
{
	if (*ptr)
		g_string_free (*ptr, TRUE);
}

static gboolean
_is_utf8 (const char *str)
{
	g_return_val_if_fail (str, FALSE);

	return g_utf8_validate (str, -1, NULL);
}

/*****************************************************************************/

static void
__attribute__((__format__ (__printf__, 3, 4)))
setting_vpn_add_data_item_v (NMSettingVpn *setting,
                             const char *key,
                             const char *format,
                             ...)
{
	char buf[256];
	char *s;
	int l;
	va_list ap, ap2;

	g_return_if_fail (NM_IS_SETTING_VPN (setting));
	g_return_if_fail (key && key[0]);

	/* let's first try with a stack allocated buffer,
	 * it's large enough for most cases. */
	va_start (ap, format);
	va_copy (ap2, ap);
	l = g_vsnprintf (buf, sizeof (buf), format, ap2);
	va_end (ap2);

	if (l < sizeof (buf) - 1) {
		va_end (ap);
		nm_setting_vpn_add_data_item (setting, key, buf);
		return;
	}

	s = g_strdup_vprintf (format, ap);
	va_end (ap);
	nm_setting_vpn_add_data_item (setting, key, s);
	g_free (s);
}

static void
setting_vpn_add_data_item_int64 (NMSettingVpn *setting,
                                 const char *key,
                                 gint64 value)
{
	setting_vpn_add_data_item_v (setting, key, "%"G_GINT64_FORMAT, value);
}

static void
setting_vpn_add_data_item (NMSettingVpn *setting,
                           const char *key,
                           const char *value)
{
	g_return_if_fail (NM_IS_SETTING_VPN (setting));
	g_return_if_fail (key && key[0]);
	g_return_if_fail (value && value[0]);
	g_return_if_fail (_is_utf8 (value));

	nm_setting_vpn_add_data_item (setting, key, value);
}

static void
setting_vpn_add_data_item_utf8safe (NMSettingVpn *setting,
                                    const char *key,
                                    const char *value)
{
	gs_free char *s = NULL;

	g_return_if_fail (NM_IS_SETTING_VPN (setting));
	g_return_if_fail (key && key[0]);
	g_return_if_fail (value && value[0]);

	nm_setting_vpn_add_data_item (setting, key,
	                              nm_utils_str_utf8safe_escape (value, 0, &s));
}

static void
setting_vpn_add_data_item_path (NMSettingVpn *setting,
                                const char *key,
                                const char *value)
{
	setting_vpn_add_data_item_utf8safe (setting, key, value);
}

static gboolean
setting_vpn_eq_data_item_utf8safe (NMSettingVpn *setting,
                                   const char *key,
                                   const char *expected_value)
{
	gs_free char *s_free = NULL;
	const char *s;

	s = nm_setting_vpn_get_data_item (setting, key);
	if (!s)
		return expected_value == NULL;

	if (!expected_value)
		return FALSE;
	return nm_streq (expected_value, nm_utils_str_utf8safe_unescape (s, &s_free));
}

/*****************************************************************************/

static gboolean
args_params_check_nargs_minmax (const char **params, guint nargs_min, guint nargs_max, char **out_error)
{
	guint nargs;

	g_return_val_if_fail (params, FALSE);
	g_return_val_if_fail (params[0], FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	nargs = g_strv_length ((char **) params) - 1;

	if (nargs < nargs_min || nargs > nargs_max) {
		if (nargs_min != nargs_max) {
			*out_error = g_strdup_printf (ngettext ("option %s expects between %u and %u argument",
			                                        "option %s expects between %u and %u arguments",
			                                        nargs_max),
			                              params[0], nargs_min, nargs_max);
		} else if (nargs_min == 0)
			*out_error = g_strdup_printf (_("option %s expects no arguments"), params[0]);
		else {
			*out_error = g_strdup_printf (ngettext ("option %s expects exactly one argument",
			                                        "option %s expects exactly %u arguments",
			                                         nargs_min),
			                              params[0], nargs_min);
		}
		return FALSE;
	}
	return TRUE;
}

static gboolean
args_params_check_nargs_n (const char **params, guint nargs, char **out_error)
{
	return args_params_check_nargs_minmax (params, nargs, nargs, out_error);
}

static gboolean
args_params_check_arg_nonempty (const char **params,
                                guint n_param,
                                const char *argument_name,
                                char **out_error)
{
	g_return_val_if_fail (params, FALSE);
	g_return_val_if_fail (params[0], FALSE);
	g_return_val_if_fail (n_param > 0 && n_param < g_strv_length ((char **) params), FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	if (params[n_param][0] == '\0') {
		if (argument_name)
			*out_error = g_strdup_printf (_("argument %s of “%s” can not be empty"), argument_name, params[0]);
		else
			*out_error = g_strdup_printf (_("argument of “%s” can not be empty"), params[0]);
		return FALSE;
	}
	return TRUE;
}

static gboolean
args_params_check_arg_utf8 (const char **params,
                            guint n_param,
                            const char *argument_name,
                            char **out_error)
{
	if (!args_params_check_arg_nonempty (params, n_param, argument_name, out_error))
		return FALSE;
	if (!_is_utf8 (params[n_param])) {
		if (argument_name)
			*out_error = g_strdup_printf (_("argument %s of “%s” must be UTF-8 encoded"), argument_name, params[0]);
		else
			*out_error = g_strdup_printf (_("argument of “%s” must be UTF-8 encoded"), params[0]);
		return FALSE;
	}
	return TRUE;
}

static gboolean
args_params_parse_int64 (const char **params,
                         guint n_param,
                         gint64 min,
                         gint64 max,
                         gint64 *out,
                         char **out_error)
{
	gint64 v;

	g_return_val_if_fail (params, FALSE);
	g_return_val_if_fail (params[0], FALSE);
	g_return_val_if_fail (n_param > 0, FALSE);
	g_return_val_if_fail (n_param < g_strv_length ((char **) params), FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	v = _nm_utils_ascii_str_to_int64 (params[n_param], 10, min, max, -1);
	if (errno) {
		*out_error = g_strdup_printf (_("invalid %uth argument to “%s” where number expected"),
		                              n_param,
		                              params[0]);
		return FALSE;
	}
	*out = v;
	return TRUE;
}

static gboolean
args_params_parse_port (const char **params, guint n_param, gint64 *out, char **out_error)
{
	return args_params_parse_int64 (params, n_param, 1, 65535, out, out_error);
}

static gboolean
args_params_parse_ip4 (const char **params,
                       guint n_param,
                       gboolean ovpn_extended_format,
                       in_addr_t *out,
                       char **out_error)
{
	in_addr_t a;
	const char *p;

	g_return_val_if_fail (params, FALSE);
	g_return_val_if_fail (params[0], FALSE);
	g_return_val_if_fail (n_param > 0, FALSE);
	g_return_val_if_fail (n_param < g_strv_length ((char **) params), FALSE);
	g_return_val_if_fail (out, FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	if (inet_pton (AF_INET, params[n_param], &a) == 1) {
		*out = a;
		return TRUE;
	}

	if (   ovpn_extended_format
	    && NM_IN_STRSET (params[n_param], "vpn_gateway", "net_gateway", "remote_host")) {
		/* we don't support these special destinations, as they currently cannot be expressed
		 * in a connection. */
		*out_error = g_strdup_printf (_("unsupported %uth argument %s to “%s”"),
		                              n_param,
		                              params[n_param],
		                              params[0]);
		return FALSE;
	}

	if (   ovpn_extended_format
	    && params[n_param]
	    && strlen (params[n_param]) <= 255) {
		for (p = params[n_param]; *p; p++) {
			if (NM_IN_SET (*p, '-', '.'))
				continue;
			if (g_ascii_isalnum (*p))
				continue;
			goto not_dns;
		}
		/* we also don't support specifing a FQDN. */
		*out_error = g_strdup_printf (_("unsupported %uth argument to “%s” which looks like a FQDN but only IPv4 address supported"),
		                              n_param,
		                              params[0]);
		return FALSE;
	}

not_dns:
	*out_error = g_strdup_printf (_("invalid %uth argument to “%s” where IPv4 address expected"),
	                              n_param,
	                              params[0]);
	return FALSE;
}

static gboolean
args_params_parse_key_direction (const char **params,
                                 guint n_param,
                                 const char **out_key_direction,
                                 char **out_error)
{
	g_return_val_if_fail (params, FALSE);
	g_return_val_if_fail (params[0], FALSE);
	g_return_val_if_fail (n_param > 0, FALSE);
	g_return_val_if_fail (n_param < g_strv_length ((char **) params), FALSE);
	g_return_val_if_fail (out_key_direction, FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	/* params will be freed in the next loop iteration. "internalize" the values. */
	if (nm_streq (params[n_param], "0"))
		*out_key_direction = "0";
	else if (nm_streq (params[n_param], "1"))
		*out_key_direction = "1";
	else {
		*out_error = g_strdup_printf (_("invalid %uth key-direction argument to “%s”"), n_param, params[0]);
		return FALSE;
	}
	return TRUE;
}

static char *
args_params_error_message_invalid_arg (const char **params, guint n_param)
{
	g_return_val_if_fail (params, NULL);
	g_return_val_if_fail (params[0], NULL);
	g_return_val_if_fail (n_param > 0, FALSE);
	g_return_val_if_fail (n_param < g_strv_length ((char **) params), FALSE);

	return g_strdup_printf (_("invalid %uth argument to “%s”"), n_param, params[0]);
}

/*****************************************************************************/

static char
_ch_step_1 (const char **str, gsize *len)
{
	char ch;
	g_assert (str);
	g_assert (len && *len > 0);

	ch = (*str)[0];

	(*str)++;
	(*len)--;
	return ch;
}

static void
_ch_skip_over_leading_whitespace (const char **str, gsize *len)
{
	while (*len > 0 && g_ascii_isspace ((*str)[0]))
		_ch_step_1 (str, len);
}

static void
_strbuf_append_c (char **buf, gsize *len, char ch)
{
	nm_assert (buf);
	nm_assert (len);

	g_return_if_fail (*len > 0);

	(*buf)[0] = ch;
	(*len)--;
	*buf = &(*buf)[1];
}

static gboolean
args_parse_line (const char *line,
                 gsize line_len,
                 const char ***out_p,
                 char **out_error)
{
	gs_unref_array GArray *index = NULL;
	gs_free char *str_buf_orig = NULL;
	char *str_buf;
	gsize str_buf_len;
	gsize i;
	const char *line_start = line;
	char **data;
	char *pdata;

	/* reimplement openvpn's parse_line(). */

	g_return_val_if_fail (line, FALSE);
	g_return_val_if_fail (out_p && !*out_p, FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	*out_p = NULL;

	/* we expect no newline during the first line_len chars. */
	for (i = 0; i < line_len; i++) {
		if (NM_IN_SET (line[i], '\0', '\n'))
			g_return_val_if_reached (FALSE);
	}

	/* if the line ends with '\r', drop that right way (covers \r\n). */
	if (line_len > 0 && line[line_len - 1] == '\r')
		line_len--;

	/* skip over leading space. */
	_ch_skip_over_leading_whitespace (&line, &line_len);

	if (line_len == 0)
		return TRUE;

	if (NM_IN_SET (line[0], ';', '#')) {
		/* comment. Note that als openvpn allows for leading spaces
		 * *before* the comment starts */
		return TRUE;
	}

	/* the maximum required buffer is @line_len+1 characters. We don't produce
	 * *more* characters then given in the input (plus trailing '\0'). */
	str_buf_len = line_len + 1;
	str_buf_orig = g_malloc (str_buf_len);
	str_buf = str_buf_orig;

	index = g_array_new (FALSE, FALSE, sizeof (gsize));

	for (;;) {
		char quote, ch0;
		gssize word_start = line - line_start;
		gsize index_i;

		index_i = str_buf - str_buf_orig;
		g_array_append_val (index, index_i);

		switch ((ch0 = _ch_step_1 (&line, &line_len))) {
		case '"':
		case '\'':
			quote = ch0;

			while (line_len > 0 && line[0] != quote) {
				if (quote == '"' && line[0] == '\\') {
					_ch_step_1 (&line, &line_len);
					if (line_len <= 0)
						break;
				}
				_strbuf_append_c (&str_buf, &str_buf_len, _ch_step_1 (&line, &line_len));
			}

			if (line_len <= 0) {
				*out_error = g_strdup_printf (_("unterminated %s at position %lld"),
				                              quote == '"' ? _("double quote") : _("single quote"),
				                              (long long) word_start);
				return FALSE;
			}

			/* openvpn terminates parsing of quoted paramaters after the closing quote.
			 * E.g. "'a'b" gives "a", "b". */
			_ch_step_1 (&line, &line_len);
			break;
		default:
			/* once openvpn encounters a non-quoted word, it doesn't consider quoting
			 * inside the word.
			 * E.g. "a'b'" gives "a'b'". */
			for (;;) {
				if (ch0 == '\\') {
					if (line_len <= 0) {
						*out_error = g_strdup_printf (_("trailing escaping backslash at position %lld"),
						                              (long long) word_start);
						return FALSE;
					}
					ch0 = _ch_step_1 (&line, &line_len);
				}
				_strbuf_append_c (&str_buf, &str_buf_len, ch0);
				if (line_len <= 0)
					break;
				ch0 = _ch_step_1 (&line, &line_len);
				if (g_ascii_isspace (ch0))
					break;
			}
			break;
		}

		/* the current word is complete.*/
		_strbuf_append_c (&str_buf, &str_buf_len, '\0');
		_ch_skip_over_leading_whitespace (&line, &line_len);

		if (line_len <= 0)
			break;

		if (NM_IN_SET (line[0], ';', '#')) {
			/* comments are allowed to start at the beginning of the next word. */
			break;
		}
	}

	str_buf_len = str_buf - str_buf_orig;

	/* pack the result in a strv array */
	data = g_malloc ((sizeof (const char *) * (index->len + 1)) + str_buf_len);

	pdata = (char *) &data[index->len + 1];
	memcpy (pdata, str_buf_orig, str_buf_len);

	for (i = 0; i < index->len; i++)
		data[i] = &pdata[g_array_index (index, gsize, i)];
	data[i] = NULL;

	*out_p = (const char **) data;

	return TRUE;
}

gboolean
_nmovpn_test_args_parse_line (const char *line,
                              gsize line_len,
                              const char ***out_p,
                              char **out_error)
{
	return args_parse_line (line, line_len, out_p, out_error);
}

static gboolean
args_next_line (const char **content,
                gsize *content_len,
                const char **cur_line,
                gsize *cur_line_len,
                const char **cur_line_delimiter)
{
	const char *s;
	gsize l, offset;

	g_return_val_if_fail (content, FALSE);
	g_return_val_if_fail (content_len, FALSE);
	g_return_val_if_fail (cur_line, FALSE);
	g_return_val_if_fail (cur_line_len, FALSE);
	g_return_val_if_fail (cur_line_delimiter, FALSE);

	l = *content_len;

	if (l <= 0)
		return FALSE;

	*cur_line = s = *content;

	while (l > 0 && !NM_IN_SET (s[0], '\0', '\n'))
		_ch_step_1 (&s, &l);

	offset = s - *content;
	*cur_line_len = offset;

	/* cur_line_delimiter will point to a (static) string
	 * containing the dropped character.
	 * Or NULL if we reached the end of content. */
	if (l > 0) {
		if (s[0] == '\0')
			*cur_line_delimiter = "\0";
		else
			*cur_line_delimiter = "\n";
		offset++;
	} else
		*cur_line_delimiter = NULL;

	*content_len -= offset;
	*content += offset;

	return TRUE;
}

/*****************************************************************************/

static gboolean
parse_http_proxy_auth (const char *default_path,
                       const char *file,
                       char **out_user,
                       char **out_pass,
                       char **out_error)
{
	gs_free char *file_free = NULL;
	gs_free char *contents = NULL;
	char **lines, **iter;

	g_return_val_if_fail (out_user && !*out_user, FALSE);
	g_return_val_if_fail (out_pass && !*out_pass, FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	if (!file ||
	    NM_IN_STRSET (file, "stdin", "auto"))
		return TRUE;

	if (!g_path_is_absolute (file)) {
		file_free = g_build_path ("/", default_path, file, NULL);
		file = file_free;
	}

	/* Grab user/pass from authfile */
	if (!g_file_get_contents (file, &contents, NULL, NULL)) {
		*out_error = g_strdup_printf (_("unable to read HTTP proxy auth file"));
		return FALSE;
	}

	lines = g_strsplit_set (contents, "\n\r", 0);
	for (iter = lines; iter && *iter; iter++) {
		if ((*iter)[0] == '\0')
			continue;
		if (!*out_user)
			*out_user = g_strdup (g_strstrip (*iter));
		else if (!*out_pass) {
			*out_pass = g_strdup (g_strstrip (*iter));
			break;
		}
	}
	g_strfreev (lines);

	if (!*out_user || !*out_pass) {
		*out_error = g_strdup_printf (_("cannot read user/password from HTTP proxy auth file"));
		g_clear_pointer (out_user, g_free);
		g_clear_pointer (out_pass, g_free);
		return FALSE;
	}

	if (   !_is_utf8 (*out_user)
	    || !_is_utf8 (*out_pass)) {
		*out_error = g_strdup_printf (_("user/password from HTTP proxy auth file must be UTF-8 encoded"));
		g_clear_pointer (out_user, g_free);
		g_clear_pointer (out_pass, g_free);
		return FALSE;
	}
	return TRUE;
}

/*****************************************************************************/

typedef struct {
	char *token;
	char *path;
	gsize token_start_line;
	GString *blob_data;
	const char *key;
} InlineBlobData;

static void
inline_blob_data_free (InlineBlobData *data)
{
	g_return_if_fail (data);

	g_free (data->token);
	g_free (data->path);
	g_string_free (data->blob_data, TRUE);
	g_slice_free (InlineBlobData, data);
}

static char *
inline_blob_construct_path (const char *basename, const char *token)
{
	gs_free char *f_filename = NULL;

	g_return_val_if_fail (basename, NULL);
	g_return_val_if_fail (token && token[0], NULL);

	/* Construct file name to write the data in */
	f_filename = g_strdup_printf ("%s-%s.pem", basename, token);

	if (_nmovpn_test_temp_path)
		return g_build_filename (_nmovpn_test_temp_path, f_filename, NULL);

	return g_build_filename (g_get_home_dir (), ".cert/nm-openvpn", f_filename, NULL);
}

static gboolean
inline_blob_mkdir_parents (const InlineBlobData *data, const char *filepath, char **out_error)
{
	gs_free char *dirname = NULL;

	g_return_val_if_fail (filepath && filepath[0], FALSE);
	g_return_val_if_fail (out_error && !*out_error, FALSE);

	dirname = g_path_get_dirname (filepath);
	if (NM_IN_STRSET (dirname, "/", "."))
		return TRUE;

	if (g_file_test (dirname, G_FILE_TEST_IS_DIR))
		return TRUE;

	if (g_file_test (dirname, G_FILE_TEST_EXISTS)) {
		*out_error = g_strdup_printf (_("“%s” is not a directory"), dirname);
		return FALSE;
	}

	if (!inline_blob_mkdir_parents (data, dirname, out_error))
		return FALSE;

	if (mkdir (dirname, 0755) < 0) {
		*out_error = g_strdup_printf (_("cannot create “%s” directory"), dirname);
		return FALSE;
	}

	return TRUE;
}

static gboolean
inline_blob_write_out (const InlineBlobData *data, GError **error)
{
	mode_t saved_umask;

	if (!_nmovpn_test_temp_path) {
		gs_free char *err_msg = NULL;

		/* in test mode we don't create the certificate directory. */
		if (!inline_blob_mkdir_parents (data, data->path, &err_msg)) {
			g_set_error (error,
			             NMV_EDITOR_PLUGIN_ERROR,
			             NMV_EDITOR_PLUGIN_ERROR_FAILED,
			             _("cannot write <%s> blob from line %ld to file (%s)"),
			             data->token,
			             (long) data->token_start_line,
			             err_msg);
			return FALSE;
		}
	}

	saved_umask = umask (S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);

	/* The file is written with the default umask. Whether that is safe enough
	 * to protect (potentally) private data or allows the openvpn service to
	 * access the file later on is left as exercise for the user. */
	if (!g_file_set_contents (data->path, data->blob_data->str, data->blob_data->len, NULL)) {
		g_set_error (error,
		             NMV_EDITOR_PLUGIN_ERROR,
		             NMV_EDITOR_PLUGIN_ERROR_FAILED,
		             _("cannot write <%s> blob from line %ld to file “%s”"),
		             data->token,
		             (long) data->token_start_line,
		             data->path);
		umask (saved_umask);
		return FALSE;
	}

	umask (saved_umask);
	return TRUE;
}

/*****************************************************************************/

NMConnection *
do_import (const char *path, const char *contents, gsize contents_len, GError **error)
{
	gs_unref_object NMConnection *connection = NULL;
	NMSettingConnection *s_con;
	NMSettingIPConfig *s_ip4;
	NMSettingVpn *s_vpn;
	const char *cur_line, *cur_line_delimiter;
	gsize cur_line_len;
	gsize contents_cur_line;
	gboolean have_client = FALSE, have_remote = FALSE;
	gboolean have_pass = FALSE, have_sk = FALSE;
	const char *ctype = NULL;
	gs_free char *basename = NULL;
	gs_free char *default_path = NULL;
	char *tmp, *tmp2;
	const char *ta_direction = NULL, *secret_direction = NULL;
	gboolean allow_ta_direction = FALSE, allow_secret_direction = FALSE;
	gboolean have_cert = FALSE, have_key = FALSE, have_ca = FALSE, have_pkcs12 = FALSE;
	const char *cert_path = NULL, *key_path = NULL, *ca_path = NULL;
	GSList *inline_blobs = NULL;
	GSList *sl_iter;

	g_return_val_if_fail (!error || !*error, NULL);

	connection = nm_simple_connection_new ();

	s_con = NM_SETTING_CONNECTION (nm_setting_connection_new ());
	nm_connection_add_setting (connection, NM_SETTING (s_con));
	s_ip4 = NM_SETTING_IP_CONFIG (nm_setting_ip4_config_new ());
	nm_connection_add_setting (connection, NM_SETTING (s_ip4));
	g_object_set (s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_AUTO, NULL);
	s_vpn = NM_SETTING_VPN (nm_setting_vpn_new ());
	g_object_set (s_vpn, NM_SETTING_VPN_SERVICE_TYPE, NM_VPN_SERVICE_TYPE_OPENVPN, NULL);
	nm_connection_add_setting (connection, NM_SETTING (s_vpn));

	/* Get the default path for ca, cert, key file, these files maybe
	 * in same path with the configuration file */
	if (g_path_is_absolute (path))
		default_path = g_path_get_dirname (path);
	else {
		tmp = g_get_current_dir ();
		tmp2 = g_path_get_dirname (path);
		default_path = g_build_filename (tmp, tmp2, NULL);
		g_free (tmp);
		g_free (tmp2);
	}

	basename = g_path_get_basename (path);
	tmp = strrchr (basename, '.');
	if (tmp)
		*tmp = '\0';
	g_object_set (s_con, NM_SETTING_CONNECTION_ID, basename, NULL);

	if (strncmp (contents, "\xEF\xBB\xBF", 3) == 0) {
		/* skip over UTF-8 BOM */
		contents += 3;
		contents_len -= 3;
	}

	contents_cur_line = 0;
	while (args_next_line (&contents,
	                       &contents_len,
	                       &cur_line,
	                       &cur_line_len,
	                       &cur_line_delimiter)) {
		gs_free const char **params = NULL;
		char *line_error = NULL;
		gint64 v_int64;

		contents_cur_line++;

		if (!args_parse_line (cur_line, cur_line_len, &params, &line_error))
			goto handle_line_error;

		if (!params) {
			/* empty line of comments. */
			continue;
		}

		g_assert (params[0]);

		/* allow for a leading double-dash and skip over it (bypass_doubledash). */
		if (g_str_has_prefix (params[0], "--"))
			params[0] = &params[0][2];

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_CLIENT, NMV_OVPN_TAG_TLS_CLIENT)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			have_client = TRUE;
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_KEY_DIRECTION)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_key_direction (params, 1, &ta_direction, &line_error))
				goto handle_line_error;
			secret_direction = ta_direction;
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_DEV)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_nonempty (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_utf8safe (s_vpn, NM_OPENVPN_KEY_DEV, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_DEV_TYPE)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!NM_IN_STRSET (params[1], "tun", "tap")) {
				line_error = args_params_error_message_invalid_arg (params, 1);
				goto handle_line_error;
			}
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_DEV_TYPE, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PROTO)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			/* Valid parameters are defined in shared/utils.h
			 * 'tcp' isn't technically valid, but it used to be accepted so
			 * we'll handle it here anyway.
			 */
			if (!NM_IN_STRSET (params[1], NMOVPN_PROTCOL_TYPES)) {
				line_error = g_strdup_printf (_("proto expects protocol type like “udp” or “tcp”"));
				goto handle_line_error;
			}
			if (!NM_IN_STRSET (params[1], "udp", "udp4", "udp6")) {
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_PROTO_TCP, "yes");
			}
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_MSSFIX)) {
			if (!args_params_check_nargs_minmax (params, 0, 1, &line_error))
				goto handle_line_error;
			if (params[1]) {
				if (!args_params_parse_int64 (params, 1, 0, G_MAXINT32, &v_int64, &line_error))
					goto handle_line_error;
				setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_MSSFIX, v_int64);
			} else
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_MSSFIX, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_MTU_DISC)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!NM_IN_STRSET (params[1], "no", "maybe", "yes")) {
				line_error = g_strdup_printf (_("unsupported mtu-disc argument"));
				goto handle_line_error;
			}
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_MTU_DISC, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_CRL_VERIFY)) {
			const char *file;
			gs_free char *file_free = NULL;

			if (!args_params_check_nargs_minmax (params, 1, 2, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_nonempty (params, 1, NULL, &line_error))
				goto handle_line_error;
			if (params[2] && !nm_streq (params[2], "dir")) {
				line_error = g_strdup_printf (_("unsupported crl-verify argument"));
				goto handle_line_error;
			}

			file = params[1];
			if (!g_path_is_absolute (file))
				file = file_free = g_build_filename (default_path, file, NULL);

			if (params[2])
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_CRL_VERIFY_DIR, file);
			else
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_CRL_VERIFY_FILE, file);
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_NS_CERT_TYPE)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!NM_IN_STRSET (params[1], NM_OPENVPN_NS_CERT_TYPE_CLIENT, NM_OPENVPN_NS_CERT_TYPE_SERVER)) {
				line_error = g_strdup_printf (_("invalid option"));
				goto handle_line_error;
			}
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_NS_CERT_TYPE, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TUN_MTU)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, 0xffff, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_TUNNEL_MTU, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0],
		                  NMV_OVPN_TAG_CONNECT_TIMEOUT,
		                  NMV_OVPN_TAG_SERVER_POLL_TIMEOUT)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, G_MAXINT, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_CONNECT_TIMEOUT, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_FRAGMENT)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, 0xffff, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_FRAGMENT_SIZE, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_ALLOW_COMPRESSION)) {
			if (!args_params_check_nargs_minmax (params, 1, 1, &line_error))
				goto handle_line_error;

			if (!NM_IN_STRSET (params[1], "yes", "no", "asym")) {
				line_error = g_strdup_printf (_("allow-compression: invalid argument"));
				goto handle_line_error;
			}

			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_ALLOW_COMPRESSION, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_COMP_LZO)) {
			const char *v;

			if (!args_params_check_nargs_minmax (params, 0, 1, &line_error))
				goto handle_line_error;

			v = params[1] ?: "adaptive";

			if (nm_streq (v, "no")) {
				/* old plasma-nm used to set "comp-lzo=no" to mean unset, thus it spoiled
				 * the "no" option to be used in the connection. Workaround that, by instead
				 * using "no-by-default" (bgo#769177). */
				v = "no-by-default";
			} else if (!NM_IN_STRSET (v, "yes", "adaptive")) {
				line_error = g_strdup_printf (_("unsupported comp-lzo argument"));
				goto handle_line_error;
			}
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_COMP_LZO, v);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_COMPRESS)) {
			if (!args_params_check_nargs_minmax (params, 0, 1, &line_error))
				goto handle_line_error;
			if (params[1]) {
				if (!NM_IN_STRSET (params[1], "lzo", "lz4", "lz4-v2")) {
					line_error = g_strdup_printf (_("unsupported compress argument"));
					goto handle_line_error;
				}
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_COMPRESS, params[1]);
			} else {
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_COMPRESS, "yes");
			}
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_FLOAT)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_FLOAT, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_RENEG_SEC)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, G_MAXINT, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_RENEG_SECONDS, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_MAX_ROUTES)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, 100000000, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_MAX_ROUTES, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_HTTP_PROXY_RETRY, NMV_OVPN_TAG_SOCKS_PROXY_RETRY)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_RETRY, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_HTTP_PROXY, NMV_OVPN_TAG_SOCKS_PROXY)) {
			const char *proxy_type = NULL;
			gint64 port = 0;
			gs_free char *user = NULL;
			gs_free char *pass = NULL;

			if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_HTTP_PROXY)) {
				proxy_type = "http";
				if (!args_params_check_nargs_minmax (params, 2, 4, &line_error))
					goto handle_line_error;
			} else {
				proxy_type = "socks";
				if (!args_params_check_nargs_minmax (params, 1, 3, &line_error))
					goto handle_line_error;
			}

			if (!args_params_check_arg_utf8 (params, 1, "service", &line_error))
				goto handle_line_error;

			if (params[2]) {
				if (!args_params_parse_port (params, 2, &port, &line_error))
					goto handle_line_error;

				if (params[3]) {
					if (!parse_http_proxy_auth (default_path, params[3], &user, &pass, &line_error))
						goto handle_line_error;
				}
			}

			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_TYPE, proxy_type);

			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_SERVER, params[1]);
			if (port > 0)
				setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_PROXY_PORT, port);
			if (user)
				setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_USERNAME, user);
			if (pass) {
				nm_setting_vpn_add_secret (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD, pass);
				nm_setting_set_secret_flags (NM_SETTING (s_vpn),
				                             NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD,
				                             NM_SETTING_SECRET_FLAG_AGENT_OWNED,
				                             NULL);
			}
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_REMOTE)) {
			const char *prev;
			GString *new_remote;
			int port = -1;
			gboolean host_has_colon;
			struct in6_addr a;

			if (!args_params_check_nargs_minmax (params, 1, 3, &line_error))
				goto handle_line_error;

			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			if (strchr (params[1], ' ')) {
				line_error = g_strdup_printf (_("remote cannot contain space"));
				goto handle_line_error;
			}
			if (strchr (params[1], ',')) {
				line_error = g_strdup_printf (_("remote cannot contain comma"));
				goto handle_line_error;
			}

			if (params[2]) {
				if (!args_params_parse_port (params, 2, &v_int64, &line_error))
					goto handle_line_error;
				port = v_int64;

				if (params[3]) {
					if (!NM_IN_STRSET (params[3], NMOVPN_PROTCOL_TYPES)) {
						line_error = g_strdup_printf (_("remote expects protocol type like “udp” or “tcp”"));
						goto handle_line_error;
					}
				}
			}

			new_remote = g_string_sized_new (64);

			prev = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE);
			if (prev) {
				g_string_assign (new_remote, prev);
				g_string_append (new_remote, ", ");
			}

			host_has_colon = (strchr (params[1], ':') != NULL);
			if (   host_has_colon
			    && inet_pton (AF_INET6, params[1], &a) == 1) {
				/* need to escape the host. */
				g_string_append_printf (new_remote, "[%s]", params[1]);
			} else
				g_string_append (new_remote, params[1]);

			if (params[2]) {
				g_string_append_printf (new_remote, ":%d", port);
				if (params[3]) {
					g_string_append_c (new_remote, ':');
					g_string_append (new_remote, params[3]);
				} else if (host_has_colon)
					g_string_append_c (new_remote, ':');
			} else if (host_has_colon)
				g_string_append (new_remote, "::");

			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE, new_remote->str);
			g_string_free (new_remote, TRUE);

			have_remote = TRUE;
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_REMOTE_RANDOM)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_RANDOM, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_REMOTE_RANDOM_HOSTNAME)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_RANDOM_HOSTNAME, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_ALLOW_PULL_FQDN)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_ALLOW_PULL_FQDN, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TUN_IPV6)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TUN_IPV6, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PUSH_PEER_INFO)) {
			if (!args_params_check_nargs_n (params, 0, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_PUSH_PEER_INFO, "yes");
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PORT, NMV_OVPN_TAG_RPORT)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_port (params, 1, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_PORT, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PING, NMV_OVPN_TAG_PING_EXIT, NMV_OVPN_TAG_PING_RESTART)) {
			const char *key = NULL;

			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, G_MAXINT, &v_int64, &line_error))
				goto handle_line_error;

			if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PING))
				key = NM_OPENVPN_KEY_PING;
			else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PING_EXIT))
				key = NM_OPENVPN_KEY_PING_EXIT;
			else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PING_RESTART))
				key = NM_OPENVPN_KEY_PING_RESTART;

			setting_vpn_add_data_item_int64 (s_vpn, key, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_VERSION_MIN)) {
			if (!args_params_check_nargs_minmax (params, 1, 2, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TLS_VERSION_MIN, params[1]);
			if (params[2]) {
				if (nm_streq (params[2], "or-highest")) {
					setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TLS_VERSION_MIN_OR_HIGHEST, "yes");
				} else {
					line_error = g_strdup_printf (_("invalid keyword “%s” in tls-version-min"), params[2]);
					goto handle_line_error;
				}
			}
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_VERSION_MAX)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TLS_VERSION_MAX, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0],
		                  NMV_OVPN_TAG_CA,
		                  NMV_OVPN_TAG_CERT,
		                  NMV_OVPN_TAG_EXTRA_CERTS,
		                  NMV_OVPN_TAG_KEY,
		                  NMV_OVPN_TAG_PKCS12,
		                  NMV_OVPN_TAG_SECRET,
		                  NMV_OVPN_TAG_TLS_AUTH,
		                  NMV_OVPN_TAG_TLS_CRYPT,
		                  NMV_OVPN_TAG_TLS_CRYPT_V2)) {
			const char *file;
			gs_free char *file_free = NULL;
			gboolean can_have_direction;
			const char *s_direction = NULL;

			can_have_direction = NM_IN_STRSET (params[0],
			                                   NMV_OVPN_TAG_SECRET,
			                                   NMV_OVPN_TAG_TLS_AUTH);

			if (!args_params_check_nargs_minmax (params, 1, can_have_direction ? 2 : 1, &line_error))
				goto handle_line_error;

			if (!args_params_check_arg_nonempty (params, 1, NULL, &line_error))
				goto handle_line_error;
			file = params[1];

			if (params[2]) {
				if (!args_params_parse_key_direction (params, 2, &s_direction, &line_error))
					goto handle_line_error;
			}

			if (!g_path_is_absolute (file))
				file = file_free = g_build_filename (default_path, file, NULL);

			if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_PKCS12)) {
				 /* OpenVPN allows --pkcs12 with external (PEM) --ca. Don't overwrite it with the PKCS#12 file. */
				if (!have_ca)
					setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_CA, file);
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_CERT, file);
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_KEY, file);
				have_pkcs12 = TRUE;
			} else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_CA)) {
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_CA, file);
				have_ca = TRUE;
			} else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_CERT)) {
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_CERT, file);
				have_cert = TRUE;
			} else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_KEY)) {
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_KEY, file);
				have_key = TRUE;
			} else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_SECRET)) {
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_STATIC_KEY, file);
				if (s_direction)
					secret_direction = s_direction;
				allow_secret_direction = TRUE;
				have_sk = TRUE;
			} else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_AUTH)) {
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_TA, file);
				if (s_direction)
					ta_direction = s_direction;
				allow_ta_direction = TRUE;
			} else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_CRYPT))
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_TLS_CRYPT, file);
			else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_CRYPT_V2))
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_TLS_CRYPT_V2, file);
			else if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_EXTRA_CERTS))
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_EXTRA_CERTS, file);
			else
				g_assert_not_reached ();
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_CIPHER)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_CIPHER, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_CIPHER)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TLS_CIPHER, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_KEEPALIVE)) {
			gint64 v2;

			if (!args_params_check_nargs_n (params, 2, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 0, G_MAXINT, &v_int64, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 2, 0, G_MAXINT, &v2, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_PING, v_int64);
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_PING_RESTART, v2);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_KEYSIZE)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_parse_int64 (params, 1, 1, 65535, &v_int64, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item_int64 (s_vpn, NM_OPENVPN_KEY_KEYSIZE, v_int64);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_TLS_REMOTE)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TLS_REMOTE, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_VERIFY_X509_NAME)) {
			const char *type = NM_OPENVPN_VERIFY_X509_NAME_TYPE_SUBJECT;
			gs_free char *item = NULL;

			if (!args_params_check_nargs_minmax (params, 1, 2, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;

			if (params[2]) {
				if (!NM_IN_STRSET (params[2],
				                   NM_OPENVPN_VERIFY_X509_NAME_TYPE_SUBJECT,
				                   NM_OPENVPN_VERIFY_X509_NAME_TYPE_NAME,
				                   NM_OPENVPN_VERIFY_X509_NAME_TYPE_NAME_PREFIX)) {
					line_error = g_strdup_printf (_("invalid verify-x509-name type"));
					goto handle_line_error;
				}

				type = params[2];
			}

			item = g_strdup_printf ("%s:%s", type, params[1]);
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_VERIFY_X509_NAME, item);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_REMOTE_CERT_TLS)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!NM_IN_STRSET (params[1], NM_OPENVPN_REM_CERT_TLS_CLIENT, NM_OPENVPN_REM_CERT_TLS_SERVER)) {
				line_error = g_strdup_printf (_("invalid option"));
				goto handle_line_error;
			}
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_CERT_TLS, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_IFCONFIG)) {
			if (!args_params_check_nargs_n (params, 2, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, "local", &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 2, "remote", &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_LOCAL_IP, params[1]);
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_IP, params[2]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_AUTH_USER_PASS)) {
			if (!args_params_check_nargs_minmax (params, 0, 1, &line_error))
				goto handle_line_error;
			have_pass = TRUE;
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_AUTH)) {
			if (!args_params_check_nargs_n (params, 1, &line_error))
				goto handle_line_error;
			if (!args_params_check_arg_utf8 (params, 1, NULL, &line_error))
				goto handle_line_error;
			setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_AUTH, params[1]);
			continue;
		}

		if (NM_IN_STRSET (params[0], NMV_OVPN_TAG_ROUTE)) {
			in_addr_t network;
			in_addr_t gateway = 0;
			guint32 prefix = 32;
			gint64 metric = -1;
			NMIPRoute *route;

			if (!args_params_check_nargs_minmax (params, 1, 4, &line_error))
				goto handle_line_error;

			if (!args_params_parse_ip4 (params, 1, TRUE, &network, &line_error))
				goto handle_line_error;

			if (params[2]) {
				in_addr_t netmask;

				if (!args_params_parse_ip4 (params, 2, FALSE, &netmask, &line_error))
					goto handle_line_error;
				prefix = nm_utils_ip4_netmask_to_prefix (netmask);

				if (params[3]) {
					if (!args_params_parse_ip4 (params, 3, TRUE, &gateway, &line_error))
						goto handle_line_error;
					if (params[4]) {
						if (!args_params_parse_int64 (params, 4, 0, G_MAXUINT32, &v_int64, &line_error))
							goto handle_line_error;
						metric = (guint32) v_int64;
					}
				}
			}

			if (prefix == 0 && network == 0) {
				/* the default-route cannot be specified as normal route in NMSettingIPConfig.
				 * Just set never-default=FALSE (which is already the default). */
				g_object_set (s_ip4,
				              NM_SETTING_IP_CONFIG_NEVER_DEFAULT,
				              FALSE,
				              NULL);
				continue;
			}

			route = nm_ip_route_new_binary (AF_INET, &network, prefix, params[3] ? &gateway : NULL, metric, NULL);
			nm_setting_ip_config_add_route (s_ip4, route);
			nm_ip_route_unref (route);
		}

		if (params[0][0] == '<' && params[0][strlen (params[0]) - 1] == '>') {
			gs_free char *token = g_strndup (&params[0][1], strlen (params[0]) - 2);
			gs_free char *end_token = NULL;
			gsize end_token_len;
			gsize my_contents_cur_line = contents_cur_line;
			gboolean is_base64 = FALSE;
			char *f_path;
			const char *key;
			GString *blob_data;
			InlineBlobData *inline_blob_data;

			if (nm_streq (token, INLINE_BLOB_CA)) {
				key = NM_OPENVPN_KEY_CA;
				have_ca = TRUE;
			} else if (nm_streq (token, INLINE_BLOB_CERT)) {
				key = NM_OPENVPN_KEY_CERT;
				have_cert = TRUE;
			} else if (nm_streq (token, INLINE_BLOB_KEY)) {
				key = NM_OPENVPN_KEY_KEY;
				have_key = TRUE;
			} else if (nm_streq (token, INLINE_BLOB_PKCS12)) {
				is_base64 = TRUE;
				key = NULL;
				have_pkcs12 = TRUE;
			} else if (nm_streq (token, INLINE_BLOB_EXTRA_CERTS))
				key = NM_OPENVPN_KEY_EXTRA_CERTS;
			else if (nm_streq (token, INLINE_BLOB_CRL_VERIFY))
				key = NM_OPENVPN_KEY_CRL_VERIFY_FILE;
			else if (nm_streq (token, INLINE_BLOB_TLS_CRYPT))
				key = NM_OPENVPN_KEY_TLS_CRYPT;
			else if (nm_streq (token, INLINE_BLOB_TLS_CRYPT_V2))
				key = NM_OPENVPN_KEY_TLS_CRYPT_V2;
			else if (nm_streq (token, INLINE_BLOB_TLS_AUTH)) {
				key = NM_OPENVPN_KEY_TA;
				allow_ta_direction = TRUE;
			} else if (nm_streq (token, INLINE_BLOB_SECRET)) {
				key = NM_OPENVPN_KEY_STATIC_KEY;
				allow_secret_direction = TRUE;
			} else {
				line_error = g_strdup_printf (_("unsupported blob/xml element"));
				goto handle_line_error;
			}

			end_token = g_strdup_printf ("</%s>", token);
			end_token_len = strlen (end_token);

			blob_data = g_string_new (NULL);

			while (args_next_line (&contents,
			                       &contents_len,
			                       &cur_line,
			                       &cur_line_len,
			                       &cur_line_delimiter)) {
				my_contents_cur_line++;

				/* skip over trailing space like openvpn does. */
				_ch_skip_over_leading_whitespace (&cur_line, &cur_line_len);

				if (!strncmp (cur_line, end_token, end_token_len)) {
					end_token_len = 0;
					break;
				}

				g_string_append_len (blob_data, cur_line, cur_line_len);
				if (cur_line_delimiter)
					g_string_append_c (blob_data, cur_line_delimiter[0]);
			}
			if (end_token_len) {
				line_error = g_strdup_printf (_("unterminated blob element <%s>"), token);
				g_string_free (blob_data, TRUE);
				goto handle_line_error;
			}

			if (is_base64) {
				gs_free guint8 *d = NULL;
				gsize l;

				d = g_base64_decode (blob_data->str, &l);
				g_string_truncate (blob_data, 0);
				g_string_append_len (blob_data, (const char *) d, l);
			}

			/* the latest cert wins... */
			for (sl_iter = inline_blobs; sl_iter; sl_iter = sl_iter->next) {
				InlineBlobData *d = sl_iter->data;

				if (nm_streq (d->token, token)) {
					inline_blobs = g_slist_delete_link (inline_blobs, sl_iter);
					inline_blob_data_free (d);
					break;
				}
			}

			f_path = inline_blob_construct_path (basename, token);

			inline_blob_data = g_slice_new (InlineBlobData);
			inline_blob_data->blob_data = blob_data;
			inline_blob_data->token_start_line = contents_cur_line;
			inline_blob_data->path = f_path;
			inline_blob_data->token = token;
			inline_blob_data->key = key;
			token = NULL;

			inline_blobs = g_slist_prepend (inline_blobs, inline_blob_data);
			contents_cur_line = my_contents_cur_line;

			if (key)
				setting_vpn_add_data_item_path (s_vpn, key, f_path);
			else {
				nm_assert (nm_streq (token, INLINE_BLOB_PKCS12));
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_CA, f_path);
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_CERT, f_path);
				setting_vpn_add_data_item_path (s_vpn, NM_OPENVPN_KEY_KEY, f_path);
			}
			continue;
		}

		/* TODO: currently we ignore any unknown options and skip over them. */
		continue;

handle_line_error:
		g_set_error (error,
		             NMV_EDITOR_PLUGIN_ERROR,
		             NMV_EDITOR_PLUGIN_ERROR_FILE_INVALID,
		             _("configuration error: %s (line %ld)"),
		             line_error ? : _("unknown or unsupported option"),
		             (long) contents_cur_line);
		g_free (line_error);
		goto out_error;
	}

	if (allow_secret_direction && secret_direction)
		setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_STATIC_KEY_DIRECTION, secret_direction);
	if (allow_ta_direction && ta_direction)
		setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_TA_DIR, ta_direction);

	if (!have_client && !have_sk) {
		g_set_error_literal (error,
		                     NMV_EDITOR_PLUGIN_ERROR,
		                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
		                     _("The file to import wasn’t a valid OpenVPN client configuration"));
		goto out_error;
	}

	if (!have_remote) {
		g_set_error_literal (error,
		                     NMV_EDITOR_PLUGIN_ERROR,
		                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
		                     _("The file to import wasn’t a valid OpenVPN configuration (no remote)"));
		goto out_error;
	}

	/* Validate PKCS#12/PEM/CA settings. PKCS#12 and PEM cannot be mixed,
	 * with the exception of PKCS#12 cert + PEM CA. */
	if (have_client && have_pkcs12) {
		if (have_cert || have_key) {
			g_set_error_literal (error,
			                     NMV_EDITOR_PLUGIN_ERROR,
			                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
			                     _("The file to import wasn’t a valid OpenVPN configuration (--pkcs12 can not be used with --cert or --key)"));
			goto out_error;
		}

		if (have_ca) {
			ca_path = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CA);
			if (is_pkcs12 (ca_path)) {
				g_set_error_literal (error,
				                     NMV_EDITOR_PLUGIN_ERROR,
				                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
				                     _("The file to import wasn’t a valid OpenVPN configuration (--ca can not be PKCS#12 format)"));
				goto out_error;
			}
		}
	} else if (have_client && !have_pkcs12) {
		if (!have_ca) {
			g_set_error_literal (error,
			                     NMV_EDITOR_PLUGIN_ERROR,
			                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
			                     _("The file to import wasn’t a valid OpenVPN configuration (missing --ca)"));
			goto out_error;
		}

		ca_path = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CA);

		if ((have_cert || have_key) && !(have_cert && have_key)) {
			g_set_error_literal (error,
			                     NMV_EDITOR_PLUGIN_ERROR,
			                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
			                     _("The file to import wasn’t a valid OpenVPN configuration (if you use one of --cert or --key, you must use them both)"));
			goto out_error;
		}

		cert_path = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CERT);
		key_path = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_KEY);

		if (cert_path && is_pkcs12 (cert_path)) {
			g_set_error_literal (error,
			                     NMV_EDITOR_PLUGIN_ERROR,
			                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
			                     _("The file to import wasn’t a valid OpenVPN configuration (--cert can not be PKCS#12 format.)"));
			goto out_error;
		}

		if (key_path && is_pkcs12 (key_path)) {
			g_set_error_literal (error,
			                     NMV_EDITOR_PLUGIN_ERROR,
			                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
			                     _("The file to import wasn’t a valid OpenVPN configuration (--key can not be PKCS#12 format.)"));
			goto out_error;
		}
	}

	/* Determine connection type */
	if (have_pass) {
		if (have_cert || have_pkcs12)
			ctype = NM_OPENVPN_CONTYPE_PASSWORD_TLS;
		else if (have_ca)
			ctype = NM_OPENVPN_CONTYPE_PASSWORD;
	} else if (have_cert || have_pkcs12) {
		ctype = NM_OPENVPN_CONTYPE_TLS;
	} else if (have_sk)
		ctype = NM_OPENVPN_CONTYPE_STATIC_KEY;

	if (!ctype)
		ctype = NM_OPENVPN_CONTYPE_TLS;

	setting_vpn_add_data_item (s_vpn, NM_OPENVPN_KEY_CONNECTION_TYPE, ctype);

	/* Default secret flags to be agent-owned */
	if (have_pass) {
		nm_setting_set_secret_flags (NM_SETTING (s_vpn),
		                             NM_OPENVPN_KEY_PASSWORD,
		                             NM_SETTING_SECRET_FLAG_AGENT_OWNED,
		                             NULL);
	}
	if (have_key || have_pkcs12) {
		gs_free char *key_path_free = NULL;

		if (is_encrypted (nm_utils_str_utf8safe_unescape (key_path, &key_path_free))) {
			/* If there should be a private key password, default it to
			 * being agent-owned. */
			nm_setting_set_secret_flags (NM_SETTING (s_vpn),
			                             NM_OPENVPN_KEY_CERTPASS,
			                             NM_SETTING_SECRET_FLAG_AGENT_OWNED,
			                             NULL);
		}
	}

	if (inline_blobs) {
		GSList *tmp_list = NULL;

		/* filter out blobs that are shadowed and not used. */
		while ((sl_iter = inline_blobs)) {
			InlineBlobData *data = sl_iter->data;
			gboolean is_good = TRUE;

			inline_blobs = inline_blobs->next;

			/* Check whether the setting was not overwritten by a later entry in the config-file. */
			if (nm_streq (data->token, INLINE_BLOB_PKCS12)) {
				if (   !setting_vpn_eq_data_item_utf8safe (s_vpn, NM_OPENVPN_KEY_CA, data->path)
				    && !setting_vpn_eq_data_item_utf8safe (s_vpn, NM_OPENVPN_KEY_CERT, data->path)
				    && !setting_vpn_eq_data_item_utf8safe (s_vpn, NM_OPENVPN_KEY_KEY, data->path))
					is_good = FALSE;
			} else {
				if (!setting_vpn_eq_data_item_utf8safe (s_vpn, data->key, data->path))
					is_good = FALSE;
			}
			if (!is_good) {
				g_slist_free_1 (sl_iter);
				inline_blob_data_free (data);
				continue;
			}

			/* we prepend the element to a new, temporary list. That means, we
			 * reverse the order here. That is just what we need, because the
			 * list was previously constructed in reverse order. */
			sl_iter->next = tmp_list;
			tmp_list = sl_iter;
		}
		inline_blobs = tmp_list;
	}

	for (sl_iter = inline_blobs; sl_iter; sl_iter = sl_iter->next) {
		if (!inline_blob_write_out (sl_iter->data, error))
			goto out_error;
	}

	g_slist_free_full (inline_blobs, (GDestroyNotify) inline_blob_data_free);

	g_return_val_if_fail (!error || !*error, connection);
	return g_steal_pointer (&connection);

out_error:
	g_slist_free_full (inline_blobs, (GDestroyNotify) inline_blob_data_free);
	g_return_val_if_fail (!error || *error, NULL);
	return NULL;
}

/*****************************************************************************/

static const char *
escape_arg (const char *value, char **buf)
{
	const char *s;
	char *result, *i_result;
	gboolean needs_double_quotes = FALSE;
	gboolean needs_quotation = FALSE;
	gsize len;

	nm_assert (value);
	nm_assert (buf && !*buf);

	if (value[0] == '\0')
		return (*buf = g_strdup ("''"));

	/* check if the string contains only benign characters... */
	len = 0;
	for (s = value; s[0]; s++) {
		char c = s[0];

		len++;
		if (   (c >= '0' && c <= '9')
		    || (c >= 'a' && c <= 'z')
		    || (c >= 'A' && c <= 'Z')
		    || NM_IN_SET (c, '_', '-', ':', '/'))
			continue;

		needs_quotation = TRUE;
		if (NM_IN_SET (c, '\'', '\n'))
			needs_double_quotes = TRUE;
	}
	if (!needs_quotation)
		return value;

	if (!needs_double_quotes) {
		result = g_malloc (len + 2 + 1);
		result[0] = '\'';
		memcpy (&result[1], value, len);
		result[1 + len] = '\'';
		result[2 + len] = '\0';
	} else {
		i_result = result = g_malloc (len * 2 + 3);
		*(i_result++) = '"';
		for (s = value; s[0]; s++) {
			if (s[0] == '\n') {
				/* Openvpn does not support encoding '\n' in the ovpn configuration file.
				 * This configuration cannot be expressed in an ovpn file.
				 *
				 * Still escape it as '\n', although openvpn's parser will warn
				 * about the invalid escape sequence. */
				*(i_result++) = '\\';
				*(i_result++) = 'n';
			} else {
				if (NM_IN_SET (s[0], '\\', '"'))
					*(i_result++) = '\\';
				*(i_result++) = s[0];
			}
		}
		*(i_result++) = '"';
		*(i_result++) = '\0';
	}

	*buf = result;
	return result;
}

static void
args_write_line_v (GString *f, gsize nargs, const char **args)
{
	gsize i;
	gboolean printed = FALSE;

	nm_assert (args);
	nm_assert (args[0]);

	for (i = 0; i < nargs; i++) {
		gs_free char *tmp = NULL;

		/* NULL is skipped. This is for convenience to specify
		 * optional arguments. */
		if (!args[i])
			continue;

		if (printed)
			g_string_append_c (f, ' ');
		printed = TRUE;
		g_string_append (f, escape_arg (args[i], &tmp));
	}
	g_string_append_c (f, '\n');
}
#define args_write_line(f, ...) args_write_line_v(f, NM_NARG (__VA_ARGS__), (const char *[]) { __VA_ARGS__ })

static void
args_write_line_int64 (GString *f, const char *key, gint64 value)
{
	char tmp[64];

	args_write_line (f, key, nm_sprintf_buf (tmp, "%"G_GINT64_FORMAT, value));
}

static void
args_write_line_setting_value_int (GString *f,
                                   const char *tag_key,
                                   NMSettingVpn *s_vpn,
                                   const char *setting_key)
{
	const char *value;
	gint64 v;

	nm_assert (tag_key && tag_key[0]);
	nm_assert (NM_IS_SETTING_VPN (s_vpn));
	nm_assert (setting_key && setting_key[0]);

	value = nm_setting_vpn_get_data_item (s_vpn, setting_key);
	if (!nmovpn_arg_is_set (value))
		return;

	v = _nm_utils_ascii_str_to_int64 (value, 10, G_MININT64, G_MAXINT64, 0);
	if (errno)
		return;
	args_write_line_int64 (f, tag_key, v);
}

static void
args_write_line_setting_value (GString *f,
                               const char *tag_key,
                               NMSettingVpn *s_vpn,
                               const char *setting_key)
{
	const char *value;

	value = nm_setting_vpn_get_data_item (s_vpn, setting_key);
	if (nmovpn_arg_is_set (value))
		args_write_line (f, tag_key, value);
}

/*****************************************************************************/

static GString *
do_export_create (NMConnection *connection, const char *path, GError **error)
{
	NMSettingConnection *s_con;
	NMSettingIPConfig *s_ip4;
	NMSettingVpn *s_vpn;
	const char *value;
	const char *gateways;
	char **gw_list, **gw_iter;
	const char *connection_type;
	const char *local_ip = NULL;
	const char *remote_ip = NULL;
	const char *proxy_type = NULL;
	guint i, num;
	nm_auto(_auto_free_gstring_p) GString *f = NULL;

	if (!path || !path[0]) {
		g_set_error_literal (error,
		                     NMV_EDITOR_PLUGIN_ERROR,
		                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
		                     _("missing path argument"));
		return NULL;
	}

	s_con = nm_connection_get_setting_connection (connection);
	s_vpn = nm_connection_get_setting_vpn (connection);
	if (!s_con || !s_vpn) {
		g_set_error_literal (error,
		                     NMV_EDITOR_PLUGIN_ERROR,
		                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
		                     _("connection is not a valid OpenVPN connection"));
		return NULL;
	}

	gateways = nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE));
	if (!gateways) {
		g_set_error_literal (error,
		                     NMV_EDITOR_PLUGIN_ERROR,
		                     NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
		                     _("connection was incomplete (missing gateway)"));
		return NULL;
	}

	connection_type = nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CONNECTION_TYPE));

	f = g_string_sized_new (512);

	if (NM_IN_STRSET (connection_type, NM_OPENVPN_CONTYPE_TLS,
	                                   NM_OPENVPN_CONTYPE_PASSWORD,
	                                   NM_OPENVPN_CONTYPE_PASSWORD_TLS))
		args_write_line (f, NMV_OVPN_TAG_CLIENT);

	/* 'remote' */
	gw_list = g_strsplit_set (gateways, " \t,", 0);
	for (gw_iter = gw_list; gw_iter && *gw_iter; gw_iter++) {
		gs_free char *str_free = NULL;
		const char *host, *port, *proto;
		gssize eidx;

		eidx = nmovpn_remote_parse (*gw_iter,
		                            &str_free,
		                            &host,
		                            &port,
		                            &proto,
		                            NULL);
		if (eidx >= 0)
			continue;
		args_write_line (f,
		                 NMV_OVPN_TAG_REMOTE,
		                 host,
		                 port ?: (proto ? "1194" : NULL),
		                 proto);
	}
	g_strfreev (gw_list);

	if (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_RANDOM), "yes"))
		args_write_line (f, NMV_OVPN_TAG_REMOTE_RANDOM);

	if (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_RANDOM_HOSTNAME), "yes"))
		args_write_line (f, NMV_OVPN_TAG_REMOTE_RANDOM_HOSTNAME);

	if (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_ALLOW_PULL_FQDN), "yes"))
		args_write_line (f, NMV_OVPN_TAG_ALLOW_PULL_FQDN);

	if (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TUN_IPV6), "yes"))
		args_write_line (f, NMV_OVPN_TAG_TUN_IPV6);

	if (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PUSH_PEER_INFO), "yes"))
		args_write_line (f, NMV_OVPN_TAG_PUSH_PEER_INFO);

	{
		gs_free char *cacert_free = NULL, *user_cert_free = NULL, *private_key_free = NULL;
		const char *cacert = NULL, *user_cert = NULL, *private_key = NULL;

		if (NM_IN_STRSET (connection_type, NM_OPENVPN_CONTYPE_TLS,
		                                   NM_OPENVPN_CONTYPE_PASSWORD,
		                                   NM_OPENVPN_CONTYPE_PASSWORD_TLS)) {
			value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CA);
			if (nmovpn_arg_is_set (value))
				cacert = nm_utils_str_utf8safe_unescape (value, &cacert_free);
		}

		if (NM_IN_STRSET (connection_type, NM_OPENVPN_CONTYPE_TLS,
		                                   NM_OPENVPN_CONTYPE_PASSWORD_TLS)) {
			value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CERT);
			if (nmovpn_arg_is_set (value))
				user_cert = nm_utils_str_utf8safe_unescape (value, &user_cert_free);

			value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_KEY);
			if (nmovpn_arg_is_set (value))
				private_key = nm_utils_str_utf8safe_unescape (value, &private_key_free);
		}

		if (user_cert && private_key) {
			if (   nm_streq (user_cert, private_key)
			    && is_pkcs12 (user_cert))
				args_write_line (f, NMV_OVPN_TAG_PKCS12, user_cert);
			else {
				args_write_line (f, NMV_OVPN_TAG_CERT, user_cert);
				args_write_line (f, NMV_OVPN_TAG_KEY, private_key);
			}
		}

		/* The CA can be included in a PKCS#12 file above, or a separate file (PEM format).
		 * If it's pkcs12 we can't use --ca as it only accepts PEM format. */
		if (   cacert
		    && !is_pkcs12 (cacert))
			args_write_line (f, NMV_OVPN_TAG_CA, cacert);
	}

	if (NM_IN_STRSET (connection_type, NM_OPENVPN_CONTYPE_PASSWORD,
	                                   NM_OPENVPN_CONTYPE_PASSWORD_TLS))
		args_write_line (f, NMV_OVPN_TAG_AUTH_USER_PASS);

	if (NM_IN_STRSET (connection_type, NM_OPENVPN_CONTYPE_STATIC_KEY)) {
		value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_STATIC_KEY);
		if (nmovpn_arg_is_set (value)) {
			gs_free char *s_free = NULL;

			args_write_line (f,
			                 NMV_OVPN_TAG_SECRET,
			                 nm_utils_str_utf8safe_unescape (value, &s_free),
			                 nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_STATIC_KEY_DIRECTION)));
		}
	}

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_RENEG_SEC, s_vpn, NM_OPENVPN_KEY_RENEG_SECONDS);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_MAX_ROUTES, s_vpn, NM_OPENVPN_KEY_MAX_ROUTES);

	args_write_line_setting_value (f, NMV_OVPN_TAG_CIPHER, s_vpn, NM_OPENVPN_KEY_CIPHER);

	args_write_line_setting_value (f, NMV_OVPN_TAG_TLS_CIPHER, s_vpn, NM_OPENVPN_KEY_TLS_CIPHER);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_KEYSIZE, s_vpn, NM_OPENVPN_KEY_KEYSIZE);

	args_write_line_setting_value (f, NMV_OVPN_TAG_ALLOW_COMPRESSION, s_vpn, NM_OPENVPN_KEY_ALLOW_COMPRESSION);

	value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_ALLOW_COMPRESSION);
	if (!nm_streq0 (value, "no")) {
		value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_COMP_LZO);
		if (value) {
			if (nm_streq (value, "no-by-default"))
				value = "no";
			args_write_line (f, NMV_OVPN_TAG_COMP_LZO, value);
		}

		value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_COMPRESS);
		if (nm_streq0 (value, "yes"))
			args_write_line (f, NMV_OVPN_TAG_COMPRESS);
		else if (value)
			args_write_line_setting_value (f, NMV_OVPN_TAG_COMPRESS, s_vpn, NM_OPENVPN_KEY_COMPRESS);
	}

	if (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_FLOAT), "yes"))
		args_write_line (f, NMV_OVPN_TAG_FLOAT);

	value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_MSSFIX);
	if (nm_streq0 (value, "yes"))
		args_write_line (f, NMV_OVPN_TAG_MSSFIX);
	else if (value)
		args_write_line_setting_value_int (f, NMV_OVPN_TAG_MSSFIX, s_vpn, NM_OPENVPN_KEY_MSSFIX);

	args_write_line_setting_value (f, NMV_OVPN_TAG_MTU_DISC, s_vpn, NM_OPENVPN_KEY_MTU_DISC);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_TUN_MTU, s_vpn, NM_OPENVPN_KEY_TUNNEL_MTU);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_CONNECT_TIMEOUT, s_vpn, NM_OPENVPN_KEY_CONNECT_TIMEOUT);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_FRAGMENT, s_vpn, NM_OPENVPN_KEY_FRAGMENT_SIZE);

	value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CRL_VERIFY_FILE);
	if (value)
		args_write_line (f, NMV_OVPN_TAG_CRL_VERIFY, value);
	else {
		value = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_CRL_VERIFY_DIR);
		if (value)
			args_write_line (f, NMV_OVPN_TAG_CRL_VERIFY, value, "dir");
	}

	{
		gs_free char *device_free = NULL;
		const char *device_type, *device;

		device_type = nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_DEV_TYPE));
		device = nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_DEV));
		device = nm_utils_str_utf8safe_unescape (device, &device_free);
		args_write_line (f,
		                 NMV_OVPN_TAG_DEV,
		                 device ?:
		                     (device_type ?:
		                         (nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TAP_DEV), "yes")
		                             ? "tap" : "tun")));
		if (device_type)
			args_write_line (f, NMV_OVPN_TAG_DEV_TYPE, device_type);
	}

	args_write_line (f,
	                 NMV_OVPN_TAG_PROTO,
	                 nm_streq0 (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROTO_TCP), "yes")
	                     ? "tcp" : "udp");

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_PORT, s_vpn, NM_OPENVPN_KEY_PORT);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_PING, s_vpn, NM_OPENVPN_KEY_PING);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_PING_EXIT, s_vpn, NM_OPENVPN_KEY_PING_EXIT);

	args_write_line_setting_value_int (f, NMV_OVPN_TAG_PING_RESTART, s_vpn, NM_OPENVPN_KEY_PING_RESTART);

	local_ip = nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_LOCAL_IP));
	remote_ip = nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_REMOTE_IP));
	if (local_ip && remote_ip)
		args_write_line (f, NMV_OVPN_TAG_IFCONFIG, local_ip, remote_ip);

	if (NM_IN_STRSET (connection_type,
	                  NM_OPENVPN_CONTYPE_TLS,
	                  NM_OPENVPN_CONTYPE_PASSWORD_TLS)) {
		const char *x509_name, *key;

		args_write_line_setting_value (f, NMV_OVPN_TAG_REMOTE_CERT_TLS, s_vpn, NM_OPENVPN_KEY_REMOTE_CERT_TLS);
		args_write_line_setting_value (f, NMV_OVPN_TAG_NS_CERT_TYPE, s_vpn, NM_OPENVPN_KEY_NS_CERT_TYPE);
		args_write_line_setting_value (f, NMV_OVPN_TAG_TLS_REMOTE, s_vpn, NM_OPENVPN_KEY_TLS_REMOTE);

		x509_name =  nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_VERIFY_X509_NAME);
		if (nmovpn_arg_is_set (x509_name)) {
			const char *name;
			gs_free char *type = NULL;

			name = strchr (x509_name, ':');
			if (name) {
				type = g_strndup (x509_name, name - x509_name);
				name++;
			} else
				name = x509_name;

			args_write_line (f, NMV_OVPN_TAG_VERIFY_X509_NAME, name, type);
		}

		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TA);
		if (nmovpn_arg_is_set (key)) {
			gs_free char *s_free = NULL;
			args_write_line (f,
			                 NMV_OVPN_TAG_TLS_AUTH,
			                 nm_utils_str_utf8safe_unescape (key, &s_free),
			                 nmovpn_arg_is_set (nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TA_DIR)));
		}

		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TLS_CRYPT);
		if (nmovpn_arg_is_set (key)) {
			gs_free char *s_free = NULL;
			args_write_line (f,
			                 NMV_OVPN_TAG_TLS_CRYPT,
			                 nm_utils_str_utf8safe_unescape (key, &s_free));
		}

		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TLS_CRYPT_V2);
		if (nmovpn_arg_is_set (key)) {
			gs_free char *s_free = NULL;
			args_write_line (f,
			                 NMV_OVPN_TAG_TLS_CRYPT_V2,
			                 nm_utils_str_utf8safe_unescape (key, &s_free));
		}

		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TLS_VERSION_MIN);
		if (nmovpn_arg_is_set (key)) {
			const char *or_highest = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TLS_VERSION_MIN_OR_HIGHEST);

			args_write_line (f, NMV_OVPN_TAG_TLS_VERSION_MIN, key,
			                 nm_streq0 (or_highest, "yes") ? "or-highest" : NULL);
		}

		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_TLS_VERSION_MAX);
		if (nmovpn_arg_is_set (key))
			args_write_line (f, NMV_OVPN_TAG_TLS_VERSION_MAX, key);

		key = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_EXTRA_CERTS);
		if (nmovpn_arg_is_set (key)) {
			gs_free char *s_free = NULL;
			args_write_line (f,
			                 NMV_OVPN_TAG_EXTRA_CERTS,
			                 nm_utils_str_utf8safe_unescape (key, &s_free));
		}
	}

	proxy_type = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_TYPE);
	if (nmovpn_arg_is_set (proxy_type)) {
		const char *proxy_server;
		const char *proxy_port;
		const char *proxy_retry;
		const char *proxy_username;
		const char *proxy_password;

		proxy_server = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_SERVER);
		proxy_port = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_PORT);
		proxy_retry = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_PROXY_RETRY);
		proxy_username = nm_setting_vpn_get_data_item (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_USERNAME);
		proxy_password = nm_setting_vpn_get_secret (s_vpn, NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD);

		if (nm_streq (proxy_type, "http") && proxy_server) {
			char *authfile, *authcontents, *base, *dirname;

			if (!proxy_port)
				proxy_port = "8080";

			/* If there's a username, need to write an authfile */
			base = g_path_get_basename (path);
			dirname = g_path_get_dirname (path);
			authfile = g_strdup_printf ("%s/%s-httpauthfile", dirname, base);
			g_free (base);
			g_free (dirname);

			args_write_line (f,
			                 NMV_OVPN_TAG_HTTP_PROXY,
			                 proxy_server,
			                 proxy_port,
			                 proxy_username ? authfile : NULL);
			if (proxy_retry && !strcmp (proxy_retry, "yes"))
				args_write_line (f, NMV_OVPN_TAG_HTTP_PROXY_RETRY);

			/* Write out the authfile */
			if (proxy_username) {
				authcontents = g_strdup_printf ("%s\n%s\n",
				                                proxy_username,
				                                proxy_password ? proxy_password : "");
				g_file_set_contents (authfile, authcontents, -1, NULL);
				g_free (authcontents);
			}
			g_free (authfile);
		} else if (nm_streq (proxy_type, "socks") && proxy_server) {
			if (!proxy_port)
				proxy_port = "1080";
			args_write_line (f, NMV_OVPN_TAG_SOCKS_PROXY, proxy_server, proxy_port);
			if (proxy_retry && !strcmp (proxy_retry, "yes"))
				args_write_line (f, NMV_OVPN_TAG_SOCKS_PROXY_RETRY);
		}
	}

	s_ip4 = nm_connection_get_setting_ip4_config (connection);
	if (s_ip4) {
		num = nm_setting_ip_config_get_num_routes (s_ip4);
		for (i = 0; i < num; i++) {
			char netmask_str[INET_ADDRSTRLEN] = { 0 };
			const char *next_hop_str, *dest_str;
			in_addr_t netmask;
			guint prefix;
			guint64 metric;
			char metric_buf[50];
			NMIPRoute *route;

			route = nm_setting_ip_config_get_route (s_ip4, i);
			dest_str = nm_ip_route_get_dest (route);
			next_hop_str = nm_ip_route_get_next_hop (route) ? : "0.0.0.0",
			prefix = nm_ip_route_get_prefix (route);
			metric = nm_ip_route_get_metric (route);
			netmask = nm_utils_ip4_prefix_to_netmask (prefix);
			inet_ntop (AF_INET, (const void *) &netmask, netmask_str, sizeof (netmask_str));

			args_write_line (f,
			                 NMV_OVPN_TAG_ROUTE,
			                 dest_str,
			                 netmask_str,
			                 next_hop_str,
			                 metric == -1 ? NULL : nm_sprintf_buf (metric_buf, "%u", (unsigned) metric));
		}
	}

	/* Add hard-coded stuff */
	args_write_line (f, NMV_OVPN_TAG_NOBIND);
	args_write_line (f, NMV_OVPN_TAG_AUTH_NOCACHE);
	args_write_line (f, NMV_OVPN_TAG_SCRIPT_SECURITY, "2");
	args_write_line (f, NMV_OVPN_TAG_PERSIST_KEY);
	args_write_line (f, NMV_OVPN_TAG_PERSIST_TUN);
	args_write_line (f, NMV_OVPN_TAG_USER, NM_OPENVPN_USER);
	args_write_line (f, NMV_OVPN_TAG_GROUP, NM_OPENVPN_GROUP);

	return g_steal_pointer (&f);
}

gboolean
do_export (const char *path, NMConnection *connection, GError **error)
{
	nm_auto(_auto_free_gstring_p) GString *f = NULL;
	gs_free_error GError *local = NULL;

	f = do_export_create (connection, path, error);
	if (!f)
		return FALSE;

	if (!g_file_set_contents (path, f->str, f->len, &local)) {
		g_set_error (error,
		             NMV_EDITOR_PLUGIN_ERROR,
		             NMV_EDITOR_PLUGIN_ERROR_FILE_NOT_VPN,
		             _("failed to write file: %s"),
		             local->message);
		return FALSE;
	}

	return TRUE;
}
