X-Git-Url: http://review.tizen.org/git/?a=blobdiff_plain;ds=sidebyside;f=vpn%2Fplugins%2Fopenconnect.c;h=d600e61e4b7db80344720606351c079672303d97;hb=c647a4b6f1132684c9d8b8ad71ec38d81147b278;hp=5feaed9d5346cf1301c953308529e1c37f490bc7;hpb=9c99015013fcf0abde65abaf2203dde64c07a962;p=platform%2Fupstream%2Fconnman.git diff --git a/vpn/plugins/openconnect.c b/vpn/plugins/openconnect.c index 5feaed9..d600e61 100755 --- a/vpn/plugins/openconnect.c +++ b/vpn/plugins/openconnect.c @@ -3,6 +3,7 @@ * ConnMan VPN daemon * * Copyright (C) 2007-2013 Intel Corporation. All rights reserved. + * Copyright (C) 2019 Jolla Ltd. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as @@ -47,25 +48,137 @@ #include "vpn.h" #define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) +#define OC_MAX_READBUF_LEN 128 + +enum opt_type { + OPT_STRING = 0, + OPT_BOOL = 1, +}; struct { - const char *cm_opt; - const char *oc_opt; - char has_value; + const char *cm_opt; + const char *oc_opt; + bool has_value; + bool enabled; // Use as task parameter + enum opt_type type; } oc_options[] = { - { "OpenConnect.NoCertCheck", "--no-cert-check", 0 }, + { "OpenConnect.AllowSelfSignedCert", NULL, 1, 0, OPT_BOOL}, + { "OpenConnect.AuthType", NULL, 1, 0, OPT_STRING}, + { "OpenConnect.CACert", "--cafile", 1, 1, OPT_STRING}, + { "OpenConnect.ClientCert", NULL, 1, 0, OPT_STRING}, + { "OpenConnect.DisableIPv6", "--disable-ipv6", 1, 1, OPT_BOOL}, + { "OpenConnect.PKCSClientCert", NULL, 1, 0, OPT_STRING}, + { "OpenConnect.Protocol", "--protocol", 1, 1, OPT_STRING}, + /* --no-cert-check is disabled in openconnect 8.02 */ + { "OpenConnect.NoCertCheck", "--no-cert-check", 0, 0, OPT_BOOL}, + { "OpenConnect.NoHTTPKeepalive", "--no-http-keepalive", 1, 1, OPT_BOOL}, + { "OpenConnect.NoDTLS", "--no-dtls", 1, 1, OPT_BOOL}, + { "OpenConnect.ServerCert", "--servercert", 1, 1, OPT_STRING}, + { "OpenConnect.Usergroup", "--usergroup", 1, 1, OPT_STRING}, + { "OpenConnect.UserPrivateKey", NULL, 1, 0, OPT_STRING}, + { "VPN.MTU", "--base-mtu", 1, 1, OPT_STRING}, +}; + +enum oc_connect_type { + OC_CONNECT_COOKIE = 0, + OC_CONNECT_COOKIE_WITH_USERPASS, + OC_CONNECT_USERPASS, + OC_CONNECT_PUBLICKEY, + OC_CONNECT_PKCS, }; +static const char *connect_types[] = {"cookie", "cookie_with_userpass", + "userpass", "publickey", "pkcs", NULL}; +static const char *protocols[] = { "anyconnect", "nc", "gp", NULL}; + struct oc_private_data { struct vpn_provider *provider; struct connman_task *task; char *if_name; + char *dbus_sender; vpn_provider_connect_cb_t cb; void *user_data; + int fd_in; + int out_ch_id; + int err_ch_id; + GIOChannel *out_ch; + GIOChannel *err_ch; + enum oc_connect_type connect_type; + bool interactive; }; +static bool is_valid_protocol(const char* protocol) +{ + if (!protocol || !*protocol) + return false; + + return g_strv_contains(protocols, protocol); +} + +static void oc_connect_done(struct oc_private_data *data, int err) +{ + connman_info("data %p err %d/%s", data, err, strerror(err)); + + if (data && data->cb) { + vpn_provider_connect_cb_t cb = data->cb; + void *user_data = data->user_data; + + /* Make sure we don't invoke this callback twice */ + data->cb = NULL; + data->user_data = NULL; + cb(data->provider, user_data, err); + } +} + +static void close_io_channel(struct oc_private_data *data, GIOChannel *channel) +{ + int id = 0; + + connman_info("data %p channel %p", data, channel); + + if (!data || !channel) + return; + + if (data->out_ch == channel) { + id = data->out_ch_id; + data->out_ch = NULL; + data->out_ch_id = 0; + } else if (data->err_ch == channel) { + id = data->err_ch_id; + data->err_ch = NULL; + data->err_ch_id = 0; + } else { + return; + } + + if (id) + g_source_remove(id); + + g_io_channel_shutdown(channel, FALSE, NULL); + g_io_channel_unref(channel); +} + static void free_private_data(struct oc_private_data *data) { + connman_info("data %p", data); + + if (!data || !data->provider) + return; + + connman_info("provider %p", data->provider); + + if (vpn_provider_get_plugin_data(data->provider) == data) + vpn_provider_set_plugin_data(data->provider, NULL); + + vpn_provider_unref(data->provider); + + if (data->fd_in > 0) + close(data->fd_in); + data->fd_in = -1; + close_io_channel(data, data->out_ch); + close_io_channel(data, data->err_ch); + + g_free(data->dbus_sender); g_free(data->if_name); g_free(data); } @@ -73,17 +186,51 @@ static void free_private_data(struct oc_private_data *data) static int task_append_config_data(struct vpn_provider *provider, struct connman_task *task) { - const char *option; + const char *option = NULL; int i; for (i = 0; i < (int)ARRAY_SIZE(oc_options); i++) { - if (!oc_options[i].oc_opt) + if (!oc_options[i].oc_opt || !oc_options[i].enabled) continue; - option = vpn_provider_get_string(provider, - oc_options[i].cm_opt); - if (!option) - continue; + if (oc_options[i].has_value) { + option = vpn_provider_get_string(provider, + oc_options[i].cm_opt); + if (!option) + continue; + + /* Add boolean type values only if set as true. */ + if (oc_options[i].type == OPT_BOOL) { + if (!vpn_provider_get_boolean(provider, + oc_options[i].cm_opt, + false)) + continue; + + /* No option is set for boolean type values. */ + option = NULL; + } + + /* Skip protocol if it is invalid. */ + if (!g_strcmp0(oc_options[i].cm_opt, + "OpenConnect.Protocol")) { + if (!is_valid_protocol(option)) + continue; + } + } + + /* + * Add server certificate fingerprint only when self signed + * certificates are explicitly allowed. Using --servercert as + * parameter will accept any server with matching fingerprint, + * which would disregard the setting of AllowSelfSignedCert. + */ + if (!g_strcmp0(oc_options[i].cm_opt, + "OpenConnect.ServerCert")) { + if (!vpn_provider_get_boolean(provider, + "OpenConnect.AllowSelfSignedCert", + false)) + continue; + } if (connman_task_add_argument(task, oc_options[i].oc_opt, @@ -103,6 +250,11 @@ static int oc_notify(DBusMessage *msg, struct vpn_provider *provider) char *netmask = NULL, *gateway = NULL; unsigned char prefix_len = 0; struct connman_ipaddress *ipaddress; + struct oc_private_data *data; + + connman_info("provider %p", provider); + + data = vpn_provider_get_plugin_data(provider); dbus_message_iter_init(msg, &iter); @@ -111,6 +263,7 @@ static int oc_notify(DBusMessage *msg, struct vpn_provider *provider) if (!provider) { connman_error("No provider found"); + oc_connect_done(data, ENOENT); return VPN_STATE_FAILURE; } @@ -214,110 +367,770 @@ static int oc_notify(DBusMessage *msg, struct vpn_provider *provider) g_free(domain); connman_ipaddress_free(ipaddress); + oc_connect_done(data, 0); return VPN_STATE_CONNECT; } -static int run_connect(struct vpn_provider *provider, - struct connman_task *task, const char *if_name, - vpn_provider_connect_cb_t cb, void *user_data) +static ssize_t full_write(int fd, const void *buf, size_t len) { - const char *vpnhost, *vpncookie, *servercert, *mtu; - int fd, err = 0, len; + ssize_t byte_write; + + while (len) { + byte_write = write(fd, buf, len); + if (byte_write < 0) { + connman_error("failed to write config to openconnect: " + " %s\n", strerror(errno)); + return byte_write; + } + len -= byte_write; + buf += byte_write; + } - vpnhost = vpn_provider_get_string(provider, "OpenConnect.VPNHost"); - if (!vpnhost) - vpnhost = vpn_provider_get_string(provider, "Host"); - vpncookie = vpn_provider_get_string(provider, "OpenConnect.Cookie"); - servercert = vpn_provider_get_string(provider, - "OpenConnect.ServerCert"); + return len; +} - if (!vpncookie || !servercert) { - err = -EINVAL; - goto done; +static ssize_t write_data(int fd, const char *data) +{ + gchar *buf; + ssize_t len; + + if (!data || !*data) + return -1; + + buf = g_strdup_printf("%s\n", data); + + len = full_write(fd, buf, strlen(buf)); + + g_free(buf); + + return len; +} + +static void oc_died(struct connman_task *task, int exit_code, void *user_data) +{ + struct oc_private_data *data = user_data; + + connman_info("task %p data %p exit_code %d user_data %p", task, data, + exit_code, user_data); + + if (!data) + return; + + if (data->provider) { + connman_agent_cancel(data->provider); + + if (task) + vpn_died(task, exit_code, data->provider); } - task_append_config_data(provider, task); + free_private_data(data); +} + +static gboolean io_channel_out_cb(GIOChannel *source, GIOCondition condition, + gpointer user_data) +{ + struct oc_private_data *data; + char *str; + + data = user_data; + + if (data->out_ch != source) + return G_SOURCE_REMOVE; + + if ((condition & G_IO_IN) && + g_io_channel_read_line(source, &str, NULL, NULL, NULL) == + G_IO_STATUS_NORMAL) { + + g_strchomp(str); + + /* Only cookie is printed to stdout */ + vpn_provider_set_string_hide_value(data->provider, + "OpenConnect.Cookie", str); + + g_free(str); + } else if (condition & (G_IO_ERR | G_IO_HUP)) { + connman_info("Out channel termination"); + close_io_channel(data, source); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static bool strv_contains_prefix(const char *strv[], const char *str) +{ + int i; + + if (!strv || !str || !*str) + return false; + + for (i = 0; strv[i]; i++) { + if (g_str_has_prefix(str, strv[i])) + return true; + } + + return false; +} + +static void clear_provider_credentials(struct vpn_provider *provider) +{ + const char *keys[] = { "OpenConnect.Username", + "OpenConnect.Password", + "OpenConnect.PKCSPassword", + "OpenConnect.Cookie", + NULL + }; + int i; + + connman_info("provider %p", provider); + + for (i = 0; keys[i]; i++) { + if (!vpn_provider_get_string_immutable(provider, keys[i])) + vpn_provider_set_string_hide_value(provider, keys[i], + "-"); + } +} + +typedef void (* request_input_reply_cb_t) (DBusMessage *reply, + void *user_data); + +static int request_input_credentials(struct oc_private_data *data, + request_input_reply_cb_t cb); + + +static void request_input_pkcs_reply(DBusMessage *reply, void *user_data) +{ + struct oc_private_data *data = user_data; + const char *password = NULL; + const char *key; + DBusMessageIter iter, dict; + int err; + + connman_info("provider %p", data->provider); + + if (!reply) + goto err; + + err = vpn_agent_check_and_process_reply_error(reply, data->provider, + data->task, data->cb, data->user_data); + if (err) { + /* Ensure cb is called only once */ + data->cb = NULL; + data->user_data = NULL; + goto err; + } + + if (!vpn_agent_check_reply_has_dict(reply)) + goto err; - connman_task_add_argument(task, "--servercert", servercert); + dbus_message_iter_init(reply, &iter); + dbus_message_iter_recurse(&iter, &dict); + while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter entry, value; - mtu = vpn_provider_get_string(provider, "VPN.MTU"); + dbus_message_iter_recurse(&dict, &entry); + if (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_STRING) + break; - if (mtu) - connman_task_add_argument(task, "--mtu", (char *)mtu); + dbus_message_iter_get_basic(&entry, &key); - connman_task_add_argument(task, "--syslog", NULL); - connman_task_add_argument(task, "--cookie-on-stdin", NULL); + if (g_str_equal(key, "OpenConnect.PKCSPassword")) { + dbus_message_iter_next(&entry); + if (dbus_message_iter_get_arg_type(&entry) + != DBUS_TYPE_VARIANT) + break; + dbus_message_iter_recurse(&entry, &value); + if (dbus_message_iter_get_arg_type(&value) + != DBUS_TYPE_STRING) + break; + dbus_message_iter_get_basic(&value, &password); + vpn_provider_set_string_hide_value(data->provider, key, + password); + } - connman_task_add_argument(task, "--script", - SCRIPTDIR "/openconnect-script"); + dbus_message_iter_next(&dict); + } - connman_task_add_argument(task, "--interface", if_name); + if (data->connect_type != OC_CONNECT_PKCS || !password) + goto err; + + if (write_data(data->fd_in, password) != 0) { + connman_error("openconnect failed to take PKCS pass phrase on" + " stdin"); + goto err; + } + + clear_provider_credentials(data->provider); + + return; +err: + oc_connect_done(data, EACCES); +} + +static gboolean io_channel_err_cb(GIOChannel *source, GIOCondition condition, + gpointer user_data) +{ + struct oc_private_data *data; + const char *auth_failures[] = { + /* Login failed */ + "Got HTTP response: HTTP/1.1 401 Unauthorized", + "Failed to obtain WebVPN cookie", + /* Cookie not valid */ + "Got inappropriate HTTP CONNECT response: " + "HTTP/1.1 401 Unauthorized", + /* Invalid cookie */ + "VPN service unavailable", + /* Problem with certificates */ + "SSL connection failure", + "Creating SSL connection failed", + "SSL connection cancelled", + NULL + }; + const char *conn_failures[] = { + "Failed to connect to", + "Failed to open HTTPS connection to", + NULL + }; + /* Handle both PKCS#12 and PKCS#8 failures */ + const char *pkcs_failures[] = { + "Failed to decrypt PKCS#12 certificate file", + "Failed to decrypt PKCS#8 certificate file", + NULL + }; + /* Handle both PKCS#12 and PKCS#8 requests */ + const char *pkcs_requests[] = { + "Enter PKCS#12 pass phrase", + "Enter PKCS#8 pass phrase", + NULL + }; + const char *server_key_hash = " --servercert"; + char *str; + bool close = false; + int err = 0; + + data = user_data; + + if (!data) + return G_SOURCE_REMOVE; + + if (source && data->err_ch != source) + return G_SOURCE_REMOVE; + + if ((condition & G_IO_IN)) { + gsize len; + int pos; + + if (!data->interactive) { + if (g_io_channel_read_line(source, &str, &len, NULL, + NULL) != G_IO_STATUS_NORMAL) + err = EIO; + else + str[len - 1] = '\0'; + } else { + GIOStatus status; + str = g_try_new0(char, OC_MAX_READBUF_LEN); + if (!str) + return G_SOURCE_REMOVE; + + for (pos = 0; pos < OC_MAX_READBUF_LEN - 1 ; ++pos) { + status = g_io_channel_read_chars(source, + str+pos, 1, &len, NULL); + + if (status == G_IO_STATUS_EOF) { + break; + } else if (status != G_IO_STATUS_NORMAL) { + err = EIO; + break; + } + + /* Ignore control chars and digits at start */ + if (!pos && (g_ascii_iscntrl(str[pos]) || + g_ascii_isdigit(str[pos]))) + --pos; + + /* Read zero length or no more to read */ + if (!len || g_io_channel_get_buffer_condition( + source) != G_IO_IN || + str[pos] == '\n') + break; + } + + /* + * When self signed certificates are allowed and server + * SHA1 fingerprint is printed to stderr there is a + * newline char at the end of SHA1 fingerprint. + */ + if (str[pos] == '\n') + str[pos] = '\0'; + } + + connman_info("openconnect: %s", str); + + if (err || !str || !*str) { + connman_info("error reading from openconnect"); + } else if (g_str_has_prefix(str, server_key_hash)) { + const char *fingerprint; + int position; + bool allow_self_signed; + + allow_self_signed = vpn_provider_get_boolean( + data->provider, + "OpenConnect.AllowSelfSignedCert", + false); + + if (allow_self_signed) { + position = strlen(server_key_hash) + 1; + fingerprint = g_strstrip(str + position); + + connman_info("Set server key hash: \"%s\"", + fingerprint); + + vpn_provider_set_string(data->provider, + "OpenConnect.ServerCert", + fingerprint); + + /* + * OpenConnect waits for "yes" or "no" as + * response to certificate acceptance request. + */ + if (write_data(data->fd_in, "yes") != 0) + connman_error("openconnect: cannot " + "write answer to certificate " + "accept request"); + + } else { + connman_warn("Self signed certificate is not " + " allowed"); + + /* + * Close IO channel to avoid deadlock as an + * answer is expected for the certificate + * accept request. + */ + close = true; + err = ECONNREFUSED; + } + } else if (strv_contains_prefix(pkcs_failures, str)) { + connman_warn("PKCS failure: %s", str); + close = true; + err = EACCES; + } else if (strv_contains_prefix(pkcs_requests, str)) { + connman_info("PKCS file pass phrase request: %s", str); + err = request_input_credentials(data, + request_input_pkcs_reply); + + if (err != -EINPROGRESS) { + err = EACCES; + close = true; + } else { + err = 0; + } + } else if (strv_contains_prefix(auth_failures, str)) { + connman_warn("authentication failed: %s", str); + err = EACCES; + } else if (strv_contains_prefix(conn_failures, str)) { + connman_warn("connection failed: %s", str); + err = ECONNREFUSED; + } + + g_free(str); + } else if (condition & (G_IO_ERR | G_IO_HUP)) { + connman_info("Err channel termination"); + close = true; + } + + if (err) { + switch (err) { + case EACCES: + clear_provider_credentials(data->provider); + break; + case ECONNREFUSED: + /* + * This will trigger VPN_PROVIDER_ERROR_CONNECT_FAILED + * in vpn-provider.c:connect_cb(). + */ + default: + break; + } + + oc_connect_done(data, err); + } + + if (close) { + close_io_channel(data, source); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static int run_connect(struct oc_private_data *data) +{ + struct vpn_provider *provider; + struct connman_task *task; + const char *vpnhost; + const char *vpncookie = NULL; + const char *username; + const char *password = NULL; + const char *certificate = NULL; + const char *private_key; + const char *setting_str; + bool setting; + bool use_stdout = false; + int fd_out = -1; + int fd_err; + int err = 0; + + if (!data) + return -EINVAL; + + provider = data->provider; + task = data->task; + + connman_info("provider %p task %p", provider, task); + + switch (data->connect_type) { + case OC_CONNECT_COOKIE: + vpncookie = vpn_provider_get_string(provider, + "OpenConnect.Cookie"); + if (!vpncookie || !g_strcmp0(vpncookie, "-")) { + err = -EACCES; + goto done; + } + + connman_task_add_argument(task, "--cookie-on-stdin", NULL); + break; + case OC_CONNECT_COOKIE_WITH_USERPASS: + vpncookie = vpn_provider_get_string(provider, + "OpenConnect.Cookie"); + /* No cookie set yet, username and password used first */ + if (!vpncookie || !g_strcmp0(vpncookie, "-")) { + username = vpn_provider_get_string(provider, + "OpenConnect.Username"); + password = vpn_provider_get_string(provider, + "OpenConnect.Password"); + if (!username || !password || + !g_strcmp0(username, "-") || + !g_strcmp0(password, "-")) { + err = -EACCES; + goto done; + } + + connman_task_add_argument(task, "--cookieonly", NULL); + connman_task_add_argument(task, "--user", username); + connman_task_add_argument(task, "--passwd-on-stdin", + NULL); + + /* Use stdout only when cookie is to be read. */ + use_stdout = true; + } else { + connman_task_add_argument(task, "--cookie-on-stdin", + NULL); + } + + break; + case OC_CONNECT_USERPASS: + username = vpn_provider_get_string(provider, + "OpenConnect.Username"); + password = vpn_provider_get_string(provider, + "OpenConnect.Password"); + if (!username || !password || !g_strcmp0(username, "-") || + !g_strcmp0(password, "-")) { + err = -EACCES; + goto done; + } + + connman_task_add_argument(task, "--user", username); + connman_task_add_argument(task, "--passwd-on-stdin", NULL); + break; + case OC_CONNECT_PUBLICKEY: + certificate = vpn_provider_get_string(provider, + "OpenConnect.ClientCert"); + private_key = vpn_provider_get_string(provider, + "OpenConnect.UserPrivateKey"); + + if (!certificate || !private_key) { + err = -EACCES; + goto done; + } + + connman_task_add_argument(task, "--certificate", certificate); + connman_task_add_argument(task, "--sslkey", private_key); + break; + case OC_CONNECT_PKCS: + certificate = vpn_provider_get_string(provider, + "OpenConnect.PKCSClientCert"); + if (!certificate) { + err = -EACCES; + goto done; + } + + connman_task_add_argument(task, "--certificate", certificate); + + password = vpn_provider_get_string(data->provider, + "OpenConnect.PKCSPassword"); + /* Add password only if it is has been set */ + if (!password || !g_strcmp0(password, "-")) + break; + + connman_task_add_argument(task, "--passwd-on-stdin", NULL); + break; + } + + vpnhost = vpn_provider_get_string(provider, "OpenConnect.VPNHost"); + if (!vpnhost || !*vpnhost) + vpnhost = vpn_provider_get_string(provider, "Host"); + + task_append_config_data(provider, task); + + /* + * To clarify complex situation, if cookie is expected to be printed + * to stdout all other output must go to syslog. But with PKCS all + * output must be caught in order to get message about file decryption + * error. For this reason, the mode has to be interactive as well. + */ + switch (data->connect_type) { + case OC_CONNECT_COOKIE: + /* fall through */ + case OC_CONNECT_COOKIE_WITH_USERPASS: + /* fall through */ + case OC_CONNECT_USERPASS: + /* fall through */ + case OC_CONNECT_PUBLICKEY: + connman_task_add_argument(task, "--syslog", NULL); + + setting = vpn_provider_get_boolean(provider, + "OpenConnect.AllowSelfSignedCert", + false); + setting_str = vpn_provider_get_string(provider, + "OpenConnect.ServerCert"); + + /* + * Run in interactive mode if self signed certificates are + * allowed and there is no set server SHA1 fingerprint. + */ + if (setting_str || !setting) + connman_task_add_argument(task, "--non-inter", NULL); + else + data->interactive = true; + break; + case OC_CONNECT_PKCS: + data->interactive = true; + break; + } + + connman_task_add_argument(task, "--script", SCRIPTDIR "/vpn-script"); + + connman_task_add_argument(task, "--interface", data->if_name); connman_task_add_argument(task, (char *)vpnhost, NULL); - err = connman_task_run(task, vpn_died, provider, - &fd, NULL, NULL); + err = connman_task_run(task, oc_died, data, &data->fd_in, use_stdout ? + &fd_out : NULL, &fd_err); if (err < 0) { - connman_error("openconnect failed to start"); err = -EIO; goto done; } - len = strlen(vpncookie); - if (write(fd, vpncookie, len) != (ssize_t)len || - write(fd, "\n", 1) != 1) { - connman_error("openconnect failed to take cookie on stdin"); - err = -EIO; + switch (data->connect_type) { + case OC_CONNECT_COOKIE: + if (write_data(data->fd_in, vpncookie) != 0) { + connman_error("openconnect failed to take cookie on " + "stdin"); + err = -EIO; + } + + break; + case OC_CONNECT_USERPASS: + if (write_data(data->fd_in, password) != 0) { + connman_error("openconnect failed to take password on " + "stdin"); + err = -EIO; + } + + break; + case OC_CONNECT_COOKIE_WITH_USERPASS: + if (!vpncookie || !g_strcmp0(vpncookie, "-")) { + if (write_data(data->fd_in, password) != 0) { + connman_error("openconnect failed to take " + "password on stdin"); + err = -EIO; + } + } else { + if (write_data(data->fd_in, vpncookie) != 0) { + connman_error("openconnect failed to take " + "cookie on stdin"); + err = -EIO; + } + } + + break; + case OC_CONNECT_PUBLICKEY: + break; + case OC_CONNECT_PKCS: + if (!password || !g_strcmp0(password, "-")) + break; + + if (write_data(data->fd_in, password) != 0) { + connman_error("openconnect failed to take PKCS " + "pass phrase on stdin"); + err = -EIO; + } + + break; + } + + if (err) { + if (fd_out > 0) + close(fd_out); + + if (fd_err > 0) + close(fd_err); + goto done; } + err = -EINPROGRESS; + + if (use_stdout) { + data->out_ch = g_io_channel_unix_new(fd_out); + + /* Use ASCII encoding only */ + if (g_io_channel_set_encoding(data->out_ch, NULL, NULL) != + G_IO_STATUS_NORMAL) { + close_io_channel(data, data->out_ch); + err = -EIO; + } else { + data->out_ch_id = g_io_add_watch(data->out_ch, + G_IO_IN | G_IO_ERR | G_IO_HUP, + (GIOFunc)io_channel_out_cb, + data); + } + } + + data->err_ch = g_io_channel_unix_new(fd_err); + + /* Use ASCII encoding only */ + if (g_io_channel_set_encoding(data->err_ch, NULL, NULL) != + G_IO_STATUS_NORMAL) { + close_io_channel(data, data->err_ch); + err = -EIO; + } else { + data->err_ch_id = g_io_add_watch(data->err_ch, + G_IO_IN | G_IO_ERR | G_IO_HUP, + (GIOFunc)io_channel_err_cb, data); + } + done: - if (cb) - cb(provider, user_data, err); + clear_provider_credentials(data->provider); return err; } -static void request_input_append_informational(DBusMessageIter *iter, - void *user_data) +static void request_input_append(DBusMessageIter *iter, + const char *str_type, const char *str, void *user_data) { - const char *str; + const char *string; - str = "string"; - connman_dbus_dict_append_basic(iter, "Type", DBUS_TYPE_STRING, &str); + connman_dbus_dict_append_basic(iter, "Type", + DBUS_TYPE_STRING, &str_type); + connman_dbus_dict_append_basic(iter, "Requirement", + DBUS_TYPE_STRING, &str); + + if (!user_data) + return; - str = "informational"; - connman_dbus_dict_append_basic(iter, "Requirement", DBUS_TYPE_STRING, - &str); + string = user_data; + connman_dbus_dict_append_basic(iter, "Value", DBUS_TYPE_STRING, + &string); +} - str = user_data; - connman_dbus_dict_append_basic(iter, "Value", DBUS_TYPE_STRING, &str); +static void request_input_append_informational(DBusMessageIter *iter, + void *user_data) +{ + request_input_append(iter, "string", "informational", user_data); } static void request_input_append_mandatory(DBusMessageIter *iter, void *user_data) { - char *str = "string"; + request_input_append(iter, "string", "mandatory", user_data); +} - connman_dbus_dict_append_basic(iter, "Type", - DBUS_TYPE_STRING, &str); - str = "mandatory"; - connman_dbus_dict_append_basic(iter, "Requirement", - DBUS_TYPE_STRING, &str); +static void request_input_append_optional(DBusMessageIter *iter, + void *user_data) +{ + request_input_append(iter, "string", "optional", user_data); } -static void request_input_cookie_reply(DBusMessage *reply, void *user_data) +static void request_input_append_password(DBusMessageIter *iter, + void *user_data) +{ + request_input_append(iter, "password", "mandatory", user_data); +} + +static void request_input_append_to_dict(struct vpn_provider *provider, + DBusMessageIter *dict, + connman_dbus_append_cb_t function_cb, const char *key) +{ + const char *str; + bool immutable = false; + + if (!provider || !dict || !function_cb || !key) + return; + + str = vpn_provider_get_string(provider, key); + /* Ignore empty informational content */ + if (!str && function_cb == request_input_append_informational) + return; + + /* If value is "-", it is cleared by VPN agent */ + if (!g_strcmp0(str, "-")) + str = NULL; + + if (str) + immutable = vpn_provider_get_string_immutable(provider, key); + + if (immutable) { + /* Hide immutable password types */ + if (function_cb == request_input_append_password) + str = "********"; + + /* Send immutable as informational */ + function_cb = request_input_append_informational; + } + + connman_dbus_dict_append_dict(dict, key, function_cb, + str ? (void *)str : NULL); +} + +static void request_input_credentials_reply(DBusMessage *reply, void *user_data) { struct oc_private_data *data = user_data; - char *cookie = NULL, *servercert = NULL, *vpnhost = NULL; - char *key; + const char *cookie = NULL; + const char *servercert = NULL; + const char *vpnhost = NULL; + const char *username = NULL; + const char *password = NULL; + const char *pkcspassword = NULL; + const char *key; DBusMessageIter iter, dict; + int err; - DBG("provider %p", data->provider); + connman_info("provider %p", data->provider); - if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) + if (!reply) goto err; + err = vpn_agent_check_and_process_reply_error(reply, data->provider, + data->task, data->cb, data->user_data); + if (err) { + /* Ensure cb is called only once */ + data->cb = NULL; + data->user_data = NULL; + goto out; + } + if (!vpn_agent_check_reply_has_dict(reply)) goto err; @@ -344,7 +1157,6 @@ static void request_input_cookie_reply(DBusMessage *reply, void *user_data) dbus_message_iter_get_basic(&value, &cookie); vpn_provider_set_string_hide_value(data->provider, key, cookie); - } else if (g_str_equal(key, "OpenConnect.ServerCert")) { dbus_message_iter_next(&entry); if (dbus_message_iter_get_arg_type(&entry) @@ -369,43 +1181,103 @@ static void request_input_cookie_reply(DBusMessage *reply, void *user_data) break; dbus_message_iter_get_basic(&value, &vpnhost); vpn_provider_set_string(data->provider, key, vpnhost); + } else if (g_str_equal(key, "Username")) { + dbus_message_iter_next(&entry); + if (dbus_message_iter_get_arg_type(&entry) + != DBUS_TYPE_VARIANT) + break; + dbus_message_iter_recurse(&entry, &value); + if (dbus_message_iter_get_arg_type(&value) + != DBUS_TYPE_STRING) + break; + dbus_message_iter_get_basic(&value, &username); + vpn_provider_set_string_hide_value(data->provider, + "OpenConnect.Username", username); + } else if (g_str_equal(key, "Password")) { + dbus_message_iter_next(&entry); + if (dbus_message_iter_get_arg_type(&entry) + != DBUS_TYPE_VARIANT) + break; + dbus_message_iter_recurse(&entry, &value); + if (dbus_message_iter_get_arg_type(&value) + != DBUS_TYPE_STRING) + break; + dbus_message_iter_get_basic(&value, &password); + vpn_provider_set_string_hide_value(data->provider, + "OpenConnect.Password", password); + } else if (g_str_equal(key, "OpenConnect.PKCSPassword")) { + dbus_message_iter_next(&entry); + if (dbus_message_iter_get_arg_type(&entry) + != DBUS_TYPE_VARIANT) + break; + dbus_message_iter_recurse(&entry, &value); + if (dbus_message_iter_get_arg_type(&value) + != DBUS_TYPE_STRING) + break; + dbus_message_iter_get_basic(&value, &pkcspassword); + vpn_provider_set_string_hide_value(data->provider, key, + pkcspassword); } dbus_message_iter_next(&dict); } - if (!cookie || !servercert || !vpnhost) - goto err; - - run_connect(data->provider, data->task, data->if_name, data->cb, - data->user_data); + switch (data->connect_type) { + case OC_CONNECT_COOKIE: + if (!cookie) + goto err; + + break; + case OC_CONNECT_USERPASS: + /* fall through */ + case OC_CONNECT_COOKIE_WITH_USERPASS: + if (!username || !password) + goto err; + + break; + case OC_CONNECT_PUBLICKEY: + break; // This should not be reached. + case OC_CONNECT_PKCS: + if (!pkcspassword) + goto err; + + break; + } - free_private_data(data); + err = run_connect(data); + if (err != -EINPROGRESS) + goto err; return; err: - vpn_provider_indicate_error(data->provider, - VPN_PROVIDER_ERROR_AUTH_FAILED); + oc_connect_done(data, EACCES); +out: free_private_data(data); } -static int request_cookie_input(struct vpn_provider *provider, - struct oc_private_data *data, - const char *dbus_sender) +static int request_input_credentials(struct oc_private_data *data, + request_input_reply_cb_t cb) { DBusMessage *message; - const char *path, *agent_sender, *agent_path; + const char *path; + const char *agent_sender; + const char *agent_path; + const char *username; DBusMessageIter iter; DBusMessageIter dict; - const char *str; int err; void *agent; - agent = connman_agent_get_info(dbus_sender, &agent_sender, - &agent_path); - if (!provider || !agent || !agent_path) + if (!data || !cb) + return -ESRCH; + + connman_info("provider %p", data->provider); + + agent = connman_agent_get_info(data->dbus_sender, + &agent_sender, &agent_path); + if (!data->provider || !agent || !agent_path) return -ESRCH; message = dbus_message_new_method_call(agent_sender, agent_path, @@ -416,120 +1288,235 @@ static int request_cookie_input(struct vpn_provider *provider, dbus_message_iter_init_append(message, &iter); - path = vpn_provider_get_path(provider); - dbus_message_iter_append_basic(&iter, - DBUS_TYPE_OBJECT_PATH, &path); + path = vpn_provider_get_path(data->provider); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH, &path); connman_dbus_dict_open(&iter, &dict); - str = vpn_provider_get_string(provider, "OpenConnect.CACert"); - if (str) - connman_dbus_dict_append_dict(&dict, "OpenConnect.CACert", + request_input_append_to_dict(data->provider, &dict, request_input_append_informational, - (void *)str); - - str = vpn_provider_get_string(provider, "OpenConnect.ClientCert"); - if (str) - connman_dbus_dict_append_dict(&dict, "OpenConnect.ClientCert", + "OpenConnect.CACert"); + + /* + * For backwards compatibility add OpenConnect.ServerCert and + * OpenConnect.VPNHost as madnatory only in the default authentication + * mode. Otherwise. add the fields as informational. These should be + * set in provider settings and not to be queried with every connection + * attempt. + */ + request_input_append_to_dict(data->provider, &dict, + data->connect_type == OC_CONNECT_COOKIE ? + request_input_append_optional : request_input_append_informational, - (void *)str); - - connman_dbus_dict_append_dict(&dict, "OpenConnect.ServerCert", - request_input_append_mandatory, NULL); + "OpenConnect.ServerCert"); - connman_dbus_dict_append_dict(&dict, "OpenConnect.VPNHost", - request_input_append_mandatory, NULL); + request_input_append_to_dict(data->provider, &dict, + data->connect_type == OC_CONNECT_COOKIE ? + request_input_append_optional : + request_input_append_informational, + "OpenConnect.VPNHost"); + + if (vpn_provider_get_authentication_errors(data->provider)) + vpn_agent_append_auth_failure(&dict, data->provider, NULL); + + switch (data->connect_type) { + case OC_CONNECT_COOKIE: + request_input_append_to_dict(data->provider, &dict, + request_input_append_mandatory, + "OpenConnect.Cookie"); + break; + /* + * The authentication is done with username and password to get the + * cookie for connection. + */ + case OC_CONNECT_COOKIE_WITH_USERPASS: + /* fallthrough */ + case OC_CONNECT_USERPASS: + username = vpn_provider_get_string(data->provider, + "OpenConnect.Username"); + vpn_agent_append_user_info(&dict, data->provider, username); + break; + case OC_CONNECT_PUBLICKEY: + return -EINVAL; + case OC_CONNECT_PKCS: + request_input_append_to_dict(data->provider, &dict, + request_input_append_informational, + "OpenConnect.PKCSClientCert"); - connman_dbus_dict_append_dict(&dict, "OpenConnect.Cookie", - request_input_append_mandatory, NULL); + request_input_append_to_dict(data->provider, &dict, + request_input_append_password, + "OpenConnect.PKCSPassword"); + break; + } - vpn_agent_append_host_and_name(&dict, provider); + vpn_agent_append_host_and_name(&dict, data->provider); connman_dbus_dict_close(&iter, &dict); - err = connman_agent_queue_message(provider, message, - connman_timeout_input_request(), - request_input_cookie_reply, data, agent); + err = connman_agent_queue_message(data->provider, message, + connman_timeout_input_request(), cb, data, agent); - if (err < 0 && err != -EBUSY) { - DBG("error %d sending agent request", err); - dbus_message_unref(message); + dbus_message_unref(message); + if (err < 0 && err != -EBUSY) { + connman_error("cannot send agent request, error: %d", err); return err; } - dbus_message_unref(message); - return -EINPROGRESS; } +static enum oc_connect_type get_authentication_type( + struct vpn_provider *provider) +{ + const char *auth; + enum oc_connect_type type; + + auth = vpn_provider_get_string(provider, "OpenConnect.AuthType"); + if (!auth) + goto out; + + for (type = 0; connect_types[type]; type++) { + if (!g_strcmp0(auth, connect_types[type])) { + connman_info("auth type %d/%s", type, + connect_types[type]); + return type; + } + } + +out: + /* Default to cookie */ + return OC_CONNECT_COOKIE; +} + static int oc_connect(struct vpn_provider *provider, struct connman_task *task, const char *if_name, vpn_provider_connect_cb_t cb, const char *dbus_sender, void *user_data) { - const char *vpnhost, *vpncookie, *servercert; + struct oc_private_data *data; + const char *vpncookie; + const char *certificate; + const char *username; + const char *password; + const char *private_key; int err; - vpnhost = vpn_provider_get_string(provider, "Host"); - if (!vpnhost) { - connman_error("Host not set; cannot enable VPN"); - return -EINVAL; - } + connman_info("provider %p task %p", provider, task); + + data = g_try_new0(struct oc_private_data, 1); + if (!data) + return -ENOMEM; - vpncookie = vpn_provider_get_string(provider, "OpenConnect.Cookie"); - servercert = vpn_provider_get_string(provider, - "OpenConnect.ServerCert"); - if (!vpncookie || !servercert) { - struct oc_private_data *data; + vpn_provider_set_plugin_data(provider, data); + data->provider = vpn_provider_ref(provider); + data->task = task; + data->if_name = g_strdup(if_name); + data->dbus_sender = g_strdup(dbus_sender); + data->cb = cb; + data->user_data = user_data; + data->connect_type = get_authentication_type(provider); + + switch (data->connect_type) { + case OC_CONNECT_COOKIE: + vpncookie = vpn_provider_get_string(provider, + "OpenConnect.Cookie"); + if (!vpncookie || !g_strcmp0(vpncookie, "-")) + goto request_input; + + break; + case OC_CONNECT_USERPASS: + username = vpn_provider_get_string(provider, + "OpenConnect.Username"); + password = vpn_provider_get_string(provider, + "OpenConnect.Password"); + if (!username || !password || !g_strcmp0(username, "-") || + !g_strcmp0(password, "-")) + goto request_input; + + break; + case OC_CONNECT_COOKIE_WITH_USERPASS: + vpncookie = vpn_provider_get_string(provider, + "OpenConnect.Cookie"); + /* Username and password must be set if cookie is missing */ + if (!vpncookie) { + username = vpn_provider_get_string(provider, + "OpenConnect.Username"); + password = vpn_provider_get_string(provider, + "OpenConnect.Password"); + + if (!username || !password || + !g_strcmp0(username, "-") || + !g_strcmp0(password, "-")) + goto request_input; + } else if (!g_strcmp0(vpncookie, "-")) { + goto request_input; + } - data = g_try_new0(struct oc_private_data, 1); - if (!data) - return -ENOMEM; + break; + case OC_CONNECT_PUBLICKEY: + certificate = vpn_provider_get_string(provider, + "OpenConnect.ClientCert"); + private_key = vpn_provider_get_string(provider, + "OpenConnect.UserPrivateKey"); - data->provider = provider; - data->task = task; - data->if_name = g_strdup(if_name); - data->cb = cb; - data->user_data = user_data; + if (!certificate || !private_key) { + connman_warn("missing certificate and/or private key"); + oc_connect_done(data, EACCES); + free_private_data(data); + return -EACCES; + } - err = request_cookie_input(provider, data, dbus_sender); - if (err != -EINPROGRESS) { - vpn_provider_indicate_error(data->provider, - VPN_PROVIDER_ERROR_LOGIN_FAILED); + break; + case OC_CONNECT_PKCS: + certificate = vpn_provider_get_string(provider, + "OpenConnect.PKCSClientCert"); + if (!certificate) { + connman_warn("missing PKCS certificate"); + oc_connect_done(data, EACCES); free_private_data(data); + return -EACCES; } - return err; + + break; + } + + return run_connect(data); + +request_input: + err = request_input_credentials(data, request_input_credentials_reply); + if (err != -EINPROGRESS) { + oc_connect_done(data, err); + vpn_provider_indicate_error(data->provider, + VPN_PROVIDER_ERROR_LOGIN_FAILED); + free_private_data(data); } - return run_connect(provider, task, if_name, cb, user_data); + return err; +} + +static void oc_disconnect(struct vpn_provider *provider) +{ + connman_info("provider %p", provider); + + if (!provider) + return; + + /* + * OpenConnect may be disconnect by timeout in connmand before running + * the openconnect process. In such case it is important to cancel the + * agent request to avoid having multiple ones visible. + */ + connman_agent_cancel(provider); } static int oc_save(struct vpn_provider *provider, GKeyFile *keyfile) { - const char *setting, *option; + const char *save_group; + const char *option; int i; - setting = vpn_provider_get_string(provider, - "OpenConnect.ServerCert"); - if (setting) - g_key_file_set_string(keyfile, - vpn_provider_get_save_group(provider), - "OpenConnect.ServerCert", setting); - - setting = vpn_provider_get_string(provider, - "OpenConnect.CACert"); - if (setting) - g_key_file_set_string(keyfile, - vpn_provider_get_save_group(provider), - "OpenConnect.CACert", setting); - - setting = vpn_provider_get_string(provider, - "VPN.MTU"); - if (setting) - g_key_file_set_string(keyfile, - vpn_provider_get_save_group(provider), - "VPN.MTU", setting); + save_group = vpn_provider_get_save_group(provider); for (i = 0; i < (int)ARRAY_SIZE(oc_options); i++) { if (strncmp(oc_options[i].cm_opt, "OpenConnect.", 12) == 0) { @@ -538,8 +1525,7 @@ static int oc_save(struct vpn_provider *provider, GKeyFile *keyfile) if (!option) continue; - g_key_file_set_string(keyfile, - vpn_provider_get_save_group(provider), + g_key_file_set_string(keyfile, save_group, oc_options[i].cm_opt, option); } } @@ -549,23 +1535,63 @@ static int oc_save(struct vpn_provider *provider, GKeyFile *keyfile) static int oc_error_code(struct vpn_provider *provider, int exit_code) { + connman_info("%d", exit_code); + + /* OpenConnect process return values are ambiguous in definition + * https://github.com/openconnect/openconnect/blob/master/main.c#L1693 + * and it is safer not to rely on them. Login error cannot be + * differentiated from connection errors, e.g., when self signed + * certificate is rejected by user setting. + */ switch (exit_code) { - case 1: case 2: - vpn_provider_set_string_hide_value(provider, - "OpenConnect.Cookie", NULL); + /* Cookie has failed */ + clear_provider_credentials(provider); return VPN_PROVIDER_ERROR_LOGIN_FAILED; + case 1: + /* fall through */ default: return VPN_PROVIDER_ERROR_UNKNOWN; } } +static int oc_route_env_parse(struct vpn_provider *provider, const char *key, + int *family, unsigned long *idx, enum vpn_provider_route_type *type) +{ + char *end; + const char *start; + + if (g_str_has_prefix(key, "CISCO_SPLIT_INC_")) { + *family = AF_INET; + start = key + strlen("CISCO_SPLIT_INC_"); + } else if (g_str_has_prefix(key, "CISCO_IPV6_SPLIT_INC_")) { + *family = AF_INET6; + start = key + strlen("CISCO_IPV6_SPLIT_INC_"); + } else + return -EINVAL; + + *idx = g_ascii_strtoull(start, &end, 10); + + if (strncmp(end, "_ADDR", 5) == 0) + *type = VPN_PROVIDER_ROUTE_TYPE_ADDR; + else if (strncmp(end, "_MASK", 5) == 0) + *type = VPN_PROVIDER_ROUTE_TYPE_MASK; + else if (strncmp(end, "_MASKLEN", 8) == 0 && *family == AF_INET6) + *type = VPN_PROVIDER_ROUTE_TYPE_MASK; + else + return -EINVAL; + + return 0; +} + static struct vpn_driver vpn_driver = { .notify = oc_notify, .connect = oc_connect, + .disconnect = oc_disconnect, .error_code = oc_error_code, .save = oc_save, + .route_env_parse = oc_route_env_parse, }; static int openconnect_init(void)