+/* Core of cmd_wkd_get and task_check_wkd_support. If CTX is NULL
+ * this function will not write anything to the assuan output. */
+static gpg_error_t
+proc_wkd_get (ctrl_t ctrl, assuan_context_t ctx, char *line)
+{
+ gpg_error_t err = 0;
+ char *mbox = NULL;
+ char *domainbuf = NULL;
+ char *domain; /* Points to mbox or domainbuf. This is used to
+ * connect to the host. */
+ char *domain_orig;/* Points to mbox. This is the used for the
+ * query; i.e. the domain part of the
+ * addrspec. */
+ char sha1buf[20];
+ char *uri = NULL;
+ char *encodedhash = NULL;
+ int opt_submission_addr;
+ int opt_policy_flags;
+ int is_wkd_query; /* True if this is a real WKD query. */
+ int no_log = 0;
+ char portstr[20] = { 0 };
+ int subdomain_mode = 0;
+
+ opt_submission_addr = has_option (line, "--submission-address");
+ opt_policy_flags = has_option (line, "--policy-flags");
+ if (has_option (line, "--quick"))
+ ctrl->timeout = opt.connect_quick_timeout;
+ line = skip_options (line);
+ is_wkd_query = !(opt_policy_flags || opt_submission_addr);
+
+ mbox = mailbox_from_userid (line, 0);
+ if (!mbox || !(domain = strchr (mbox, '@')))
+ {
+ err = set_error (GPG_ERR_INV_USER_ID, "no mailbox in user id");
+ goto leave;
+ }
+ *domain++ = 0;
+ domain_orig = domain;
+
+
+ /* Let's check whether we already know that the domain does not
+ * support WKD. */
+ if (is_wkd_query)
+ {
+ if (domaininfo_is_wkd_not_supported (domain_orig))
+ {
+ err = gpg_error (GPG_ERR_NO_DATA);
+ dirmngr_status_printf (ctrl, "NOTE", "wkd_cached_result %u", err);
+ goto leave;
+ }
+ }
+
+
+ /* First try the new "openpgp" subdomain. We check that the domain
+ * is valid because it is later used as an unescaped filename part
+ * of the URI. */
+ if (is_valid_domain_name (domain_orig))
+ {
+ dns_addrinfo_t aibuf;
+
+ domainbuf = strconcat ( "openpgpkey.", domain_orig, NULL);
+ if (!domainbuf)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ /* FIXME: We should put a cache into dns-stuff because the same
+ * query (with a different port and socket type, though) will be
+ * done later by http function. */
+ err = resolve_dns_name (ctrl, domainbuf, 0, 0, 0, &aibuf, NULL);
+ if (err)
+ {
+ err = 0;
+ xfree (domainbuf);
+ domainbuf = NULL;
+ }
+ else /* Got a subdomain. */
+ {
+ free_dns_addrinfo (aibuf);
+ subdomain_mode = 1;
+ domain = domainbuf;
+ }
+ }
+
+ /* Check for SRV records unless we have a subdomain. */
+ if (!subdomain_mode)
+ {
+ struct srventry *srvs;
+ unsigned int srvscount;
+ size_t domainlen, targetlen;
+ int i;
+
+ err = get_dns_srv (ctrl, domain, "openpgpkey", NULL, &srvs, &srvscount);
+ if (err)
+ {
+ /* Ignore server failed becuase there are too many resolvers
+ * which do not work as expected. */
+ if (gpg_err_code (err) == GPG_ERR_SERVER_FAILED)
+ err = 0; /*(srvcount is guaranteed to be 0)*/
+ else
+ goto leave;
+ }
+
+ /* Check for rogue DNS names. */
+ for (i = 0; i < srvscount; i++)
+ {
+ if (!is_valid_domain_name (srvs[i].target))
+ {
+ err = gpg_error (GPG_ERR_DNS_ADDRESS);
+ log_error ("rogue openpgpkey SRV record for '%s'\n", domain);
+ xfree (srvs);
+ goto leave;
+ }
+ }
+
+ /* Find the first target which also ends in DOMAIN or is equal
+ * to DOMAIN. */
+ domainlen = strlen (domain);
+ for (i = 0; i < srvscount; i++)
+ {
+ if (DBG_DNS)
+ log_debug ("srv: trying '%s:%hu'\n", srvs[i].target, srvs[i].port);
+ targetlen = strlen (srvs[i].target);
+ if ((targetlen > domainlen + 1
+ && srvs[i].target[targetlen - domainlen - 1] == '.'
+ && !ascii_strcasecmp (srvs[i].target + targetlen - domainlen,
+ domain))
+ || (targetlen == domainlen
+ && !ascii_strcasecmp (srvs[i].target, domain)))
+ {
+ /* found. */
+ domainbuf = xtrystrdup (srvs[i].target);
+ if (!domainbuf)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (srvs);
+ goto leave;
+ }
+ domain = domainbuf;
+ if (srvs[i].port)
+ snprintf (portstr, sizeof portstr, ":%hu", srvs[i].port);
+ break;
+ }
+ }
+ xfree (srvs);
+ }
+
+ /* Prepare the hash of the local part. */
+ gcry_md_hash_buffer (GCRY_MD_SHA1, sha1buf, mbox, strlen (mbox));
+ encodedhash = zb32_encode (sha1buf, 8*20);
+ if (!encodedhash)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ if (opt_submission_addr)
+ {
+ uri = strconcat ("https://",
+ domain,
+ portstr,
+ "/.well-known/openpgpkey/",
+ subdomain_mode? domain_orig : "",
+ subdomain_mode? "/" : "",
+ "submission-address",
+ NULL);
+ }
+ else if (opt_policy_flags)
+ {
+ uri = strconcat ("https://",
+ domain,
+ portstr,
+ "/.well-known/openpgpkey/",
+ subdomain_mode? domain_orig : "",
+ subdomain_mode? "/" : "",
+ "policy",
+ NULL);
+ }
+ else
+ {
+ char *escapedmbox;
+
+ escapedmbox = http_escape_string (mbox, "%;?&=+#");
+ if (escapedmbox)
+ {
+ uri = strconcat ("https://",
+ domain,
+ portstr,
+ "/.well-known/openpgpkey/",
+ subdomain_mode? domain_orig : "",
+ subdomain_mode? "/" : "",
+ "hu/",
+ encodedhash,
+ "?l=",
+ escapedmbox,
+ NULL);
+ xfree (escapedmbox);
+ no_log = 1;
+ if (uri)
+ {
+ err = dirmngr_status_printf (ctrl, "SOURCE", "https://%s%s",
+ domain, portstr);
+ if (err)
+ goto leave;
+ }
+ }
+ }
+ if (!uri)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ /* Setup an output stream and perform the get. */
+ {
+ estream_t outfp;
+
+ outfp = ctx? es_fopencookie (ctx, "w", data_line_cookie_functions) : NULL;
+ if (!outfp && ctx)
+ err = set_error (GPG_ERR_ASS_GENERAL,
+ "error setting up a data stream");
+ else
+ {
+ if (ctrl->server_local)
+ {
+ if (no_log)
+ ctrl->server_local->inhibit_data_logging = 1;
+ ctrl->server_local->inhibit_data_logging_now = 0;
+ ctrl->server_local->inhibit_data_logging_count = 0;
+ }
+ err = ks_action_fetch (ctrl, uri, outfp);
+ es_fclose (outfp);
+ if (ctrl->server_local)
+ ctrl->server_local->inhibit_data_logging = 0;
+
+ /* Register the result under the domain name of MBOX. */
+ switch (gpg_err_code (err))
+ {
+ case 0:
+ domaininfo_set_wkd_supported (domain_orig);
+ break;
+
+ case GPG_ERR_NO_NAME:
+ /* There is no such domain. */
+ domaininfo_set_no_name (domain_orig);
+ break;
+
+ case GPG_ERR_NO_DATA:
+ if (is_wkd_query && ctrl->server_local)
+ {
+ /* Mark that and schedule a check. */
+ domaininfo_set_wkd_not_found (domain_orig);
+ workqueue_add_task (task_check_wkd_support, domain_orig,
+ ctrl->server_local->session_id, 1);
+ }
+ else if (opt_policy_flags) /* No policy file - no support. */
+ domaininfo_set_wkd_not_supported (domain_orig);
+ break;
+
+ default:
+ /* Don't register other errors. */
+ break;
+ }
+ }
+ }
+
+ leave:
+ xfree (uri);
+ xfree (encodedhash);
+ xfree (mbox);
+ xfree (domainbuf);
+ return err;
+}
+
+
+static const char hlp_wkd_get[] =
+ "WKD_GET [--submission-address|--policy-flags] <user_id>\n"
+ "\n"
+ "Return the key or other info for <user_id>\n"
+ "from the Web Key Directory.";
+static gpg_error_t
+cmd_wkd_get (assuan_context_t ctx, char *line)
+{
+ ctrl_t ctrl = assuan_get_pointer (ctx);
+ gpg_error_t err;
+
+ err = proc_wkd_get (ctrl, ctx, line);
+
+ return leave_cmd (ctx, err);
+}
+
+
+/* A task to check whether DOMAIN supports WKD. This is done by
+ * checking whether the policy flags file can be read. */
+static const char *
+task_check_wkd_support (ctrl_t ctrl, const char *domain)
+{
+ char *string;
+
+ if (!ctrl || !domain)
+ return "check_wkd_support";
+
+ string = strconcat ("--policy-flags foo@", domain, NULL);
+ if (!string)
+ log_error ("%s: %s\n", __func__, gpg_strerror (gpg_error_from_syserror ()));
+ else
+ {
+ proc_wkd_get (ctrl, NULL, string);
+ xfree (string);
+ }
+
+ return NULL;
+}
+
+
+\f