/*
 * Hotspot 2.0 SPP server
 * Copyright (c) 2012-2013, Qualcomm Atheros, Inc.
 *
 * This software may be distributed under the terms of the BSD license.
 * See README for more details.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>
#include <sqlite3.h>

#include "common.h"
#include "base64.h"
#include "md5_i.h"
#include "xml-utils.h"
#include "spp_server.h"


#define SPP_NS_URI "http://www.wi-fi.org/specifications/hotspot2dot0/v1.0/spp"

#define URN_OMA_DM_DEVINFO "urn:oma:mo:oma-dm-devinfo:1.0"
#define URN_OMA_DM_DEVDETAIL "urn:oma:mo:oma-dm-devdetail:1.0"
#define URN_OMA_DM_DMACC "urn:oma:mo:oma-dm-dmacc:1.0"
#define URN_HS20_PPS "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0"


/* TODO: timeout to expire sessions */

enum hs20_session_operation {
	NO_OPERATION,
	UPDATE_PASSWORD,
	CONTINUE_SUBSCRIPTION_REMEDIATION,
	CONTINUE_POLICY_UPDATE,
	USER_REMEDIATION,
	SUBSCRIPTION_REGISTRATION,
	POLICY_REMEDIATION,
	POLICY_UPDATE,
	FREE_REMEDIATION,
};


static char * db_get_session_val(struct hs20_svc *ctx, const char *user,
				 const char *realm, const char *session_id,
				 const char *field);
static char * db_get_osu_config_val(struct hs20_svc *ctx, const char *realm,
				    const char *field);
static xml_node_t * build_policy(struct hs20_svc *ctx, const char *user,
				 const char *realm, int use_dmacc);


static int db_add_session(struct hs20_svc *ctx,
			  const char *user, const char *realm,
			  const char *sessionid, const char *pw,
			  const char *redirect_uri,
			  enum hs20_session_operation operation)
{
	char *sql;
	int ret = 0;

	sql = sqlite3_mprintf("INSERT INTO sessions(timestamp,id,user,realm,"
			      "operation,password,redirect_uri) "
			      "VALUES "
			      "(strftime('%%Y-%%m-%%d %%H:%%M:%%f','now'),"
			      "%Q,%Q,%Q,%d,%Q,%Q)",
			      sessionid, user ? user : "", realm ? realm : "",
			      operation, pw ? pw : "",
			      redirect_uri ? redirect_uri : "");
	if (sql == NULL)
		return -1;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to add session entry into sqlite "
			    "database: %s", sqlite3_errmsg(ctx->db));
		ret = -1;
	}
	sqlite3_free(sql);
	return ret;
}


