diff --git a/configs/ari.conf.sample b/configs/ari.conf.sample
new file mode 100644
index 0000000000000000000000000000000000000000..11e2b065e77ab54e305ae3dea023bf5ab4220187
--- /dev/null
+++ b/configs/ari.conf.sample
@@ -0,0 +1,23 @@
+[general]
+enabled = yes		; When set to no, stasis-http support is disabled.
+;pretty = no		; When set to yes, responses from stasis-http are
+;			; formatted to be human readable.
+;allowed_origins =	; Comma separated list of allowed origins, for
+;			; Cross-Origin Resource Sharing. May be set to * to
+;			; allow all origins.
+;auth_realm =		; Realm to use for authentication. Defaults to Asterisk
+;			; REST Interface.
+
+;[user-username]
+;read_only = no		; When set to yes, user is only authorized for
+;			; read-only requests.
+;
+;password =		; Crypted or plaintext password (see password_format).
+;
+; password_format may be set to plain (the default) or crypt. When set to crypt,
+; crypt(3) is used to validate the password. A crypted password can be generated
+; using mkpasswd -m sha-512.
+;
+; When set to plain, the password is in plaintext.
+;
+;password_format = plain
diff --git a/configs/stasis_http.conf.sample b/configs/stasis_http.conf.sample
deleted file mode 100644
index 1527a32be2a324226e6028f1f58f974623a0811e..0000000000000000000000000000000000000000
--- a/configs/stasis_http.conf.sample
+++ /dev/null
@@ -1,25 +0,0 @@
-[general]
-enabled = yes		; When set to no, stasis-http support is disabled
-;pretty = no		; When set to yes, responses from stasis-http are
-;			; formatted to be human readable
-;allowed_origins =	; Comma separated list of allowed origins, for
-;			; Cross-Origin Resource Sharing. May be set to * to allow
-;			; all origins.
-
-;[user-username]
-;read_only = no		; When set to yes, user is only authorized for
-;			; read-only requests
-;
-; If a password is specified, user must authenticate using HTTP Basic
-; authentication. If no password is specified, then the user may authenticate
-; simply by adding ?api_key=username to their requests.
-;
-;password =		; Crypted or plaintext password (see crypt_password)
-;
-; crypt_password may be set to crypt (the default) or plain. When set to crypt,
-; crypt(3) is used to encrypt the password. A crypted password can be generated
-; using mkpasswd -m sha-512.
-;
-; When set to plain, the password is in plaintext
-;
-;crypt_password = plain
diff --git a/main/Makefile b/main/Makefile
index 2949653651ee62ad537d226f9c5451e0027bac96..62ae6d4fb3b96a2a48e2c105aa889627a5d64267 100644
--- a/main/Makefile
+++ b/main/Makefile
@@ -37,6 +37,7 @@ AST_LIBS+=$(SQLITE3_LIB)
 AST_LIBS+=$(ASTSSL_LIBS)
 AST_LIBS+=$(JANSSON_LIB)
 AST_LIBS+=$(UUID_LIB)
+AST_LIBS+=$(CRYPT_LIB)
 
 ifneq ($(findstring $(OSARCH), linux-gnu uclinux linux-uclibc kfreebsd-gnu),)
   ifneq ($(findstring LOADABLE_MODULES,$(MENUSELECT_CFLAGS)),)
@@ -153,6 +154,7 @@ db.o: _ASTCFLAGS+=$(SQLITE3_INCLUDE)
 asterisk.o: _ASTCFLAGS+=$(LIBEDIT_INCLUDE)
 cli.o: _ASTCFLAGS+=$(LIBEDIT_INCLUDE)
 json.o: _ASTCFLAGS+=$(JANSSON_INCLUDE)
+util.o: _ASTCFLAGS+=$(CRYPT_INCLUDE)
 uuid.o: _ASTCFLAGS+=$(UUID_INCLUDE)
 
 ifneq ($(findstring ENABLE_UPLOADS,$(MENUSELECT_CFLAGS)),)
diff --git a/main/http.c b/main/http.c
index c7dc623a5cc047de8dd2509c81e11629b46bf571..d459eb1841dd84a872d468204486179d89b6a67b 100644
--- a/main/http.c
+++ b/main/http.c
@@ -867,6 +867,93 @@ struct ast_variable *ast_http_get_cookies(struct ast_variable *headers)
 	return cookies;
 }
 
+static struct ast_http_auth *auth_create(const char *userid,
+	const char *password)
+{
+	RAII_VAR(struct ast_http_auth *, auth, NULL, ao2_cleanup);
+	size_t userid_len;
+	size_t password_len;
+
+	if (!userid || !password) {
+		ast_log(LOG_ERROR, "Invalid userid/password\n");
+		return NULL;
+	}
+
+	userid_len = strlen(userid) + 1;
+	password_len = strlen(password) + 1;
+
+	/* Allocate enough room to store everything in one memory block */
+	auth = ao2_alloc(sizeof(*auth) + userid_len + password_len, NULL);
+	if (!auth) {
+		return NULL;
+	}
+
+	/* Put the userid right after the struct */
+	auth->userid = (char *)(auth + 1);
+	strcpy(auth->userid, userid);
+
+	/* Put the password right after the userid */
+	auth->password = auth->userid + userid_len;
+	strcpy(auth->password, password);
+
+	ao2_ref(auth, +1);
+	return auth;
+}
+
+#define BASIC_PREFIX "Basic "
+#define BASIC_LEN 6 /*!< strlen(BASIC_PREFIX) */
+
+struct ast_http_auth *ast_http_get_auth(struct ast_variable *headers)
+{
+	struct ast_variable *v;
+
+	for (v = headers; v; v = v->next) {
+		const char *base64;
+		char decoded[256] = {};
+		char *username;
+		char *password;
+		int cnt;
+
+		if (strcasecmp("Authorization", v->name) != 0) {
+			continue;
+		}
+
+		if (!ast_begins_with(v->value, BASIC_PREFIX)) {
+			ast_log(LOG_DEBUG,
+				"Unsupported Authorization scheme\n");
+			continue;
+		}
+
+		/* Basic auth header parsing. RFC 2617, section 2.
+		 *   credentials = "Basic" basic-credentials
+		 *   basic-credentials = base64-user-pass
+		 *   base64-user-pass  = <base64 encoding of user-pass,
+		 *                        except not limited to 76 char/line>
+		 *   user-pass   = userid ":" password
+		 */
+
+		base64 = v->value + BASIC_LEN;
+
+		/* This will truncate "userid:password" lines to
+		 * sizeof(decoded). The array is long enough that this shouldn't
+		 * be a problem */
+		cnt = ast_base64decode((unsigned char*)decoded, base64,
+			sizeof(decoded) - 1);
+		ast_assert(cnt < sizeof(decoded));
+
+		/* Split the string at the colon */
+		password = decoded;
+		username = strsep(&password, ":");
+		if (!password) {
+			ast_log(LOG_WARNING, "Invalid Authorization header\n");
+			return NULL;
+		}
+
+		return auth_create(username, password);
+	}
+
+	return NULL;
+}
 
 static void *httpd_helper_thread(void *data)
 {
diff --git a/main/utils.c b/main/utils.c
index 1007254875eae441ac19113dfd4474393a872976..208a4d32614d01f0bab6fdbe808ed8cb3c94ceb6 100644
--- a/main/utils.c
+++ b/main/utils.c
@@ -32,12 +32,14 @@
 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 
 #include <ctype.h>
+#include <fcntl.h>
 #include <sys/stat.h>
 #include <sys/stat.h>
-
-#include <fcntl.h>
-
 #include <sys/syscall.h>
+#include <unistd.h>
+#if defined(HAVE_CRYPT_R)
+#include <crypt.h>
+#endif
 #if defined(__APPLE__)
 #include <mach/mach.h>
 #elif defined(HAVE_SYS_THR_H)
@@ -2271,6 +2273,171 @@ int ast_get_tid(void)
 	return ret;
 }
 