static void db_update_session_password(struct hs20_svc *ctx, const char *user,
				       const char *realm, const char *sessionid,
				       const char *pw)
{
	char *sql;

	sql = sqlite3_mprintf("UPDATE sessions SET password=%Q WHERE id=%Q AND "
			      "user=%Q AND realm=%Q",
			      pw, sessionid, user, realm);
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to update session password: %s",
			    sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void db_update_session_machine_managed(struct hs20_svc *ctx,
					      const char *user,
					      const char *realm,
					      const char *sessionid,
					      const int pw_mm)
{
	char *sql;

	sql = sqlite3_mprintf("UPDATE sessions SET machine_managed=%Q WHERE id=%Q AND user=%Q AND realm=%Q",
			      pw_mm ? "1" : "0", sessionid, user, realm);
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1,
			    "Failed to update session machine_managed: %s",
			    sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void db_add_session_pps(struct hs20_svc *ctx, const char *user,
			       const char *realm, const char *sessionid,
			       xml_node_t *node)
{
	char *str;
	char *sql;

	str = xml_node_to_str(ctx->xml, node);
	if (str == NULL)
		return;
	sql = sqlite3_mprintf("UPDATE sessions SET pps=%Q WHERE id=%Q AND "
			      "user=%Q AND realm=%Q",
			      str, sessionid, user, realm);
	free(str);
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to add session pps: %s",
			    sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void db_add_session_devinfo(struct hs20_svc *ctx, const char *sessionid,
				   xml_node_t *node)
{
	char *str;
	char *sql;

	str = xml_node_to_str(ctx->xml, node);
	if (str == NULL)
		return;
	sql = sqlite3_mprintf("UPDATE sessions SET devinfo=%Q WHERE id=%Q",
			      str, sessionid);
	free(str);
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to add session devinfo: %s",
			    sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void db_add_session_devdetail(struct hs20_svc *ctx,
				     const char *sessionid,
				     xml_node_t *node)
{
	char *str;
	char *sql;

	str = xml_node_to_str(ctx->xml, node);
	if (str == NULL)
		return;
	sql = sqlite3_mprintf("UPDATE sessions SET devdetail=%Q WHERE id=%Q",
			      str, sessionid);
	free(str);
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to add session devdetail: %s",
			    sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void db_remove_session(struct hs20_svc *ctx,
			      const char *user, const char *realm,
			      const char *sessionid)
{
	char *sql;

	if (user == NULL || realm == NULL) {
		sql = sqlite3_mprintf("DELETE FROM sessions WHERE "
				      "id=%Q", sessionid);
	} else {
		sql = sqlite3_mprintf("DELETE FROM sessions WHERE "
				      "user=%Q AND realm=%Q AND id=%Q",
				      user, realm, sessionid);
	}
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to delete session entry from "
			    "sqlite database: %s", sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void hs20_eventlog(struct hs20_svc *ctx,
			  const char *user, const char *realm,
			  const char *sessionid, const char *notes,
			  const char *dump)
{
	char *sql;
	char *user_buf = NULL, *realm_buf = NULL;

	debug_print(ctx, 1, "eventlog: %s", notes);

	if (user == NULL) {
		user_buf = db_get_session_val(ctx, NULL, NULL, sessionid,
					      "user");
		user = user_buf;
		realm_buf = db_get_session_val(ctx, NULL, NULL, sessionid,
					       "realm");
		realm = realm_buf;
	}

	sql = sqlite3_mprintf("INSERT INTO eventlog"
			      "(user,realm,sessionid,timestamp,notes,dump,addr)"
			      " VALUES (%Q,%Q,%Q,"
			      "strftime('%%Y-%%m-%%d %%H:%%M:%%f','now'),"
			      "%Q,%Q,%Q)",
			      user, realm, sessionid, notes,
			      dump ? dump : "", ctx->addr ? ctx->addr : "");
	free(user_buf);
	free(realm_buf);
	if (sql == NULL)
		return;
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to add eventlog entry into sqlite "
			    "database: %s", sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void hs20_eventlog_node(struct hs20_svc *ctx,
			       const char *user, const char *realm,
			       const char *sessionid, const char *notes,
			       xml_node_t *node)
{
	char *str;

	if (node)
		str = xml_node_to_str(ctx->xml, node);
	else
		str = NULL;
	hs20_eventlog(ctx, user, realm, sessionid, notes, str);
	free(str);
}


static void db_update_mo_str(struct hs20_svc *ctx, const char *user,
			     const char *realm, const char *name,
			     const char *str)
{
	char *sql;
	if (user == NULL || realm == NULL || name == NULL)
		return;
	sql = sqlite3_mprintf("UPDATE users SET %s=%Q "
		 "WHERE identity=%Q AND realm=%Q AND phase2=1",
			      name, str, user, realm);
	if (sql == NULL)
		return;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to update user MO entry in sqlite "
			    "database: %s", sqlite3_errmsg(ctx->db));
	}
	sqlite3_free(sql);
}


static void db_update_mo(struct hs20_svc *ctx, const char *user,
			 const char *realm, const char *name, xml_node_t *mo)
{
	char *str;

	str = xml_node_to_str(ctx->xml, mo);
	if (str == NULL)
		return;

	db_update_mo_str(ctx, user, realm, name, str);
	free(str);
}


static void add_text_node(struct hs20_svc *ctx, xml_node_t *parent,
			  const char *name, const char *value)
{
	xml_node_create_text(ctx->xml, parent, NULL, name, value ? value : "");
}


static void add_text_node_conf(struct hs20_svc *ctx, const char *realm,
			       xml_node_t *parent, const char *name,
			       const char *field)
{
	char *val;
	val = db_get_osu_config_val(ctx, realm, field);
	xml_node_create_text(ctx->xml, parent, NULL, name, val ? val : "");
	os_free(val);
}


static int new_password(char *buf, int buflen)
{
	int i;

	if (buflen < 1)
		return -1;
	buf[buflen - 1] = '\0';
	if (os_get_random((unsigned char *) buf, buflen - 1) < 0)
		return -1;

	for (i = 0; i < buflen - 1; i++) {
		unsigned char val = buf[i];
		val %= 2 * 26 + 10;
		if (val < 26)
			buf[i] = 'a' + val;
		else if (val < 2 * 26)
			buf[i] = 'A' + val - 26;
		else
			buf[i] = '0' + val - 2 * 26;
	}

	return 0;
}


struct get_db_field_data {
	const char *field;
	char *value;
};


static int get_db_field(void *ctx, int argc, char *argv[], char *col[])
{
	struct get_db_field_data *data = ctx;
	int i;

	for (i = 0; i < argc; i++) {
		if (os_strcmp(col[i], data->field) == 0 && argv[i]) {
			os_free(data->value);
			data->value = os_strdup(argv[i]);
			break;
		}
	}

	return 0;
}


static char * db_get_val(struct hs20_svc *ctx, const char *user,
			 const char *realm, const char *field, int dmacc)
{
	char *cmd;
	struct get_db_field_data data;

	cmd = sqlite3_mprintf("SELECT %s FROM users WHERE "
			      "%s=%Q AND realm=%Q AND phase2=1",
			      field, dmacc ? "osu_user" : "identity",
			      user, realm);
	if (cmd == NULL)
		return NULL;
	memset(&data, 0, sizeof(data));
	data.field = field;
	if (sqlite3_exec(ctx->db, cmd, get_db_field, &data, NULL) != SQLITE_OK)
	{
		debug_print(ctx, 1, "Could not find user '%s'", user);
		sqlite3_free(cmd);
		return NULL;
	}
	sqlite3_free(cmd);

	debug_print(ctx, 1, "DB: user='%s' realm='%s' field='%s' dmacc=%d --> "
		    "value='%s'", user, realm, field, dmacc, data.value);

	return data.value;
}


static int db_update_val(struct hs20_svc *ctx, const char *user,
			 const char *realm, const char *field,
			 const char *val, int dmacc)
{
	char *cmd;
	int ret;

	cmd = sqlite3_mprintf("UPDATE users SET %s=%Q WHERE "
			      "%s=%Q AND realm=%Q AND phase2=1",
			      field, val, dmacc ? "osu_user" : "identity", user,
			      realm);
	if (cmd == NULL)
		return -1;
	debug_print(ctx, 1, "DB: %s", cmd);
	if (sqlite3_exec(ctx->db, cmd, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1,
			    "Failed to update user in sqlite database: %s",
			    sqlite3_errmsg(ctx->db));
		ret = -1;
	} else {
		debug_print(ctx, 1,
			    "DB: user='%s' realm='%s' field='%s' set to '%s'",
			    user, realm, field, val);
		ret = 0;
	}
	sqlite3_free(cmd);

	return ret;
}


static char * db_get_session_val(struct hs20_svc *ctx, const char *user,
				 const char *realm, const char *session_id,
				 const char *field)
{
	char *cmd;
	struct get_db_field_data data;

	if (user == NULL || realm == NULL) {
		cmd = sqlite3_mprintf("SELECT %s FROM sessions WHERE "
				      "id=%Q", field, session_id);
	} else {
		cmd = sqlite3_mprintf("SELECT %s FROM sessions WHERE "
				      "user=%Q AND realm=%Q AND id=%Q",
				      field, user, realm, session_id);
	}
	if (cmd == NULL)
		return NULL;
	debug_print(ctx, 1, "DB: %s", cmd);
	memset(&data, 0, sizeof(data));
	data.field = field;
	if (sqlite3_exec(ctx->db, cmd, get_db_field, &data, NULL) != SQLITE_OK)
	{
		debug_print(ctx, 1, "DB: Could not find session %s: %s",
			    session_id, sqlite3_errmsg(ctx->db));
		sqlite3_free(cmd);
		return NULL;
	}
	sqlite3_free(cmd);

	debug_print(ctx, 1, "DB: return '%s'", data.value);
	return data.value;
}


static int update_password(struct hs20_svc *ctx, const char *user,
			   const char *realm, const char *pw, int dmacc)
{
	char *cmd;

	cmd = sqlite3_mprintf("UPDATE users SET password=%Q, "
			      "remediation='' "
			      "WHERE %s=%Q AND phase2=1",
			      pw, dmacc ? "osu_user" : "identity",
			      user);
	if (cmd == NULL)
		return -1;
	debug_print(ctx, 1, "DB: %s", cmd);
	if (sqlite3_exec(ctx->db, cmd, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to update database for user '%s'",
			    user);
	}
	sqlite3_free(cmd);

	return 0;
}


static int add_eap_ttls(struct hs20_svc *ctx, xml_node_t *parent)
{
	xml_node_t *node;

	node = xml_node_create(ctx->xml, parent, NULL, "EAPMethod");
	if (node == NULL)
		return -1;

	add_text_node(ctx, node, "EAPType", "21");
	add_text_node(ctx, node, "InnerMethod", "MS-CHAP-V2");

	return 0;
}


static xml_node_t * build_username_password(struct hs20_svc *ctx,
					    xml_node_t *parent,
					    const char *user, const char *pw)
{
	xml_node_t *node;
	char *b64;

	node = xml_node_create(ctx->xml, parent, NULL, "UsernamePassword");
	if (node == NULL)
		return NULL;

	add_text_node(ctx, node, "Username", user);

	b64 = (char *) base64_encode((unsigned char *) pw, strlen(pw), NULL);
	if (b64 == NULL)
		return NULL;
	add_text_node(ctx, node, "Password", b64);
	free(b64);

	return node;
}


static int add_username_password(struct hs20_svc *ctx, xml_node_t *cred,
				 const char *user, const char *pw)
{
	xml_node_t *node;

	node = build_username_password(ctx, cred, user, pw);
	if (node == NULL)
		return -1;

	add_text_node(ctx, node, "MachineManaged", "TRUE");
	add_text_node(ctx, node, "SoftTokenApp", "");
	add_eap_ttls(ctx, node);

	return 0;
}


static void add_creation_date(struct hs20_svc *ctx, xml_node_t *cred)
{
	char str[30];
	time_t now;
	struct tm tm;

	time(&now);
	gmtime_r(&now, &tm);
	snprintf(str, sizeof(str), "%04u-%02u-%02uT%02u:%02u:%02uZ",
		 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
		 tm.tm_hour, tm.tm_min, tm.tm_sec);
	xml_node_create_text(ctx->xml, cred, NULL, "CreationDate", str);
}


static xml_node_t * build_credential_pw(struct hs20_svc *ctx,
					const char *user, const char *realm,
					const char *pw)
{
	xml_node_t *cred;

	cred = xml_node_create_root(ctx->xml, NULL, NULL, NULL, "Credential");
	if (cred == NULL) {
		debug_print(ctx, 1, "Failed to create Credential node");
		return NULL;
	}
	add_creation_date(ctx, cred);
	if (add_username_password(ctx, cred, user, pw) < 0) {
		xml_node_free(ctx->xml, cred);
		return NULL;
	}
	add_text_node(ctx, cred, "Realm", realm);

	return cred;
}


static xml_node_t * build_credential(struct hs20_svc *ctx,
				     const char *user, const char *realm,
				     char *new_pw, size_t new_pw_len)
{
	if (new_password(new_pw, new_pw_len) < 0)
		return NULL;
	debug_print(ctx, 1, "Update password to '%s'", new_pw);
	return build_credential_pw(ctx, user, realm, new_pw);
}


static xml_node_t * build_credential_cert(struct hs20_svc *ctx,
					  const char *user, const char *realm,
					  const char *cert_fingerprint)
{
	xml_node_t *cred, *cert;

	cred = xml_node_create_root(ctx->xml, NULL, NULL, NULL, "Credential");
	if (cred == NULL) {
		debug_print(ctx, 1, "Failed to create Credential node");
		return NULL;
	}
	add_creation_date(ctx, cred);
	cert = xml_node_create(ctx->xml, cred, NULL, "DigitalCertificate");
	add_text_node(ctx, cert, "CertificateType", "x509v3");
	add_text_node(ctx, cert, "CertSHA256Fingerprint", cert_fingerprint);
	add_text_node(ctx, cred, "Realm", realm);

	return cred;
}


static xml_node_t * build_post_dev_data_response(struct hs20_svc *ctx,
						 xml_namespace_t **ret_ns,
						 const char *session_id,
						 const char *status,
						 const char *error_code)
{
	xml_node_t *spp_node = NULL;
	xml_namespace_t *ns;

	spp_node = xml_node_create_root(ctx->xml, SPP_NS_URI, "spp", &ns,
					"sppPostDevDataResponse");
	if (spp_node == NULL)
		return NULL;
	if (ret_ns)
		*ret_ns = ns;

	xml_node_add_attr(ctx->xml, spp_node, ns, "sppVersion", "1.0");
	xml_node_add_attr(ctx->xml, spp_node, ns, "sessionID", session_id);
	xml_node_add_attr(ctx->xml, spp_node, ns, "sppStatus", status);

	if (error_code) {
		xml_node_t *node;
		node = xml_node_create(ctx->xml, spp_node, ns, "sppError");
		if (node)
			xml_node_add_attr(ctx->xml, node, NULL, "errorCode",
					  error_code);
	}

	return spp_node;
}


static int add_update_node(struct hs20_svc *ctx, xml_node_t *spp_node,
			   xml_namespace_t *ns, const char *uri,
			   xml_node_t *upd_node)
{
	xml_node_t *node, *tnds;
	char *str;

	tnds = mo_to_tnds(ctx->xml, upd_node, 0, NULL, NULL);
	if (!tnds)
		return -1;

	str = xml_node_to_str(ctx->xml, tnds);
	xml_node_free(ctx->xml, tnds);
	if (str == NULL)
		return -1;
	node = xml_node_create_text(ctx->xml, spp_node, ns, "updateNode", str);
	free(str);

	xml_node_add_attr(ctx->xml, node, ns, "managementTreeURI", uri);

	return 0;
}


static xml_node_t * build_sub_rem_resp(struct hs20_svc *ctx,
				       const char *user, const char *realm,
				       const char *session_id,
				       int machine_rem, int dmacc)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *cred;
	char buf[400];
	char new_pw[33];
	char *real_user = NULL;
	char *status;
	char *cert;

	if (dmacc) {
		real_user = db_get_val(ctx, user, realm, "identity", dmacc);
		if (real_user == NULL) {
			debug_print(ctx, 1, "Could not find user identity for "
				    "dmacc user '%s'", user);
			return NULL;
		}
	}

	cert = db_get_val(ctx, user, realm, "cert", dmacc);
	if (cert && cert[0] == '\0')
		cert = NULL;
	if (cert) {
		cred = build_credential_cert(ctx, real_user ? real_user : user,
					     realm, cert);
	} else {
		cred = build_credential(ctx, real_user ? real_user : user,
					realm, new_pw, sizeof(new_pw));
	}
	free(real_user);
	if (!cred) {
		debug_print(ctx, 1, "Could not build credential");
		return NULL;
	}

	status = "Remediation complete, request sppUpdateResponse";
	spp_node = build_post_dev_data_response(ctx, &ns, session_id, status,
						NULL);
	if (spp_node == NULL) {
		debug_print(ctx, 1, "Could not build sppPostDevDataResponse");
		return NULL;
	}

	snprintf(buf, sizeof(buf),
		 "./Wi-Fi/%s/PerProviderSubscription/Credential1/Credential",
		 realm);

	if (add_update_node(ctx, spp_node, ns, buf, cred) < 0) {
		debug_print(ctx, 1, "Could not add update node");
		xml_node_free(ctx->xml, spp_node);
		return NULL;
	}

	hs20_eventlog_node(ctx, user, realm, session_id,
			   machine_rem ? "machine remediation" :
			   "user remediation", cred);
	xml_node_free(ctx->xml, cred);

	if (cert) {
		debug_print(ctx, 1, "Certificate credential - no need for DB "
			    "password update on success notification");
	} else {
		debug_print(ctx, 1, "Request DB password update on success "
			    "notification");
		db_add_session(ctx, user, realm, session_id, new_pw, NULL,
			       UPDATE_PASSWORD);
	}

	return spp_node;
}


static xml_node_t * machine_remediation(struct hs20_svc *ctx,
					const char *user,
					const char *realm,
					const char *session_id, int dmacc)
{
	return build_sub_rem_resp(ctx, user, realm, session_id, 1, dmacc);
}


static xml_node_t * policy_remediation(struct hs20_svc *ctx,
				       const char *user, const char *realm,
				       const char *session_id, int dmacc)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *policy;
	char buf[400];
	const char *status;

	hs20_eventlog(ctx, user, realm, session_id,
		      "requires policy remediation", NULL);

	db_add_session(ctx, user, realm, session_id, NULL, NULL,
		       POLICY_REMEDIATION);

	policy = build_policy(ctx, user, realm, dmacc);
	if (!policy) {
		return build_post_dev_data_response(
			ctx, NULL, session_id,
			"No update available at this time", NULL);
	}

	status = "Remediation complete, request sppUpdateResponse";
	spp_node = build_post_dev_data_response(ctx, &ns, session_id, status,
						NULL);
	if (spp_node == NULL)
		return NULL;

	snprintf(buf, sizeof(buf),
		 "./Wi-Fi/%s/PerProviderSubscription/Credential1/Policy",
		 realm);

	if (add_update_node(ctx, spp_node, ns, buf, policy) < 0) {
		xml_node_free(ctx->xml, spp_node);
		xml_node_free(ctx->xml, policy);
		return NULL;
	}

	hs20_eventlog_node(ctx, user, realm, session_id,
			   "policy update (sub rem)", policy);
	xml_node_free(ctx->xml, policy);

	return spp_node;
}


static xml_node_t * browser_remediation(struct hs20_svc *ctx,
					const char *session_id,
					const char *redirect_uri,
					const char *uri)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *exec_node;

	if (redirect_uri == NULL) {
		debug_print(ctx, 1, "Missing redirectURI attribute for user "
			    "remediation");
		return NULL;
	}
	debug_print(ctx, 1, "redirectURI %s", redirect_uri);

	spp_node = build_post_dev_data_response(ctx, &ns, session_id, "OK",
		NULL);
	if (spp_node == NULL)
		return NULL;

	exec_node = xml_node_create(ctx->xml, spp_node, ns, "exec");
	xml_node_create_text(ctx->xml, exec_node, ns, "launchBrowserToURI",
			     uri);
	return spp_node;
}


static xml_node_t * user_remediation(struct hs20_svc *ctx, const char *user,
				     const char *realm, const char *session_id,
				     const char *redirect_uri)
{
	char uri[300], *val;

	hs20_eventlog(ctx, user, realm, session_id,
		      "requires user remediation", NULL);
	val = db_get_osu_config_val(ctx, realm, "remediation_url");
	if (val == NULL)
		return NULL;

	db_add_session(ctx, user, realm, session_id, NULL, redirect_uri,
		       USER_REMEDIATION);

	snprintf(uri, sizeof(uri), "%s%s", val, session_id);
	os_free(val);
	return browser_remediation(ctx, session_id, redirect_uri, uri);
}


static xml_node_t * free_remediation(struct hs20_svc *ctx,
				     const char *user, const char *realm,
				     const char *session_id,
				     const char *redirect_uri)
{
	char uri[300], *val;

	hs20_eventlog(ctx, user, realm, session_id,
		      "requires free/public account remediation", NULL);
	val = db_get_osu_config_val(ctx, realm, "free_remediation_url");
	if (val == NULL)
		return NULL;

	db_add_session(ctx, user, realm, session_id, NULL, redirect_uri,
		       FREE_REMEDIATION);

	snprintf(uri, sizeof(uri), "%s%s", val, session_id);
	os_free(val);
	return browser_remediation(ctx, session_id, redirect_uri, uri);
}


static xml_node_t * no_sub_rem(struct hs20_svc *ctx,
			       const char *user, const char *realm,
			       const char *session_id)
{
	const char *status;

	hs20_eventlog(ctx, user, realm, session_id,
		      "no subscription mediation available", NULL);

	status = "No update available at this time";
	return build_post_dev_data_response(ctx, NULL, session_id, status,
					    NULL);
}


static xml_node_t * hs20_subscription_remediation(struct hs20_svc *ctx,
						  const char *user,
						  const char *realm,
						  const char *session_id,
						  int dmacc,
						  const char *redirect_uri)
{
	char *type, *identity;
	xml_node_t *ret;
	char *free_account;

	identity = db_get_val(ctx, user, realm, "identity", dmacc);
	if (identity == NULL || strlen(identity) == 0) {
		hs20_eventlog(ctx, user, realm, session_id,
			      "user not found in database for remediation",
			      NULL);
		os_free(identity);
		return build_post_dev_data_response(ctx, NULL, session_id,
						    "Error occurred",
						    "Not found");
	}
	os_free(identity);

	free_account = db_get_osu_config_val(ctx, realm, "free_account");
	if (free_account && strcmp(free_account, user) == 0) {
		free(free_account);
		return no_sub_rem(ctx, user, realm, session_id);
	}
	free(free_account);

	type = db_get_val(ctx, user, realm, "remediation", dmacc);
	if (type && strcmp(type, "free") != 0) {
		char *val;
		int shared = 0;
		val = db_get_val(ctx, user, realm, "shared", dmacc);
		if (val)
			shared = atoi(val);
		free(val);
		if (shared) {
			free(type);
			return no_sub_rem(ctx, user, realm, session_id);
		}
	}
	if (type && strcmp(type, "user") == 0)
		ret = user_remediation(ctx, user, realm, session_id,
				       redirect_uri);
	else if (type && strcmp(type, "free") == 0)
		ret = free_remediation(ctx, user, realm, session_id,
				       redirect_uri);
	else if (type && strcmp(type, "policy") == 0)
		ret = policy_remediation(ctx, user, realm, session_id, dmacc);
	else
		ret = machine_remediation(ctx, user, realm, session_id, dmacc);
	free(type);

	return ret;
}


static xml_node_t * build_policy(struct hs20_svc *ctx, const char *user,
				 const char *realm, int use_dmacc)
{
	char *policy_id;
	char fname[200];
	xml_node_t *policy, *node;

	policy_id = db_get_val(ctx, user, realm, "policy", use_dmacc);
	if (policy_id == NULL || strlen(policy_id) == 0) {
		free(policy_id);
		policy_id = strdup("default");
		if (policy_id == NULL)
			return NULL;
	}

	snprintf(fname, sizeof(fname), "%s/spp/policy/%s.xml",
		 ctx->root_dir, policy_id);
	free(policy_id);
	debug_print(ctx, 1, "Use policy file %s", fname);

	policy = node_from_file(ctx->xml, fname);
	if (policy == NULL)
		return NULL;

	node = get_node_uri(ctx->xml, policy, "Policy/PolicyUpdate/URI");
	if (node) {
		char *url;
		url = db_get_osu_config_val(ctx, realm, "policy_url");
		if (url == NULL) {
			xml_node_free(ctx->xml, policy);
			return NULL;
		}
		xml_node_set_text(ctx->xml, node, url);
		free(url);
	}

	node = get_node_uri(ctx->xml, policy, "Policy/PolicyUpdate");
	if (node && use_dmacc) {
		char *pw;
		pw = db_get_val(ctx, user, realm, "osu_password", use_dmacc);
		if (pw == NULL ||
		    build_username_password(ctx, node, user, pw) == NULL) {
			debug_print(ctx, 1, "Failed to add Policy/PolicyUpdate/"
				    "UsernamePassword");
			free(pw);
			xml_node_free(ctx->xml, policy);
			return NULL;
		}
		free(pw);
	}

	return policy;
}


static xml_node_t * hs20_policy_update(struct hs20_svc *ctx,
				       const char *user, const char *realm,
				       const char *session_id, int dmacc)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node;
	xml_node_t *policy;
	char buf[400];
	const char *status;
	char *identity;

	identity = db_get_val(ctx, user, realm, "identity", dmacc);
	if (identity == NULL || strlen(identity) == 0) {
		hs20_eventlog(ctx, user, realm, session_id,
			      "user not found in database for policy update",
			      NULL);
		os_free(identity);
		return build_post_dev_data_response(ctx, NULL, session_id,
						    "Error occurred",
						    "Not found");
	}
	os_free(identity);

	policy = build_policy(ctx, user, realm, dmacc);
	if (!policy) {
		return build_post_dev_data_response(
			ctx, NULL, session_id,
			"No update available at this time", NULL);
	}

	db_add_session(ctx, user, realm, session_id, NULL, NULL, POLICY_UPDATE);

	status = "Update complete, request sppUpdateResponse";
	spp_node = build_post_dev_data_response(ctx, &ns, session_id, status,
						NULL);
	if (spp_node == NULL)
		return NULL;

	snprintf(buf, sizeof(buf),
		 "./Wi-Fi/%s/PerProviderSubscription/Credential1/Policy",
		 realm);

	if (add_update_node(ctx, spp_node, ns, buf, policy) < 0) {
		xml_node_free(ctx->xml, spp_node);
		xml_node_free(ctx->xml, policy);
		return NULL;
	}

	hs20_eventlog_node(ctx, user, realm, session_id, "policy update",
			   policy);
	xml_node_free(ctx->xml, policy);

	return spp_node;
}


static xml_node_t * spp_get_mo(struct hs20_svc *ctx, xml_node_t *node,
			       const char *urn, int *valid, char **ret_err)
{
	xml_node_t *child, *tnds, *mo;
	const char *name;
	char *mo_urn;
	char *str;
	char fname[200];

	*valid = -1;
	if (ret_err)
		*ret_err = NULL;

	xml_node_for_each_child(ctx->xml, child, node) {
		xml_node_for_each_check(ctx->xml, child);
		name = xml_node_get_localname(ctx->xml, child);
		if (strcmp(name, "moContainer") != 0)
			continue;
		mo_urn = xml_node_get_attr_value_ns(ctx->xml, child, SPP_NS_URI,
						    "moURN");
		if (strcasecmp(urn, mo_urn) == 0) {
			xml_node_get_attr_value_free(ctx->xml, mo_urn);
			break;
		}
		xml_node_get_attr_value_free(ctx->xml, mo_urn);
	}

	if (child == NULL)
		return NULL;

	debug_print(ctx, 1, "moContainer text for %s", urn);
	debug_dump_node(ctx, "moContainer", child);

	str = xml_node_get_text(ctx->xml, child);
	debug_print(ctx, 1, "moContainer payload: '%s'", str);
	tnds = xml_node_from_buf(ctx->xml, str);
	xml_node_get_text_free(ctx->xml, str);
	if (tnds == NULL) {
		debug_print(ctx, 1, "could not parse moContainer text");
		return NULL;
	}

	snprintf(fname, sizeof(fname), "%s/spp/dm_ddf-v1_2.dtd", ctx->root_dir);
	if (xml_validate_dtd(ctx->xml, tnds, fname, ret_err) == 0)
		*valid = 1;
	else if (ret_err && *ret_err &&
		 os_strcmp(*ret_err, "No declaration for attribute xmlns of element MgmtTree\n") == 0) {
		free(*ret_err);
		debug_print(ctx, 1, "Ignore OMA-DM DDF DTD validation error for MgmtTree namespace declaration with xmlns attribute");
		*ret_err = NULL;
		*valid = 1;
	} else
		*valid = 0;

	mo = tnds_to_mo(ctx->xml, tnds);
	xml_node_free(ctx->xml, tnds);
	if (mo == NULL) {
		debug_print(ctx, 1, "invalid moContainer for %s", urn);
	}

	return mo;
}


static xml_node_t * spp_exec_upload_mo(struct hs20_svc *ctx,
				       const char *session_id, const char *urn)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *node, *exec_node;

	spp_node = build_post_dev_data_response(ctx, &ns, session_id, "OK",
						NULL);
	if (spp_node == NULL)
		return NULL;

	exec_node = xml_node_create(ctx->xml, spp_node, ns, "exec");

	node = xml_node_create(ctx->xml, exec_node, ns, "uploadMO");
	xml_node_add_attr(ctx->xml, node, ns, "moURN", urn);

	return spp_node;
}


static xml_node_t * hs20_subscription_registration(struct hs20_svc *ctx,
						   const char *realm,
						   const char *session_id,
						   const char *redirect_uri)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *exec_node;
	char uri[300], *val;

	if (db_add_session(ctx, NULL, realm, session_id, NULL, redirect_uri,
			   SUBSCRIPTION_REGISTRATION) < 0)
		return NULL;
	val = db_get_osu_config_val(ctx, realm, "signup_url");
	if (val == NULL)
		return NULL;

	spp_node = build_post_dev_data_response(ctx, &ns, session_id, "OK",
						NULL);
	if (spp_node == NULL)
		return NULL;

	exec_node = xml_node_create(ctx->xml, spp_node, ns, "exec");

	snprintf(uri, sizeof(uri), "%s%s", val, session_id);
	os_free(val);
	xml_node_create_text(ctx->xml, exec_node, ns, "launchBrowserToURI",
			     uri);
	return spp_node;
}


static xml_node_t * hs20_user_input_remediation(struct hs20_svc *ctx,
						const char *user,
						const char *realm, int dmacc,
						const char *session_id)
{
	return build_sub_rem_resp(ctx, user, realm, session_id, 0, dmacc);
}


static char * db_get_osu_config_val(struct hs20_svc *ctx, const char *realm,
				    const char *field)
{
	char *cmd;
	struct get_db_field_data data;

	cmd = sqlite3_mprintf("SELECT value FROM osu_config WHERE realm=%Q AND "
			      "field=%Q", realm, field);
	if (cmd == NULL)
		return NULL;
	debug_print(ctx, 1, "DB: %s", cmd);
	memset(&data, 0, sizeof(data));
	data.field = "value";
	if (sqlite3_exec(ctx->db, cmd, get_db_field, &data, NULL) != SQLITE_OK)
	{
		debug_print(ctx, 1, "DB: Could not find osu_config %s: %s",
			    realm, sqlite3_errmsg(ctx->db));
		sqlite3_free(cmd);
		return NULL;
	}
	sqlite3_free(cmd);

	debug_print(ctx, 1, "DB: return '%s'", data.value);
	return data.value;
}