+/*!
+ * \brief Max length of a salt string.
+ *
+ * $[1,5,6]$[a–zA–Z0–9./]{1,16}$, plus null terminator
+ */
+#define MAX_SALT_LEN 21
+
+static char salt_chars[] =
+	"abcdefghijklmnopqrstuvwxyz"
+	"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	"0123456789"
+	"./";
+
+/*! Randomly select a character for a salt string */
+static char gen_salt_char(void)
+{
+	int which = ast_random_double() * 64;
+	return salt_chars[which];
+}
+
+/*!
+ * \brief Generates a salt to try with crypt.
+ *
+ * If given an empty string, will generate a salt for the most secure algorithm
+ * to try with crypt(). If given a previously generated salt, the algorithm will
+ * be lowered by one level of security.
+ *
+ * \param[out] current_salt Output string in which to generate the salt.
+ *                          This can be an empty string, or the results of a
+ *                          prior gen_salt call.
+ * \param max_len Length of \a current_salt.
+ * \return 0 on success.
+ * \return Non-zero on error.
+ */
+static int gen_salt(char *current_salt, size_t maxlen)
+{
+	int i;
+
+	if (maxlen < MAX_SALT_LEN || current_salt == NULL) {
+		return -1;
+	}
+
+	switch (current_salt[0]) {
+	case '\0':
+		/* Initial generation; $6$ = SHA-512 */
+		*current_salt++ = '$';
+		*current_salt++ = '6';
+		*current_salt++ = '$';
+		for (i = 0; i < 16; ++i) {
+			*current_salt++ = gen_salt_char();
+		}
+		*current_salt++ = '$';
+		*current_salt++ = '\0';
+		return 0;
+	case '$':
+		switch (current_salt[1]) {
+		case '6':
+			/* Downgrade to SHA-256 */
+			current_salt[1] = '5';
+			return 0;
+		case '5':
+			/* Downgrade to MD5 */
+			current_salt[1] = '1';
+			return 0;
+		case '1':
+			/* Downgrade to traditional crypt */
+			*current_salt++ = gen_salt_char();
+			*current_salt++ = gen_salt_char();
+			*current_salt++ = '\0';
+			return 0;
+		default:
+			/* Unrecognized algorithm */
+			return -1;
+		}
+	default:
+		/* Was already as insecure as it gets */
+		return -1;
+	}
+
+}
+
+#if defined(HAVE_CRYPT_R)
+
+char *ast_crypt(const char *key, const char *salt)
+{
+	struct crypt_data data = {};
+	const char *crypted = crypt_r(key, salt, &data);
+
+	/* Crypt may return success even if it doesn't recognize the salt. But
+	 * in those cases it always mangles the salt in some way.
+	 */
+	if (!crypted || !ast_begins_with(crypted, salt)) {
+		return NULL;
+	}
+
+	return ast_strdup(crypted);
+}
+
+int ast_crypt_validate(const char *key, const char *expected)
+{
+	struct crypt_data data = {};
+	return strcmp(expected, crypt_r(key, expected, &data)) == 0;
+}
+
+#elif defined(HAVE_CRYPT)
+
+/* crypt is not reentrant. A global mutex is neither ideal nor perfect, but good
+ * enough if crypt_r support is unavailable
+ */
+AST_MUTEX_DEFINE_STATIC(crypt_mutex);
+
+char *ast_crypt(const char *key, const char *salt)
+{
+	const char *crypted;
+	SCOPED_MUTEX(lock, &crypt_mutex);
+
+	crypted = crypt(key, salt);
+
+	/* Crypt may return success even if it doesn't recognize the salt. But
+	 * in those cases it always mangles the salt in some way.
+	 */
+	if (!crypted || !ast_begins_with(crypted, salt)) {
+		return NULL;
+	}
+
+	return ast_strdup(crypted);
+}
+
+int ast_crypt_validate(const char *key, const char *expected)
+{
+	SCOPED_MUTEX(lock, &crypt_mutex);
+	return strcmp(expected, crypt(key, expected)) == 0;
+}
+
+#else /* No crypt support */
+
+char *ast_crypt(const char *key, const char *salt)
+{
+	ast_log(LOG_WARNING,
+		"crypt() support not available; cannot encrypt password\n");
+	return NULL;
+}
+
+int ast_crypt_validate(const char *key, const char *expected)
+{
+	ast_log(LOG_WARNING,
+		"crypt() support not available; cannot validate password\n");
+	return 0;
+}
+
+#endif  /* No crypt support */
+
+char *ast_crypt_encrypt(const char *key)
+{
+	char salt[MAX_SALT_LEN] = {};
+	while (gen_salt(salt, sizeof(salt)) == 0) {
+		char *crypted = ast_crypt(key, salt);
+		if (crypted) {
+			return crypted;
+		}
+	}
+	return NULL;
+}
+
+
 char *ast_utils_which(const char *binary, char *fullpath, size_t fullpath_size)
 {
 	const char *envPATH = getenv("PATH");
diff --git a/makeopts.in b/makeopts.in
index 95e69b817f97ad03ba6f311be3783b374d768bc6..401e8f1af0bb4cfaee7430b759727a7b858ab46c 100644
--- a/makeopts.in
+++ b/makeopts.in
@@ -281,6 +281,9 @@ SRTP_INCLUDE=@SRTP_INCLUDE@
 OPENSSL_INCLUDE=@OPENSSL_INCLUDE@
 OPENSSL_LIB=@OPENSSL_LIB@
 
+CRYPT_INCLUDE=@CRYPT_INCLUDE@
+CRYPT_LIB=@CRYPT_LIB@
+
 CRYPTO_INCLUDE=@CRYPTO_INCLUDE@
 CRYPTO_LIB=@CRYPTO_LIB@
 
diff --git a/res/Makefile b/res/Makefile
index 1310dae3a08666d191f13296564a3fd7f6226126..588bc2e726f5968e91ae532c1578e76fcd3ca9e5 100644
--- a/res/Makefile
+++ b/res/Makefile
@@ -80,8 +80,8 @@ clean::
 $(if $(filter res_parking,$(EMBEDDED_MODS)),modules.link,res_parking.so): $(subst .c,.o,$(wildcard parking/*.c))
 $(subst .c,.o,$(wildcard parking/*.c)): _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_parking)
 
-res_stasis_http.so: stasis_http/ari_websockets.o
-stasis_http/ari_websockets.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_stasis_http_asterisk)
+res_stasis_http.so: stasis_http/cli.o stasis_http/config.o stasis_http/ari_websockets.o
+stasis_http/cli.o stasis_http/config.o stasis_http/ari_websockets.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_stasis_http)
 
 res_ari_model.so: stasis_http/ari_model_validators.o
 stasis_http/ari_model_validators.o: _ASTCFLAGS+=$(call MOD_ASTCFLAGS,res_ari_model)
diff --git a/res/res_stasis_http.c b/res/res_stasis_http.c
index 3ff6482b5bcd1daaa5e20c0670d7b09da35682f3..4b2e1ccd75c25b32abe2264c9be8abaeed45aab3 100644
--- a/res/res_stasis_http.c
+++ b/res/res_stasis_http.c
@@ -79,15 +79,31 @@
 /*** DOCUMENTATION
 	<configInfo name="res_stasis_http" language="en_US">
 		<synopsis>HTTP binding for the Stasis API</synopsis>
-		<configFile name="stasis_http.conf">
-			<configObject name="global">
-				<synopsis>Global configuration settings</synopsis>
+		<configFile name="ari.conf">
+			<configObject name="general">
+				<synopsis>General configuration settings</synopsis>
 				<configOption name="enabled">
 					<synopsis>Enable/disable the stasis-http module</synopsis>
 				</configOption>
 				<configOption name="pretty">
 					<synopsis>Responses from stasis-http are formatted to be human readable</synopsis>
 				</configOption>
+				<configOption name="auth_realm">
+					<synopsis>Realm to use for authentication. Defaults to Asterisk REST Interface.</synopsis>
+				</configOption>
+			</configObject>
+
+			<configObject name="user">
+				<synopsis>Per-user configuration settings</synopsis>
+				<configOption name="read_only">
+					<synopsis>When set to yes, user is only authorized for read-only requests</synopsis>
+				</configOption>
+				<configOption name="password">
+					<synopsis>Crypted or plaintext password (see password_format)</synopsis>
+				</configOption>
+				<configOption name="password_format">
+					<synopsis>password_format may be set to plain (the default) or crypt. When set to crypt, crypt(3) is used to validate the password. A crypted password can be generated using mkpasswd -m sha-512. When set to plain, the password is in plaintext</synopsis>
+				</configOption>
 			</configObject>
 		</configFile>
 	</configInfo>
@@ -97,112 +113,21 @@
 
 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 
+#include "asterisk/astobj2.h"
 #include "asterisk/module.h"
 #include "asterisk/paths.h"
 #include "asterisk/stasis_http.h"
-#include "asterisk/config_options.h"
+#include "stasis_http/internal.h"
 
 #include <string.h>
 #include <sys/stat.h>
 #include <unistd.h>
 
-/*! \brief Global configuration options for stasis http. */
-struct conf_global_options {
-	/*! Enabled by default, disabled if false. */
-	int enabled:1;
-	/*! Encoding format used during output (default compact). */
-	enum ast_json_encoding_format format;
-};
-
-/*! \brief All configuration options for stasis http. */
-struct conf {
-	/*! The general section configuration options. */
-	struct conf_global_options *global;
-};
-
-/*! \brief Locking container for safe configuration access. */
-static AO2_GLOBAL_OBJ_STATIC(confs);
-
-/*! \brief Mapping of the stasis http conf struct's globals to the
- *         general context in the config file. */
-static struct aco_type global_option = {
-	.type = ACO_GLOBAL,
-	.name = "global",
-	.item_offset = offsetof(struct conf, global),
-	.category = "^general$",
-	.category_match = ACO_WHITELIST
-};
-
-static struct aco_type *global_options[] = ACO_TYPES(&global_option);
-
-/*! \brief Disposes of the stasis http conf object */
-static void conf_destructor(void *obj)
-{
-    struct conf *cfg = obj;
-    ao2_cleanup(cfg->global);
-}
-
-/*! \brief Creates the statis http conf object. */
-static void *conf_alloc(void)
-{
-    struct conf *cfg;
-
-    if (!(cfg = ao2_alloc(sizeof(*cfg), conf_destructor))) {
-        return NULL;
-    }
-
-    if (!(cfg->global = ao2_alloc(sizeof(*cfg->global), NULL))) {
-        ao2_ref(cfg, -1);
-        return NULL;
-    }
-    return cfg;
-}
-
-/*! \brief The conf file that's processed for the module. */
-static struct aco_file conf_file = {
-	/*! The config file name. */
-	.filename = "stasis_http.conf",
-	/*! The mapping object types to be processed. */
-	.types = ACO_TYPES(&global_option),
-};
-
-CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc,
-		     .files = ACO_FILES(&conf_file));
-
-/*! \brief Bitfield handler since it is not possible to take address. */
-static int conf_bitfield_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
-{
-	struct conf_global_options *global = obj;
-
-	if (!strcasecmp(var->name, "enabled")) {
-		global->enabled = ast_true(var->value);
-	} else {
-		return -1;
-	}
-
-	return 0;
-}
-
-/*! \brief Encoding format handler converts from boolean to enum. */
-static int encoding_format_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
-{
-	struct conf_global_options *global = obj;
-
-	if (!strcasecmp(var->name, "pretty")) {
-		global->format = ast_true(var->value) ? AST_JSON_PRETTY : AST_JSON_COMPACT;
-	} else {
-		return -1;
-	}
-
-	return 0;
-}
-
 /*! \brief Helper function to check if module is enabled. */
-static char is_enabled(void)
+static int is_enabled(void)
 {
-	RAII_VAR(struct conf *, cfg, ao2_global_obj_ref(confs), ao2_cleanup);
-
-	return cfg->global->enabled;
+	RAII_VAR(struct ari_conf *, cfg, ari_config_get(), ao2_cleanup);
+	return cfg && cfg->general && cfg->general->enabled;
 }
 
 /*! Lock for \ref root_handler */
@@ -797,8 +722,67 @@ static void process_cors_request(struct ast_variable *headers,
 
 enum ast_json_encoding_format stasis_http_json_format(void)
 {
-	RAII_VAR(struct conf *, cfg, ao2_global_obj_ref(confs), ao2_cleanup);
-	return cfg->global->format;
+	RAII_VAR(struct ari_conf *, cfg, NULL, ao2_cleanup);
+	cfg = ari_config_get();
+	return cfg->general->format;
+}
+
+/*!
+ * \brief Authenticate a <code>?api_key=userid:password</code>
+ *
+ * \param api_key API key query parameter
+ * \return User object for the authenticated user.
+ * \return \c NULL if authentication failed.
+ */
+static struct ari_conf_user *authenticate_api_key(const char *api_key)
+{
+	RAII_VAR(char *, copy, NULL, ast_free);
+	char *username;
+	char *password;
+
+	password = copy = ast_strdup(api_key);
+	if (!copy) {
+		return NULL;
+	}
+
+	username = strsep(&password, ":");
+	if (!password) {
+		ast_log(LOG_WARNING, "Invalid api_key\n");
+		return NULL;
+	}
+
+	return ari_config_validate_user(username, password);
+}
+
+/*!
+ * \brief Authenticate an HTTP request.
+ *
+ * \param get_params GET parameters of the request.
+ * \param header HTTP headers.
+ * \return User object for the authenticated user.
+ * \return \c NULL if authentication failed.
+ */
+static struct ari_conf_user *authenticate_user(struct ast_variable *get_params,
+	struct ast_variable *headers)
+{
+	RAII_VAR(struct ast_http_auth *, http_auth, NULL, ao2_cleanup);
+	struct ast_variable *v;
+
+	/* HTTP Basic authentication */
+	http_auth = ast_http_get_auth(headers);
+	if (http_auth) {
+		return ari_config_validate_user(http_auth->userid,
+			http_auth->password);
+	}
+
+	/* ?api_key authentication */
+	for (v = get_params; v; v = v->next) {
+		if (strcasecmp("api_key", v->name) == 0) {
+			return authenticate_api_key(v->value);
+		}
+	}
+
+	return NULL;
 }
 
 /*!
@@ -822,8 +806,10 @@ static int stasis_http_callback(struct ast_tcptls_session_instance *ser,
 				struct ast_variable *get_params,
 				struct ast_variable *headers)
 {
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
 	RAII_VAR(struct ast_str *, response_headers, ast_str_create(40), ast_free);
 	RAII_VAR(struct ast_str *, response_body, ast_str_create(256), ast_free);
+	RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
 	struct stasis_http_response response = {};
 	int ret = 0;
 
@@ -832,10 +818,45 @@ static int stasis_http_callback(struct ast_tcptls_session_instance *ser,
 	}
 
 	response.headers = ast_str_create(40);
+	if (!response.headers) {
+		return -1;
+	}
+
+	conf = ari_config_get();
+	if (!conf || !conf->general) {
+		return -1;
+	}
 
 	process_cors_request(headers, &response);
 
-	if (ast_ends_with(uri, "/")) {
+	user = authenticate_user(get_params, headers);
+	if (!user) {
+		/* Per RFC 2617, section 1.2: The 401 (Unauthorized) response
+		 * message is used by an origin server to challenge the
+		 * authorization of a user agent. This response MUST include a
+		 * WWW-Authenticate header field containing at least one
+		 * challenge applicable to the requested resource.
+		 */
+		response.response_code = 401;
+		response.response_text = "Unauthorized";
+
+		/* Section 1.2:
+		 *   realm       = "realm" "=" realm-value
+		 *   realm-value = quoted-string
+		 * Section 2:
+		 *   challenge   = "Basic" realm
+		 */
+		ast_str_append(&response.headers, 0,
+			"WWW-Authenticate: Basic realm=\"%s\"\r\n",
+			conf->general->auth_realm);
+		response.message = ast_json_pack("{s: s}",
+			"error", "Authentication required");
+	} else if (user->read_only && method != AST_HTTP_GET && method != AST_HTTP_OPTIONS) {
+		response.message = ast_json_pack("{s: s}",
+			"error", "Write access denied");
+		response.response_code = 403;
+		response.response_text = "Forbidden";
+	} else if (ast_ends_with(uri, "/")) {
 		remove_trailing_slash(uri, &response);
 	} else if (ast_begins_with(uri, "api-docs/")) {
 		/* Serving up API docs */
@@ -875,7 +896,8 @@ static int stasis_http_callback(struct ast_tcptls_session_instance *ser,
 	if (response.message && !ast_json_is_null(response.message)) {
 		ast_str_append(&response_headers, 0,
 			       "Content-type: application/json\r\n");
-		if (ast_json_dump_str_format(response.message, &response_body, stasis_http_json_format()) != 0) {
+		if (ast_json_dump_str_format(response.message, &response_body,
+				conf->general->format) != 0) {
 			/* Error encoding response */
 			response.response_code = 500;
 			response.response_text = "Internal Server Error";
@@ -909,38 +931,39 @@ static struct ast_http_uri http_uri = {
 
 static int load_module(void)
 {
-	oom_json = ast_json_pack(
-		"{s: s}", "error", "AllocationFailed");
-
-	if (!oom_json) {
-		/* Ironic */
-		return AST_MODULE_LOAD_FAILURE;
-	}
-
 	ast_mutex_init(&root_handler_lock);
 
-	root_handler = root_handler_create();
+	/* root_handler may have been built during a declined load */
+	if (!root_handler) {
+		root_handler = root_handler_create();
+	}
 	if (!root_handler) {
 		return AST_MODULE_LOAD_FAILURE;
 	}
 
-	if (aco_info_init(&cfg_info)) {
-		aco_info_destroy(&cfg_info);
-		return AST_MODULE_LOAD_DECLINE;
+	/* oom_json may have been built during a declined load */
+	if (!oom_json) {
+		oom_json = ast_json_pack(
+			"{s: s}", "error", "Allocation failed");
+	}
+	if (!oom_json) {
+		/* Ironic */
+		return AST_MODULE_LOAD_FAILURE;
 	}
 
-	aco_option_register_custom(&cfg_info, "enabled", ACO_EXACT, global_options,
-				   "yes", conf_bitfield_handler, 0);
-	aco_option_register_custom(&cfg_info, "pretty", ACO_EXACT, global_options,
-				   "no",  encoding_format_handler, 0);
-
-	if (aco_process_config(&cfg_info, 0)) {
-		aco_info_destroy(&cfg_info);
+	if (ari_config_init() != 0) {
 		return AST_MODULE_LOAD_DECLINE;
 	}
 
 	if (is_enabled()) {
+		ast_debug(3, "ARI enabled\n");
 		ast_http_uri_link(&http_uri);
+	} else {
+		ast_debug(3, "ARI disabled\n");
+	}
+
+	if (ari_cli_register() != 0) {
+		return AST_MODULE_LOAD_FAILURE;
 	}
 
 	return AST_MODULE_LOAD_SUCCESS;
@@ -948,20 +971,22 @@ static int load_module(void)
 
 static int unload_module(void)
 {
-	ast_json_unref(oom_json);
-	oom_json = NULL;
+	ari_cli_unregister();
 
 	if (is_enabled()) {
+		ast_debug(3, "Disabling ARI\n");
 		ast_http_uri_unlink(&http_uri);
 	}
 
-	aco_info_destroy(&cfg_info);
-	ao2_global_obj_release(confs);
+	ari_config_destroy();
 
 	ao2_cleanup(root_handler);
 	root_handler = NULL;
 	ast_mutex_destroy(&root_handler_lock);
 
+	ast_json_unref(oom_json);
+	oom_json = NULL;
+
 	return 0;
 }
 
@@ -969,13 +994,15 @@ static int reload_module(void)
 {
 	char was_enabled = is_enabled();
 
-	if (aco_process_config(&cfg_info, 1)) {
+	if (ari_config_reload() != 0) {
 		return AST_MODULE_LOAD_DECLINE;
 	}
 
 	if (was_enabled && !is_enabled()) {
+		ast_debug(3, "Disabling ARI\n");
 		ast_http_uri_unlink(&http_uri);
 	} else if (!was_enabled && is_enabled()) {
+		ast_debug(3, "Enabling ARI\n");
 		ast_http_uri_link(&http_uri);
 	}
 
diff --git a/res/stasis_http/cli.c b/res/stasis_http/cli.c
new file mode 100644
index 0000000000000000000000000000000000000000..98d082b2c62394f55f7dbf3e16dcdb8477a88659
--- /dev/null
+++ b/res/stasis_http/cli.c
@@ -0,0 +1,266 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * David M. Lee, II <dlee@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Command line for ARI.
+ * \author David M. Lee, II <dlee@digium.com>
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/astobj2.h"
+#include "asterisk/cli.h"
+#include "internal.h"
+
+static char *ari_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "ari show status";
+		e->usage =
+			"Usage: ari show status\n"
+			"       Shows all ARI settings\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	default:
+		break;
+	}
+
+	if (a->argc != 3) {
+		return CLI_SHOWUSAGE;
+	}
+
+	conf = ari_config_get();
+
+	if (!conf) {
+		ast_cli(a->fd, "Error getting ARI configuration\n");
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd, "ARI Status:\n");
+	ast_cli(a->fd, "Enabled: %s\n", AST_CLI_YESNO(conf->general->enabled));
+	ast_cli(a->fd, "Output format: ");
+	switch (conf->general->format) {
+	case AST_JSON_COMPACT:
+		ast_cli(a->fd, "compact");
+		break;
+	case AST_JSON_PRETTY:
+		ast_cli(a->fd, "pretty");
+		break;
+	}
+	ast_cli(a->fd, "\n");
+	ast_cli(a->fd, "Auth realm: %s\n", conf->general->auth_realm);
+	ast_cli(a->fd, "User count: %d\n", ao2_container_count(conf->users));
+	return CLI_SUCCESS;
+}
+
+static int show_users_cb(void *obj, void *arg, int flags)
+{
+	struct ari_conf_user *user = obj;
+	struct ast_cli_args *a = arg;
+
+	ast_cli(a->fd, "%-4s  %s\n",
+		AST_CLI_YESNO(user->read_only),
+		user->username);
+	return 0;
+}
+
+static char *ari_show_users(struct ast_cli_entry *e, int cmd,
+	struct ast_cli_args *a)
+{
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "ari show users";
+		e->usage =
+			"Usage: ari show users\n"
+			"       Shows all ARI users\n";
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	default:
+		break;
+	}
+
+	if (a->argc != 3) {
+		return CLI_SHOWUSAGE;
+	}
+
+	conf = ari_config_get();
+	if (!conf) {
+		ast_cli(a->fd, "Error getting ARI configuration\n");
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd, "r/o?  Username\n");
+	ast_cli(a->fd, "----  --------\n");
+
+	ao2_callback(conf->users, OBJ_NODATA, show_users_cb, a);
+
+	return CLI_SUCCESS;
+}
+
+struct user_complete {
+	/*! Nth user to search for */
+	int state;
+	/*! Which user currently on */
+	int which;
+};
+
+static int complete_ari_user_search(void *obj, void *arg, void *data, int flags)
+{
+	struct user_complete *search = data;
+
+	if (++search->which > search->state) {
+		return CMP_MATCH;
+	}
+	return 0;
+}
+
+static char *complete_ari_user(struct ast_cli_args *a)
+{
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
+	RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
+
+	struct user_complete search = {
+		.state = a->n,
+	};
+
+	conf = ari_config_get();
+	if (!conf) {
+		ast_cli(a->fd, "Error getting ARI configuration\n");
+		return CLI_FAILURE;
+	}
+
+	user = ao2_callback_data(conf->users,
+		ast_strlen_zero(a->word) ? 0 : OBJ_PARTIAL_KEY,
+		complete_ari_user_search, (char*)a->word, &search);
+
+	return user ? ast_strdup(user->username) : NULL;
+}
+
+static char *complete_ari_show_user(struct ast_cli_args *a)
+{
+	if (a->pos == 3) {
+		return complete_ari_user(a);
+	}
+
+	return NULL;
+}
+
+static char *ari_show_user(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
+	RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "ari show user";
+		e->usage =
+			"Usage: ari show user <username>\n"
+			"       Shows a specific ARI user\n";
+		return NULL;
+	case CLI_GENERATE:
+		return complete_ari_show_user(a);
+	default:
+		break;
+	}
+
+	if (a->argc != 4) {
+		return CLI_SHOWUSAGE;
+	}
+
+	conf = ari_config_get();
+
+	if (!conf) {
+		ast_cli(a->fd, "Error getting ARI configuration\n");
+		return CLI_FAILURE;
+	}
+
+	user = ao2_find(conf->users, a->argv[3], OBJ_KEY);
+	if (!user) {
+		ast_cli(a->fd, "User '%s' not found\n", a->argv[3]);
+		return CLI_SUCCESS;
+	}
+
+	ast_cli(a->fd, "Username: %s\n", user->username);
+	ast_cli(a->fd, "Read only?: %s\n", AST_CLI_YESNO(user->read_only));
+
+	return CLI_SUCCESS;
+}
+
+static char *ari_mkpasswd(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+	RAII_VAR(char *, crypted, NULL, ast_free);
+
+	switch (cmd) {
+	case CLI_INIT:
+		e->command = "ari mkpasswd";
+		e->usage =
+			"Usage: ari mkpasswd <password>\n"
+			"       Encrypts a password for use in ari.conf\n"
+			"       Be aware that the password will be shown in the\n"
+			"       command line history. The mkpasswd shell command\n"
+			"       may be preferable.\n"
+			;
+		return NULL;
+	case CLI_GENERATE:
+		return NULL;
+	default:
+		break;
+	}
+
+	if (a->argc != 3) {
+		return CLI_SHOWUSAGE;
+	}
+
+	crypted = ast_crypt_encrypt(a->argv[2]);
+	if (!crypted) {
+		ast_cli(a->fd, "Failed to encrypt password\n");
+		return CLI_FAILURE;
+	}
+
+	ast_cli(a->fd,
+		"; Copy the following two lines into ari.conf\n");
+	ast_cli(a->fd, "password_format = crypt\n");
+	ast_cli(a->fd, "password = %s\n", crypted);
+
+	return CLI_SUCCESS;
+}
+
+static struct ast_cli_entry cli_ari[] = {
+	AST_CLI_DEFINE(ari_show, "Show ARI settings"),
+	AST_CLI_DEFINE(ari_show_users, "List ARI users"),
+	AST_CLI_DEFINE(ari_show_user, "List single ARI user"),
+	AST_CLI_DEFINE(ari_mkpasswd, "Encrypts a password"),
+};
+
+int ari_cli_register(void) {
+	return ast_cli_register_multiple(cli_ari, ARRAY_LEN(cli_ari));
+}
+
+void ari_cli_unregister(void) {
+	ast_cli_unregister_multiple(cli_ari, ARRAY_LEN(cli_ari));
+}
diff --git a/res/stasis_http/config.c b/res/stasis_http/config.c
new file mode 100644
index 0000000000000000000000000000000000000000..f02fabea450c1233d4e84a7589676becea533db9
--- /dev/null
+++ b/res/stasis_http/config.c
@@ -0,0 +1,341 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * David M. Lee, II <dlee@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! \file
+ *
+ * \brief Config framework stuffz for ARI.
+ * \author David M. Lee, II <dlee@digium.com>
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/config_options.h"
+#include "internal.h"
+
+/*! \brief Locking container for safe configuration access. */
+static AO2_GLOBAL_OBJ_STATIC(confs);
+
+/*! \brief Mapping of the stasis http conf struct's globals to the
+ *         general context in the config file. */
+static struct aco_type general_option = {
+	.type = ACO_GLOBAL,
+	.name = "general",
+	.item_offset = offsetof(struct ari_conf, general),
+	.category = "^general$",
+	.category_match = ACO_WHITELIST,
+};
+
+static struct aco_type *general_options[] = ACO_TYPES(&general_option);
+
+/*! \brief Encoding format handler converts from boolean to enum. */
+static int encoding_format_handler(const struct aco_option *opt,
+	struct ast_variable *var, void *obj)
+{
+	struct ari_conf_general *general = obj;
+
+	if (!strcasecmp(var->name, "pretty")) {
+		general->format = ast_true(var->value) ?
+			AST_JSON_PRETTY : AST_JSON_COMPACT;
+	} else {
+		return -1;
+	}
+
+	return 0;
+}
+
+/*! \brief Parses the ari_password_format enum from a config file */
+static int password_format_handler(const struct aco_option *opt,
+	struct ast_variable *var, void *obj)
+{
+	struct ari_conf_user *user = obj;
+
+	if (strcasecmp(var->value, "plain") == 0) {
+		user->password_format = ARI_PASSWORD_FORMAT_PLAIN;
+	} else if (strcasecmp(var->value, "crypt") == 0) {
+		user->password_format = ARI_PASSWORD_FORMAT_CRYPT;
+	} else {
+		return -1;
+	}
+
+	return 0;
+}
+
+/*! \brief Destructor for \ref ari_conf_user */
+static void user_dtor(void *obj)
+{
+	struct ari_conf_user *user = obj;
+	ast_debug(3, "Disposing of user %s\n", user->username);
+	ast_free(user->username);
+}
+
+/*! \brief Allocate an \ref ari_conf_user for config parsing */
+static void *user_alloc(const char *cat)
+{
+	RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
+	const char *username;
+
+	if (!cat) {
+		return NULL;
+	}
+
+	username = strchr(cat, '-') + 1;
+
+	if (!username) {
+		ast_log(LOG_ERROR, "Invalid user category '%s'\n", cat);
+		return NULL;
+	}
+
+	ast_debug(3, "Allocating user %s\n", cat);
+
+	user = ao2_alloc_options(sizeof(*user), user_dtor,
+		AO2_ALLOC_OPT_LOCK_NOLOCK);
+	if (!user) {
+		return NULL;
+	}
+
+	user->username = ast_strdup(username);
+	if (!user->username) {
+		return NULL;
+	}
+
+	ao2_ref(user, +1);
+	return user;
+}
+
+/*! \brief Sorting function for use with red/black tree */
+static int user_sort_cmp(const void *obj_left, const void *obj_right, int flags)
+{
+	const struct ari_conf_user *user_left = obj_left;
+
+	if (flags & OBJ_PARTIAL_KEY) {
+		const char *key_right = obj_right;
+		return strncasecmp(user_left->username, key_right,
+			strlen(key_right));
+	} else if (flags & OBJ_KEY) {
+		const char *key_right = obj_right;
+		return strcasecmp(user_left->username, key_right);
+	} else {
+		const struct ari_conf_user *user_right = obj_right;
+		const char *key_right = user_right->username;
+		return strcasecmp(user_left->username, key_right);
+	}
+}
+
+/*! \brief \ref aco_type item_find function */
+static void *user_find(struct ao2_container *tmp_container, const char *cat)
+{
+	const char *username;
+
+	if (!cat) {
+		return NULL;
+	}
+
+	username = strchr(cat, '-') + 1;
+	return ao2_find(tmp_container, username, OBJ_KEY);
+}
+
+static struct aco_type user_option = {
+	.type = ACO_ITEM,
+	.name = "user",
+	.category_match = ACO_WHITELIST,
+	.category = "^user-.+$",
+	.item_alloc = user_alloc,
+	.item_find = user_find,
+	.item_offset = offsetof(struct ari_conf, users),
+};
+
+static struct aco_type *user[] = ACO_TYPES(&user_option);
+
+/*! \brief \ref ari_conf destructor. */
+static void conf_destructor(void *obj)
+{
+	struct ari_conf *cfg = obj;
+	ao2_cleanup(cfg->general);
+	ao2_cleanup(cfg->users);
+}
+
+/*! \brief Allocate an \ref ari_conf for config parsing */
+static void *conf_alloc(void)
+{
+	RAII_VAR(struct ari_conf *, cfg, NULL, ao2_cleanup);
+
+	cfg = ao2_alloc_options(sizeof(*cfg), conf_destructor,
+		AO2_ALLOC_OPT_LOCK_NOLOCK);
+	if (!cfg) {
+		return NULL;
+	}
+
+	cfg->general = ao2_alloc_options(sizeof(*cfg->general), NULL,
+		AO2_ALLOC_OPT_LOCK_NOLOCK);
+	if (!cfg->general) {
+		return NULL;
+	}
+
+	cfg->users = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
+		AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, user_sort_cmp, NULL);
+
+	ao2_ref(cfg, +1);
+	return cfg;
+}
+
+#define CONF_FILENAME "ari.conf"
+
+/*! \brief The conf file that's processed for the module. */
+static struct aco_file conf_file = {
+	/*! The config file name. */
+	.filename = CONF_FILENAME,
+	/*! The mapping object types to be processed. */
+	.types = ACO_TYPES(&general_option, &user_option),
+};
+
+CONFIG_INFO_STANDARD(cfg_info, confs, conf_alloc,
+		     .files = ACO_FILES(&conf_file));
+
+struct ari_conf *ari_config_get(void)
+{
+	struct ari_conf *res = ao2_global_obj_ref(confs);
+	if (!res) {
+		ast_log(LOG_ERROR,
+			"Error obtaining config from " CONF_FILENAME "\n");
+	}
+	return res;
+}
+
+struct ari_conf_user *ari_config_validate_user(const char *username,
+	const char *password)
+{
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
+	RAII_VAR(struct ari_conf_user *, user, NULL, ao2_cleanup);
+	int is_valid = 0;
+
+	conf = ari_config_get();
+	if (!conf) {
+		return NULL;
+	}
+
+	user = ao2_find(conf->users, username, OBJ_KEY);
+	if (!user) {
+		return NULL;
+	}
+
+	if (ast_strlen_zero(user->password)) {
+		ast_log(LOG_WARNING,
+			"User '%s' missing password; authentication failed\n",
+			user->username);
+		return NULL;
+	}
+
+	switch (user->password_format) {
+	case ARI_PASSWORD_FORMAT_PLAIN:
+		is_valid = strcmp(password, user->password) == 0;
+		break;
+	case ARI_PASSWORD_FORMAT_CRYPT:
+		is_valid = ast_crypt_validate(password, user->password);
+		break;
+	}
+
+	if (!is_valid) {
+		return NULL;
+	}
+
+	ao2_ref(user, +1);
+	return user;
+}
+
+/*! \brief Callback to validate a user object */
+static int validate_user_cb(void *obj, void *arg, int flags)
+{
+	struct ari_conf_user *user = obj;
+
+	if (ast_strlen_zero(user->password)) {
+		ast_log(LOG_WARNING, "User '%s' missing password\n",
+			user->username);
+	}
+
+	return 0;
+}
+
+/*! \brief Load (or reload) configuration. */
+static int process_config(int reload)
+{
+	RAII_VAR(struct ari_conf *, conf, NULL, ao2_cleanup);
+
+	switch (aco_process_config(&cfg_info, reload)) {
+	case ACO_PROCESS_ERROR:
+		return -1;
+	case ACO_PROCESS_OK:
+	case ACO_PROCESS_UNCHANGED:
+		break;
+	}
+
+	conf = ari_config_get();
+	if (!conf) {
+		ast_assert(0); /* We just configured; it should be there */
+		return -1;
+	}
+
+	if (ao2_container_count(conf->users) == 0) {
+		ast_log(LOG_ERROR, "No configured users for ARI\n");
+	}
+
+	ao2_callback(conf->users, OBJ_NODATA, validate_user_cb, NULL);
+
+	return 0;
+}
+
+int ari_config_init(void)
+{
+	if (aco_info_init(&cfg_info)) {
+		aco_info_destroy(&cfg_info);
+		return -1;
+	}
+
+	aco_option_register(&cfg_info, "enabled", ACO_EXACT, general_options,
+		"yes", OPT_BOOL_T, 1,
+		FLDSET(struct ari_conf_general, enabled));
+	aco_option_register_custom(&cfg_info, "pretty", ACO_EXACT,
+		general_options, "no",  encoding_format_handler, 0);
+	aco_option_register(&cfg_info, "auth_realm", ACO_EXACT, general_options,
+		"Asterisk REST Interface", OPT_CHAR_ARRAY_T, 0,
+		FLDSET(struct ari_conf_general, auth_realm),
+		ARI_AUTH_REALM_LEN);
+
+	aco_option_register(&cfg_info, "read_only", ACO_EXACT, user,
+		"no", OPT_BOOL_T, 1,
+		FLDSET(struct ari_conf_user, read_only));
+	aco_option_register(&cfg_info, "password", ACO_EXACT, user,
+		"", OPT_CHAR_ARRAY_T, 0,
+		FLDSET(struct ari_conf_user, password), ARI_PASSWORD_LEN);
+	aco_option_register_custom(&cfg_info, "password_format", ACO_EXACT,
+		user, "plain",  password_format_handler, 0);
+
+	return process_config(0);
+}
+
+int ari_config_reload(void)
+{
+	return process_config(1);
+}
+
+void ari_config_destroy(void)
+{
+	aco_info_destroy(&cfg_info);
+	ao2_global_obj_release(confs);
+}
diff --git a/res/stasis_http/internal.h b/res/stasis_http/internal.h
new file mode 100644
index 0000000000000000000000000000000000000000..659f4a2aeed2744570c9724d1a1aa89294eef681
--- /dev/null
+++ b/res/stasis_http/internal.h
@@ -0,0 +1,139 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013, Digium, Inc.
+ *
+ * David M. Lee, II <dlee@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+#ifndef STASIS_HTTP_INTERNAL_H_
+#define STASIS_HTTP_INTERNAL_H_
+
+/*! \file
+ *
+ * \brief Internal API's for res_stasis_http.
+ * \author David M. Lee, II <dlee@digium.com>
+ */
+
+#include "asterisk/json.h"
+
+/*! @{ */
+
+/*!
+ * \brief Register CLI commands for ARI.
+ *
+ * \return 0 on success.
+ * \return Non-zero on error.
+ */
+int ari_cli_register(void);
+
+/*!
+ * \brief Unregister CLI commands for ARI.
+ */
+void ari_cli_unregister(void);
+
+/*! @} */
+
+/*! @{ */
+
+struct ari_conf_general;
+
+/*! \brief All configuration options for stasis http. */
+struct ari_conf {
+	/*! The general section configuration options. */
+	struct ari_conf_general *general;
+	/*! Configured users */
+	struct ao2_container *users;
+};
+
+/*! Max length for auth_realm field */
+#define ARI_AUTH_REALM_LEN 80
+
+/*! \brief Global configuration options for stasis http. */
+struct ari_conf_general {
+	/*! Enabled by default, disabled if false. */
+	int enabled;
+	/*! Encoding format used during output (default compact). */
+	enum ast_json_encoding_format format;
+	/*! Authentication realm */
+	char auth_realm[ARI_AUTH_REALM_LEN];
+};
+
+/*! \brief Password format */
+enum ari_password_format {
+	/*! \brief Plaintext password */
+	ARI_PASSWORD_FORMAT_PLAIN,
+	/*! crypt(3) password */
+	ARI_PASSWORD_FORMAT_CRYPT,
+};
+
+/*!
+ * \brief User's password mx length.
+ *
+ * If 256 seems like a lot, a crypt SHA-512 has over 106 characters.
+ */
+#define ARI_PASSWORD_LEN 256
+
+/*! \brief Per-user configuration options */
+struct ari_conf_user {
+	/*! Username for authentication */
+	char *username;
+	/*! User's password. */
+	char password[ARI_PASSWORD_LEN];
+	/*! Format for the password field */
+	enum ari_password_format password_format;
+	/*! If true, user cannot execute change operations */
+	int read_only;
+};
+
+/*!
+ * \brief Initialize the ARI configuration
+ */
+int ari_config_init(void);
+
+/*!
+ * \brief Reload the ARI configuration
+ */
+int ari_config_reload(void);
+
+/*!
+ * \brief Destroy the ARI configuration
+ */
+void ari_config_destroy(void);
+
+/*!
+ * \brief Get the current ARI configuration.
+ *
+ * This is an immutable object, so don't modify it. It is AO2 managed, so
+ * ao2_cleanup() when you're done with it.
+ *
+ * \return ARI configuration object.
+ * \return \c NULL on error.
+ */
+struct ari_conf *ari_config_get(void);
+
+/*!
+ * \brief Validated a user's credentials.
+ *
+ * \param username Name of the user.
+ * \param password User's password.
+ * \return User object.
+ * \return \c NULL if username or password is invalid.
+ */
+struct ari_conf_user *ari_config_validate_user(const char *username,
+	const char *password);
+
+/*! @} */
+
+
+#endif /* STASIS_HTTP_INTERNAL_H_ */