static xml_node_t * build_pps(struct hs20_svc *ctx,
			      const char *user, const char *realm,
			      const char *pw, const char *cert,
			      int machine_managed)
{
	xml_node_t *pps, *c, *trust, *aaa, *aaa1, *upd, *homesp;
	xml_node_t *cred, *eap, *userpw;

	pps = xml_node_create_root(ctx->xml, NULL, NULL, NULL,
				   "PerProviderSubscription");
	if (pps == NULL)
		return NULL;

	add_text_node(ctx, pps, "UpdateIdentifier", "1");

	c = xml_node_create(ctx->xml, pps, NULL, "Credential1");

	add_text_node(ctx, c, "CredentialPriority", "1");

	aaa = xml_node_create(ctx->xml, c, NULL, "AAAServerTrustRoot");
	aaa1 = xml_node_create(ctx->xml, aaa, NULL, "AAA1");
	add_text_node_conf(ctx, realm, aaa1, "CertURL",
			   "aaa_trust_root_cert_url");
	add_text_node_conf(ctx, realm, aaa1, "CertSHA256Fingerprint",
			   "aaa_trust_root_cert_fingerprint");

	upd = xml_node_create(ctx->xml, c, NULL, "SubscriptionUpdate");
	add_text_node(ctx, upd, "UpdateInterval", "4294967295");
	add_text_node(ctx, upd, "UpdateMethod", "ClientInitiated");
	add_text_node(ctx, upd, "Restriction", "HomeSP");
	add_text_node_conf(ctx, realm, upd, "URI", "spp_http_auth_url");
	trust = xml_node_create(ctx->xml, upd, NULL, "TrustRoot");
	add_text_node_conf(ctx, realm, trust, "CertURL", "trust_root_cert_url");
	add_text_node_conf(ctx, realm, trust, "CertSHA256Fingerprint",
			   "trust_root_cert_fingerprint");

	homesp = xml_node_create(ctx->xml, c, NULL, "HomeSP");
	add_text_node_conf(ctx, realm, homesp, "FriendlyName", "friendly_name");
	add_text_node_conf(ctx, realm, homesp, "FQDN", "fqdn");

	xml_node_create(ctx->xml, c, NULL, "SubscriptionParameters");

	cred = xml_node_create(ctx->xml, c, NULL, "Credential");
	add_creation_date(ctx, cred);
	if (cert) {
		xml_node_t *dc;
		dc = xml_node_create(ctx->xml, cred, NULL,
				     "DigitalCertificate");
		add_text_node(ctx, dc, "CertificateType", "x509v3");
		add_text_node(ctx, dc, "CertSHA256Fingerprint", cert);
	} else {
		userpw = build_username_password(ctx, cred, user, pw);
		add_text_node(ctx, userpw, "MachineManaged",
			      machine_managed ? "TRUE" : "FALSE");
		eap = xml_node_create(ctx->xml, userpw, NULL, "EAPMethod");
		add_text_node(ctx, eap, "EAPType", "21");
		add_text_node(ctx, eap, "InnerMethod", "MS-CHAP-V2");
	}
	add_text_node(ctx, cred, "Realm", realm);

	return pps;
}


static xml_node_t * spp_exec_get_certificate(struct hs20_svc *ctx,
					     const char *session_id,
					     const char *user,
					     const char *realm)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *enroll, *exec_node;
	char *val;
	char password[11];
	char *b64;

	if (new_password(password, sizeof(password)) < 0)
		return NULL;

	spp_node = build_post_dev_data_response(ctx, &ns, session_id, "OK",
						NULL);
	if (spp_node == NULL)
		return NULL;

	exec_node = xml_node_create(ctx->xml, spp_node, ns, "exec");

	enroll = xml_node_create(ctx->xml, exec_node, ns, "getCertificate");
	xml_node_add_attr(ctx->xml, enroll, NULL, "enrollmentProtocol", "EST");

	val = db_get_osu_config_val(ctx, realm, "est_url");
	xml_node_create_text(ctx->xml, enroll, ns, "enrollmentServerURI",
			     val ? val : "");
	os_free(val);
	xml_node_create_text(ctx->xml, enroll, ns, "estUserID", user);

	b64 = (char *) base64_encode((unsigned char *) password,
				     strlen(password), NULL);
	if (b64 == NULL) {
		xml_node_free(ctx->xml, spp_node);
		return NULL;
	}
	xml_node_create_text(ctx->xml, enroll, ns, "estPassword", b64);
	free(b64);

	db_update_session_password(ctx, user, realm, session_id, password);

	return spp_node;
}


static xml_node_t * hs20_user_input_registration(struct hs20_svc *ctx,
						 const char *session_id,
						 int enrollment_done)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *node = NULL;
	xml_node_t *pps, *tnds;
	char buf[400];
	char *str;
	char *user, *realm, *pw, *type, *mm;
	const char *status;
	int cert = 0;
	int machine_managed = 0;
	char *fingerprint;

	user = db_get_session_val(ctx, NULL, NULL, session_id, "user");
	realm = db_get_session_val(ctx, NULL, NULL, session_id, "realm");
	pw = db_get_session_val(ctx, NULL, NULL, session_id, "password");

	if (!user || !realm || !pw) {
		debug_print(ctx, 1, "Could not find session info from DB for "
			    "the new subscription");
		free(user);
		free(realm);
		free(pw);
		return NULL;
	}

	mm = db_get_session_val(ctx, NULL, NULL, session_id, "machine_managed");
	if (mm && atoi(mm))
		machine_managed = 1;
	free(mm);

	type = db_get_session_val(ctx, NULL, NULL, session_id, "type");
	if (type && strcmp(type, "cert") == 0)
		cert = 1;
	free(type);

	if (cert && !enrollment_done) {
		xml_node_t *ret;
		hs20_eventlog(ctx, user, realm, session_id,
			      "request client certificate enrollment", NULL);
		ret = spp_exec_get_certificate(ctx, session_id, user, realm);
		free(user);
		free(realm);
		free(pw);
		return ret;
	}

	if (!cert && strlen(pw) == 0) {
		machine_managed = 1;
		free(pw);
		pw = malloc(11);
		if (pw == NULL || new_password(pw, 11) < 0) {
			free(user);
			free(realm);
			free(pw);
			return NULL;
		}
	}

	status = "Provisioning complete, request sppUpdateResponse";
	spp_node = build_post_dev_data_response(ctx, &ns, session_id, status,
						NULL);
	if (spp_node == NULL)
		return NULL;

	fingerprint = db_get_session_val(ctx, NULL, NULL, session_id, "cert");
	pps = build_pps(ctx, user, realm, pw,
			fingerprint ? fingerprint : NULL, machine_managed);
	free(fingerprint);
	if (!pps) {
		xml_node_free(ctx->xml, spp_node);
		free(user);
		free(realm);
		free(pw);
		return NULL;
	}

	debug_print(ctx, 1, "Request DB subscription registration on success "
		    "notification");
	if (machine_managed) {
		db_update_session_password(ctx, user, realm, session_id, pw);
		db_update_session_machine_managed(ctx, user, realm, session_id,
						  machine_managed);
	}
	db_add_session_pps(ctx, user, realm, session_id, pps);

	hs20_eventlog_node(ctx, user, realm, session_id,
			   "new subscription", pps);
	free(user);
	free(pw);

	tnds = mo_to_tnds(ctx->xml, pps, 0, URN_HS20_PPS, NULL);
	xml_node_free(ctx->xml, pps);
	if (!tnds) {
		xml_node_free(ctx->xml, spp_node);
		free(realm);
		return NULL;
	}

	str = xml_node_to_str(ctx->xml, tnds);
	xml_node_free(ctx->xml, tnds);
	if (str == NULL) {
		xml_node_free(ctx->xml, spp_node);
		free(realm);
		return NULL;
	}

	node = xml_node_create_text(ctx->xml, spp_node, ns, "addMO", str);
	free(str);
	snprintf(buf, sizeof(buf), "./Wi-Fi/%s/PerProviderSubscription", realm);
	free(realm);
	xml_node_add_attr(ctx->xml, node, ns, "managementTreeURI", buf);
	xml_node_add_attr(ctx->xml, node, ns, "moURN", URN_HS20_PPS);

	return spp_node;
}


static xml_node_t * hs20_user_input_free_remediation(struct hs20_svc *ctx,
						     const char *user,
						     const char *realm,
						     const char *session_id)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node;
	xml_node_t *cred;
	char buf[400];
	char *status;
	char *free_account, *pw;

	free_account = db_get_osu_config_val(ctx, realm, "free_account");
	if (free_account == NULL)
		return NULL;
	pw = db_get_val(ctx, free_account, realm, "password", 0);
	if (pw == NULL) {
		free(free_account);
		return NULL;
	}

	cred = build_credential_pw(ctx, free_account, realm, pw);
	free(free_account);
	free(pw);
	if (!cred) {
		xml_node_free(ctx->xml, cred);
		return NULL;
	}

	status = "Remediation complete, request sppUpdateResponse";
	spp_node = build_post_dev_data_response(ctx, &ns, session_id, status,
						NULL);
	if (spp_node == NULL)
		return NULL;

	snprintf(buf, sizeof(buf),
		 "./Wi-Fi/%s/PerProviderSubscription/Credential1/Credential",
		 realm);

	if (add_update_node(ctx, spp_node, ns, buf, cred) < 0) {
		xml_node_free(ctx->xml, spp_node);
		return NULL;
	}

	hs20_eventlog_node(ctx, user, realm, session_id,
			   "free/public remediation", cred);
	xml_node_free(ctx->xml, cred);

	return spp_node;
}


static xml_node_t * hs20_user_input_complete(struct hs20_svc *ctx,
					     const char *user,
					     const char *realm, int dmacc,
					     const char *session_id)
{
	char *val;
	enum hs20_session_operation oper;

	val = db_get_session_val(ctx, user, realm, session_id, "operation");
	if (val == NULL) {
		debug_print(ctx, 1, "No session %s found to continue",
			    session_id);
		return NULL;
	}
	oper = atoi(val);
	free(val);

	if (oper == USER_REMEDIATION) {
		return hs20_user_input_remediation(ctx, user, realm, dmacc,
						   session_id);
	}

	if (oper == FREE_REMEDIATION) {
		return hs20_user_input_free_remediation(ctx, user, realm,
							session_id);
	}

	if (oper == SUBSCRIPTION_REGISTRATION) {
		return hs20_user_input_registration(ctx, session_id, 0);
	}

	debug_print(ctx, 1, "User session %s not in state for user input "
		    "completion", session_id);
	return NULL;
}


static xml_node_t * hs20_cert_enroll_completed(struct hs20_svc *ctx,
					       const char *user,
					       const char *realm, int dmacc,
					       const char *session_id)
{
	char *val;
	enum hs20_session_operation oper;

	val = db_get_session_val(ctx, user, realm, session_id, "operation");
	if (val == NULL) {
		debug_print(ctx, 1, "No session %s found to continue",
			    session_id);
		return NULL;
	}
	oper = atoi(val);
	free(val);

	if (oper == SUBSCRIPTION_REGISTRATION)
		return hs20_user_input_registration(ctx, session_id, 1);

	debug_print(ctx, 1, "User session %s not in state for certificate "
		    "enrollment completion", session_id);
	return NULL;
}


static xml_node_t * hs20_cert_enroll_failed(struct hs20_svc *ctx,
					    const char *user,
					    const char *realm, int dmacc,
					    const char *session_id)
{
	char *val;
	enum hs20_session_operation oper;
	xml_node_t *spp_node, *node;
	char *status;
	xml_namespace_t *ns;

	val = db_get_session_val(ctx, user, realm, session_id, "operation");
	if (val == NULL) {
		debug_print(ctx, 1, "No session %s found to continue",
			    session_id);
		return NULL;
	}
	oper = atoi(val);
	free(val);

	if (oper != SUBSCRIPTION_REGISTRATION) {
		debug_print(ctx, 1, "User session %s not in state for "
			    "enrollment failure", session_id);
		return NULL;
	}

	status = "Error occurred";
	spp_node = build_post_dev_data_response(ctx, &ns, session_id, status,
						NULL);
	if (spp_node == NULL)
		return NULL;
	node = xml_node_create(ctx->xml, spp_node, ns, "sppError");
	xml_node_add_attr(ctx->xml, node, NULL, "errorCode",
			  "Credentials cannot be provisioned at this time");
	db_remove_session(ctx, user, realm, session_id);

	return spp_node;
}


static xml_node_t * hs20_spp_post_dev_data(struct hs20_svc *ctx,
					   xml_node_t *node,
					   const char *user,
					   const char *realm,
					   const char *session_id,
					   int dmacc)
{
	const char *req_reason;
	char *redirect_uri = NULL;
	char *req_reason_buf = NULL;
	char str[200];
	xml_node_t *ret = NULL, *devinfo = NULL, *devdetail = NULL;
	xml_node_t *mo;
	char *version;
	int valid;
	char *supp, *pos;
	char *err;

	version = xml_node_get_attr_value_ns(ctx->xml, node, SPP_NS_URI,
					     "sppVersion");
	if (version == NULL || strstr(version, "1.0") == NULL) {
		ret = build_post_dev_data_response(
			ctx, NULL, session_id, "Error occurred",
			"SPP version not supported");
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "Unsupported sppVersion", ret);
		xml_node_get_attr_value_free(ctx->xml, version);
		return ret;
	}
	xml_node_get_attr_value_free(ctx->xml, version);

	mo = get_node(ctx->xml, node, "supportedMOList");
	if (mo == NULL) {
		ret = build_post_dev_data_response(
			ctx, NULL, session_id, "Error occurred",
			"Other");
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "No supportedMOList element", ret);
		return ret;
	}
	supp = xml_node_get_text(ctx->xml, mo);
	for (pos = supp; pos && *pos; pos++)
		*pos = tolower(*pos);
	if (supp == NULL ||
	    strstr(supp, URN_OMA_DM_DEVINFO) == NULL ||
	    strstr(supp, URN_OMA_DM_DEVDETAIL) == NULL ||
	    strstr(supp, URN_HS20_PPS) == NULL) {
		xml_node_get_text_free(ctx->xml, supp);
		ret = build_post_dev_data_response(
			ctx, NULL, session_id, "Error occurred",
			"One or more mandatory MOs not supported");
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "Unsupported MOs", ret);
		return ret;
	}
	xml_node_get_text_free(ctx->xml, supp);

	req_reason_buf = xml_node_get_attr_value(ctx->xml, node,
						 "requestReason");
	if (req_reason_buf == NULL) {
		debug_print(ctx, 1, "No requestReason attribute");
		return NULL;
	}
	req_reason = req_reason_buf;

	redirect_uri = xml_node_get_attr_value(ctx->xml, node, "redirectURI");

	debug_print(ctx, 1, "requestReason: %s  sessionID: %s  redirectURI: %s",
		    req_reason, session_id, redirect_uri);
	snprintf(str, sizeof(str), "sppPostDevData: requestReason=%s",
		 req_reason);
	hs20_eventlog(ctx, user, realm, session_id, str, NULL);

	devinfo = spp_get_mo(ctx, node, URN_OMA_DM_DEVINFO, &valid, &err);
	if (devinfo == NULL) {
		ret = build_post_dev_data_response(ctx, NULL, session_id,
						   "Error occurred", "Other");
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "No DevInfo moContainer in sppPostDevData",
				   ret);
		os_free(err);
		goto out;
	}

	hs20_eventlog_node(ctx, user, realm, session_id,
			   "Received DevInfo MO", devinfo);
	if (valid == 0) {
		hs20_eventlog(ctx, user, realm, session_id,
			      "OMA-DM DDF DTD validation errors in DevInfo MO",
			      err);
		ret = build_post_dev_data_response(ctx, NULL, session_id,
						   "Error occurred", "Other");
		os_free(err);
		goto out;
	}
	os_free(err);
	if (user)
		db_update_mo(ctx, user, realm, "devinfo", devinfo);

	devdetail = spp_get_mo(ctx, node, URN_OMA_DM_DEVDETAIL, &valid, &err);
	if (devdetail == NULL) {
		ret = build_post_dev_data_response(ctx, NULL, session_id,
						   "Error occurred", "Other");
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "No DevDetail moContainer in sppPostDevData",
				   ret);
		os_free(err);
		goto out;
	}

	hs20_eventlog_node(ctx, user, realm, session_id,
			   "Received DevDetail MO", devdetail);
	if (valid == 0) {
		hs20_eventlog(ctx, user, realm, session_id,
			      "OMA-DM DDF DTD validation errors "
			      "in DevDetail MO", err);
		ret = build_post_dev_data_response(ctx, NULL, session_id,
						   "Error occurred", "Other");
		os_free(err);
		goto out;
	}
	os_free(err);
	if (user)
		db_update_mo(ctx, user, realm, "devdetail", devdetail);

	if (user)
		mo = spp_get_mo(ctx, node, URN_HS20_PPS, &valid, &err);
	else {
		mo = NULL;
		err = NULL;
	}
	if (user && mo) {
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "Received PPS MO", mo);
		if (valid == 0) {
			hs20_eventlog(ctx, user, realm, session_id,
				      "OMA-DM DDF DTD validation errors "
				      "in PPS MO", err);
			xml_node_get_attr_value_free(ctx->xml, redirect_uri);
			os_free(err);
			return build_post_dev_data_response(
				ctx, NULL, session_id,
				"Error occurred", "Other");
		}
		db_update_mo(ctx, user, realm, "pps", mo);
		db_update_val(ctx, user, realm, "fetch_pps", "0", dmacc);
		xml_node_free(ctx->xml, mo);
	}
	os_free(err);

	if (user && !mo) {
		char *fetch;
		int fetch_pps;

		fetch = db_get_val(ctx, user, realm, "fetch_pps", dmacc);
		fetch_pps = fetch ? atoi(fetch) : 0;
		free(fetch);

		if (fetch_pps) {
			enum hs20_session_operation oper;
			if (strcasecmp(req_reason, "Subscription remediation")
			    == 0)
				oper = CONTINUE_SUBSCRIPTION_REMEDIATION;
			else if (strcasecmp(req_reason, "Policy update") == 0)
				oper = CONTINUE_POLICY_UPDATE;
			else
				oper = NO_OPERATION;
			if (db_add_session(ctx, user, realm, session_id, NULL,
					   NULL, oper) < 0)
				goto out;

			ret = spp_exec_upload_mo(ctx, session_id,
						 URN_HS20_PPS);
			hs20_eventlog_node(ctx, user, realm, session_id,
					   "request PPS MO upload",
					   ret);
			goto out;
		}
	}

	if (user && strcasecmp(req_reason, "MO upload") == 0) {
		char *val = db_get_session_val(ctx, user, realm, session_id,
					       "operation");
		enum hs20_session_operation oper;
		if (!val) {
			debug_print(ctx, 1, "No session %s found to continue",
				    session_id);
			goto out;
		}
		oper = atoi(val);
		free(val);
		if (oper == CONTINUE_SUBSCRIPTION_REMEDIATION)
			req_reason = "Subscription remediation";
		else if (oper == CONTINUE_POLICY_UPDATE)
			req_reason = "Policy update";
		else {
			debug_print(ctx, 1,
				    "No pending operation in session %s",
				    session_id);
			goto out;
		}
	}

	if (strcasecmp(req_reason, "Subscription registration") == 0) {
		ret = hs20_subscription_registration(ctx, realm, session_id,
						     redirect_uri);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "subscription registration response",
				   ret);
		goto out;
	}
	if (user && strcasecmp(req_reason, "Subscription remediation") == 0) {
		ret = hs20_subscription_remediation(ctx, user, realm,
						    session_id, dmacc,
						    redirect_uri);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "subscription remediation response",
				   ret);
		goto out;
	}
	if (user && strcasecmp(req_reason, "Policy update") == 0) {
		ret = hs20_policy_update(ctx, user, realm, session_id, dmacc);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "policy update response",
				   ret);
		goto out;
	}

	if (strcasecmp(req_reason, "User input completed") == 0) {
		if (devinfo)
			db_add_session_devinfo(ctx, session_id, devinfo);
		if (devdetail)
			db_add_session_devdetail(ctx, session_id, devdetail);
		ret = hs20_user_input_complete(ctx, user, realm, dmacc,
					       session_id);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "user input completed response", ret);
		goto out;
	}

	if (strcasecmp(req_reason, "Certificate enrollment completed") == 0) {
		ret = hs20_cert_enroll_completed(ctx, user, realm, dmacc,
						 session_id);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "certificate enrollment response", ret);
		goto out;
	}

	if (strcasecmp(req_reason, "Certificate enrollment failed") == 0) {
		ret = hs20_cert_enroll_failed(ctx, user, realm, dmacc,
					      session_id);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "certificate enrollment failed response",
				   ret);
		goto out;
	}

	debug_print(ctx, 1, "Unsupported requestReason '%s' user '%s'",
		    req_reason, user);
out:
	xml_node_get_attr_value_free(ctx->xml, req_reason_buf);
	xml_node_get_attr_value_free(ctx->xml, redirect_uri);
	if (devinfo)
		xml_node_free(ctx->xml, devinfo);
	if (devdetail)
		xml_node_free(ctx->xml, devdetail);
	return ret;
}


static xml_node_t * build_spp_exchange_complete(struct hs20_svc *ctx,
						const char *session_id,
						const char *status,
						const char *error_code)
{
	xml_namespace_t *ns;
	xml_node_t *spp_node, *node;

	spp_node = xml_node_create_root(ctx->xml, SPP_NS_URI, "spp", &ns,
					"sppExchangeComplete");


	xml_node_add_attr(ctx->xml, spp_node, ns, "sppVersion", "1.0");
	xml_node_add_attr(ctx->xml, spp_node, ns, "sessionID", session_id);
	xml_node_add_attr(ctx->xml, spp_node, ns, "sppStatus", status);

	if (error_code) {
		node = xml_node_create(ctx->xml, spp_node, ns, "sppError");
		xml_node_add_attr(ctx->xml, node, NULL, "errorCode",
				  error_code);
	}

	return spp_node;
}


static int add_subscription(struct hs20_svc *ctx, const char *session_id)
{
	char *user, *realm, *pw, *pw_mm, *pps, *str;
	char *sql;
	int ret = -1;
	char *free_account;
	int free_acc;
	char *type;
	int cert = 0;
	char *cert_pem, *fingerprint;

	user = db_get_session_val(ctx, NULL, NULL, session_id, "user");
	realm = db_get_session_val(ctx, NULL, NULL, session_id, "realm");
	pw = db_get_session_val(ctx, NULL, NULL, session_id, "password");
	pw_mm = db_get_session_val(ctx, NULL, NULL, session_id,
				   "machine_managed");
	pps = db_get_session_val(ctx, NULL, NULL, session_id, "pps");
	cert_pem = db_get_session_val(ctx, NULL, NULL, session_id, "cert_pem");
	fingerprint = db_get_session_val(ctx, NULL, NULL, session_id, "cert");
	type = db_get_session_val(ctx, NULL, NULL, session_id, "type");
	if (type && strcmp(type, "cert") == 0)
		cert = 1;
	free(type);

	if (!user || !realm || !pw) {
		debug_print(ctx, 1, "Could not find session info from DB for "
			    "the new subscription");
		goto out;
	}

	free_account = db_get_osu_config_val(ctx, realm, "free_account");
	free_acc = free_account && strcmp(free_account, user) == 0;
	free(free_account);

	debug_print(ctx, 1,
		    "New subscription: user='%s' realm='%s' free_acc=%d",
		    user, realm, free_acc);
	debug_print(ctx, 1, "New subscription: pps='%s'", pps);

	sql = sqlite3_mprintf("UPDATE eventlog SET user=%Q, realm=%Q WHERE "
			      "sessionid=%Q AND (user='' OR user IS NULL)",
			      user, realm, session_id);
	if (sql) {
		debug_print(ctx, 1, "DB: %s", sql);
		if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
			debug_print(ctx, 1, "Failed to update eventlog in "
				    "sqlite database: %s",
				    sqlite3_errmsg(ctx->db));
		}
		sqlite3_free(sql);
	}

	if (free_acc) {
		hs20_eventlog(ctx, user, realm, session_id,
			      "completed shared free account registration",
			      NULL);
		ret = 0;
		goto out;
	}

	sql = sqlite3_mprintf("INSERT INTO users(identity,realm,phase2,"
			      "methods,cert,cert_pem,machine_managed) VALUES "
			      "(%Q,%Q,1,%Q,%Q,%Q,%d)",
			      user, realm, cert ? "TLS" : "TTLS-MSCHAPV2",
			      fingerprint ? fingerprint : "",
			      cert_pem ? cert_pem : "",
			      pw_mm && atoi(pw_mm) ? 1 : 0);
	if (sql == NULL)
		goto out;
	debug_print(ctx, 1, "DB: %s", sql);
	if (sqlite3_exec(ctx->db, sql, NULL, NULL, NULL) != SQLITE_OK) {
		debug_print(ctx, 1, "Failed to add user in sqlite database: %s",
			    sqlite3_errmsg(ctx->db));
		sqlite3_free(sql);
		goto out;
	}
	sqlite3_free(sql);

	if (cert)
		ret = 0;
	else
		ret = update_password(ctx, user, realm, pw, 0);
	if (ret < 0) {
		sql = sqlite3_mprintf("DELETE FROM users WHERE identity=%Q AND "
				      "realm=%Q AND phase2=1",
				      user, realm);
		if (sql) {
			debug_print(ctx, 1, "DB: %s", sql);
			sqlite3_exec(ctx->db, sql, NULL, NULL, NULL);
			sqlite3_free(sql);
		}
	}

	if (pps)
		db_update_mo_str(ctx, user, realm, "pps", pps);

	str = db_get_session_val(ctx, NULL, NULL, session_id, "devinfo");
	if (str) {
		db_update_mo_str(ctx, user, realm, "devinfo", str);
		free(str);
	}

	str = db_get_session_val(ctx, NULL, NULL, session_id, "devdetail");
	if (str) {
		db_update_mo_str(ctx, user, realm, "devdetail", str);
		free(str);
	}

	if (ret == 0) {
		hs20_eventlog(ctx, user, realm, session_id,
			      "completed subscription registration", NULL);
	}

out:
	free(user);
	free(realm);
	free(pw);
	free(pw_mm);
	free(pps);
	free(cert_pem);
	free(fingerprint);
	return ret;
}


static xml_node_t * hs20_spp_update_response(struct hs20_svc *ctx,
					     xml_node_t *node,
					     const char *user,
					     const char *realm,
					     const char *session_id,
					     int dmacc)
{
	char *status;
	xml_node_t *ret;
	char *val;
	enum hs20_session_operation oper;

	status = xml_node_get_attr_value_ns(ctx->xml, node, SPP_NS_URI,
					    "sppStatus");
	if (status == NULL) {
		debug_print(ctx, 1, "No sppStatus attribute");
		return NULL;
	}

	debug_print(ctx, 1, "sppUpdateResponse: sppStatus: %s  sessionID: %s",
		    status, session_id);

	val = db_get_session_val(ctx, user, realm, session_id, "operation");
	if (!val) {
		debug_print(ctx, 1,
			    "No session active for user: %s  sessionID: %s",
			    user, session_id);
		oper = NO_OPERATION;
	} else
		oper = atoi(val);

	if (strcasecmp(status, "OK") == 0) {
		char *new_pw = NULL;

		xml_node_get_attr_value_free(ctx->xml, status);

		if (oper == USER_REMEDIATION) {
			new_pw = db_get_session_val(ctx, user, realm,
						    session_id, "password");
			if (new_pw == NULL || strlen(new_pw) == 0) {
				free(new_pw);
				ret = build_spp_exchange_complete(
					ctx, session_id, "Error occurred",
					"Other");
				hs20_eventlog_node(ctx, user, realm,
						   session_id, "No password "
						   "had been assigned for "
						   "session", ret);
				db_remove_session(ctx, user, realm, session_id);
				return ret;
			}
			oper = UPDATE_PASSWORD;
		}
		if (oper == UPDATE_PASSWORD) {
			if (!new_pw) {
				new_pw = db_get_session_val(ctx, user, realm,
							    session_id,
							    "password");
				if (!new_pw) {
					db_remove_session(ctx, user, realm,
							  session_id);
					return NULL;
				}
			}
			debug_print(ctx, 1, "Update user '%s' password in DB",
				    user);
			if (update_password(ctx, user, realm, new_pw, dmacc) <
			    0) {
				debug_print(ctx, 1, "Failed to update user "
					    "'%s' password in DB", user);
				ret = build_spp_exchange_complete(
					ctx, session_id, "Error occurred",
					"Other");
				hs20_eventlog_node(ctx, user, realm,
						   session_id, "Failed to "
						   "update database", ret);
				db_remove_session(ctx, user, realm, session_id);
				return ret;
			}
			hs20_eventlog(ctx, user, realm,
				      session_id, "Updated user password "
				      "in database", NULL);
		}
		if (oper == SUBSCRIPTION_REGISTRATION) {
			if (add_subscription(ctx, session_id) < 0) {
				debug_print(ctx, 1, "Failed to add "
					    "subscription into DB");
				ret = build_spp_exchange_complete(
					ctx, session_id, "Error occurred",
					"Other");
				hs20_eventlog_node(ctx, user, realm,
						   session_id, "Failed to "
						   "update database", ret);
				db_remove_session(ctx, user, realm, session_id);
				return ret;
			}
		}
		if (oper == POLICY_REMEDIATION || oper == POLICY_UPDATE) {
			char *val;
			val = db_get_val(ctx, user, realm, "remediation",
					 dmacc);
			if (val && strcmp(val, "policy") == 0)
				db_update_val(ctx, user, realm, "remediation",
					      "", dmacc);
			free(val);
		}
		ret = build_spp_exchange_complete(
			ctx, session_id,
			"Exchange complete, release TLS connection", NULL);
		hs20_eventlog_node(ctx, user, realm, session_id,
				   "Exchange completed", ret);
		db_remove_session(ctx, user, realm, session_id);
		return ret;
	}

	ret = build_spp_exchange_complete(ctx, session_id, "Error occurred",
					  "Other");
	hs20_eventlog_node(ctx, user, realm, session_id, "Error occurred", ret);
	db_remove_session(ctx, user, realm, session_id);
	xml_node_get_attr_value_free(ctx->xml, status);
	return ret;
}


#define SPP_SESSION_ID_LEN 16

static char * gen_spp_session_id(void)
{
	FILE *f;
	int i;
	char *session;

	session = os_malloc(SPP_SESSION_ID_LEN * 2 + 1);
	if (session == NULL)
		return NULL;

	f = fopen("/dev/urandom", "r");
	if (f == NULL) {
		os_free(session);
		return NULL;
	}
	for (i = 0; i < SPP_SESSION_ID_LEN; i++)
		os_snprintf(session + i * 2, 3, "%02x", fgetc(f));

	fclose(f);
	return session;
}

xml_node_t * hs20_spp_server_process(struct hs20_svc *ctx, xml_node_t *node,
				     const char *auth_user,
				     const char *auth_realm, int dmacc)
{
	xml_node_t *ret = NULL;
	char *session_id;
	const char *op_name;
	char *xml_err;
	char fname[200];

	debug_dump_node(ctx, "received request", node);

	if (!dmacc && auth_user && auth_realm) {
		char *real;
		real = db_get_val(ctx, auth_user, auth_realm, "identity", 0);
		if (!real) {
			real = db_get_val(ctx, auth_user, auth_realm,
					  "identity", 1);
			if (real)
				dmacc = 1;
		}
		os_free(real);
	}

	snprintf(fname, sizeof(fname), "%s/spp/spp.xsd", ctx->root_dir);
	if (xml_validate(ctx->xml, node, fname, &xml_err) < 0) {
		/*
		 * We may not be able to extract the sessionID from invalid
		 * input, but well, we can try.
		 */
		session_id = xml_node_get_attr_value_ns(ctx->xml, node,
							SPP_NS_URI,
							"sessionID");
		debug_print(ctx, 1, "SPP message failed validation");
		hs20_eventlog_node(ctx, auth_user, auth_realm, session_id,
				   "SPP message failed validation", node);
		hs20_eventlog(ctx, auth_user, auth_realm, session_id,
			      "Validation errors", xml_err);
		os_free(xml_err);
		xml_node_get_attr_value_free(ctx->xml, session_id);
		/* TODO: what to return here? */
		ret = xml_node_create_root(ctx->xml, NULL, NULL, NULL,
					   "SppValidationError");
		return ret;
	}

	session_id = xml_node_get_attr_value_ns(ctx->xml, node, SPP_NS_URI,
						"sessionID");
	if (session_id) {
		char *tmp;
		debug_print(ctx, 1, "Received sessionID %s", session_id);
		tmp = os_strdup(session_id);
		xml_node_get_attr_value_free(ctx->xml, session_id);
		if (tmp == NULL)
			return NULL;
		session_id = tmp;
	} else {
		session_id = gen_spp_session_id();
		if (session_id == NULL) {
			debug_print(ctx, 1, "Failed to generate sessionID");
			return NULL;
		}
		debug_print(ctx, 1, "Generated sessionID %s", session_id);
	}

	op_name = xml_node_get_localname(ctx->xml, node);
	if (op_name == NULL) {
		debug_print(ctx, 1, "Could not get op_name");
		return NULL;
	}

	if (strcmp(op_name, "sppPostDevData") == 0) {
		hs20_eventlog_node(ctx, auth_user, auth_realm, session_id,
				   "sppPostDevData received and validated",
				   node);
		ret = hs20_spp_post_dev_data(ctx, node, auth_user, auth_realm,
					     session_id, dmacc);
	} else if (strcmp(op_name, "sppUpdateResponse") == 0) {
		hs20_eventlog_node(ctx, auth_user, auth_realm, session_id,
				   "sppUpdateResponse received and validated",
				   node);
		ret = hs20_spp_update_response(ctx, node, auth_user,
					       auth_realm, session_id, dmacc);
	} else {
		hs20_eventlog_node(ctx, auth_user, auth_realm, session_id,
				   "Unsupported SPP message received and "
				   "validated", node);
		debug_print(ctx, 1, "Unsupported operation '%s'", op_name);
		/* TODO: what to return here? */
		ret = xml_node_create_root(ctx->xml, NULL, NULL, NULL,
					   "SppUnknownCommandError");
	}
	os_free(session_id);

	if (ret == NULL) {
		/* TODO: what to return here? */
		ret = xml_node_create_root(ctx->xml, NULL, NULL, NULL,
					   "SppInternalError");
	}

	return ret;
}


int hs20_spp_server_init(struct hs20_svc *ctx)
{
	char fname[200];
	ctx->db = NULL;
	snprintf(fname, sizeof(fname), "%s/AS/DB/eap_user.db", ctx->root_dir);
	if (sqlite3_open(fname, &ctx->db)) {
		printf("Failed to open sqlite database: %s\n",
		       sqlite3_errmsg(ctx->db));
		sqlite3_close(ctx->db);
		return -1;
	}

	return 0;
}


void hs20_spp_server_deinit(struct hs20_svc *ctx)
{
	sqlite3_close(ctx->db);
	ctx->db = NULL;
}
