# HG changeset patch
# Node ID 3a7bc97aac9bcebf11d565d25e065085d7e11002
# Parent  22f97ede1942d954a02650ec399dd085e3e048f1
diff --git a/hasl/haslcontext.c b/hasl/haslcontext.c
--- a/hasl/haslcontext.c
+++ b/hasl/haslcontext.c
@@ -26,6 +26,7 @@
 enum {
 	PROP_0,
 	PROP_ALLOWED_MECHANISMS,
+	PROP_SERVER_MECHANISMS,
 	PROP_AUTHZID,
 	PROP_USERNAME,
 	PROP_PASSWORD,
@@ -39,6 +40,8 @@
 	GObject parent;
 
 	char *allowed_mechanisms;
+	char *server_mechanisms;
+
 	char *authzid;
 	char *username;
 	char *password;
@@ -69,6 +72,9 @@
 	case PROP_ALLOWED_MECHANISMS:
 		g_value_set_string(value, hasl_context_get_allowed_mechanisms(ctx));
 		break;
+	case PROP_SERVER_MECHANISMS:
+		g_value_set_string(value, hasl_context_get_server_mechanisms(ctx));
+		break;
 	case PROP_AUTHZID:
 		g_value_set_string(value, hasl_context_get_authzid(ctx));
 		break;
@@ -100,6 +106,9 @@
 	case PROP_ALLOWED_MECHANISMS:
 		hasl_context_set_allowed_mechanisms(ctx, g_value_get_string(value));
 		break;
+	case PROP_SERVER_MECHANISMS:
+		hasl_context_set_server_mechanisms(ctx, g_value_get_string(value));
+		break;
 	case PROP_AUTHZID:
 		hasl_context_set_authzid(ctx, g_value_get_string(value));
 		break;
@@ -145,6 +154,7 @@
 	HaslContext *ctx = HASL_CONTEXT(obj);
 
 	g_clear_pointer(&ctx->allowed_mechanisms, g_free);
+	g_clear_pointer(&ctx->server_mechanisms, g_free);
 	g_clear_pointer(&ctx->authzid, g_free);
 	g_clear_pointer(&ctx->username, g_free);
 	g_clear_pointer(&ctx->password, g_free);
@@ -169,7 +179,7 @@
 	 * HaslContext:allowed-mechanisms:
 	 *
 	 * A list of mechanisms that are allowed to be used by this context. This
-	 * list can be either white space or comma separated.
+	 * list can be separated by white space, commas, or a combination of both.
 	 *
 	 * Since: 0.1
 	 */
@@ -179,6 +189,19 @@
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * HaslContext:server-mechanisms:
+	 *
+	 * A list of mechanisms that the server advertised. This list can be
+	 * separated by white space, commas, or a combination of both.
+	 *
+	 * Since: 0.4
+	 */
+	properties[PROP_SERVER_MECHANISMS] = g_param_spec_string(
+		"server-mechanisms", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * HaslContext:authzid
 	 *
 	 * The identifier for the user who is authenticating.
@@ -262,8 +285,10 @@
 	g_return_if_fail(HASL_IS_CONTEXT(ctx));
 
 	g_clear_object(&ctx->current_mechanism);
+	ctx->current_mechanism_index = -1;
 
-	ctx->current_mechanism_index = -1;
+	g_clear_pointer(&ctx->available_mechanisms, g_strfreev);
+	ctx->available_mechanisms_count = 0;
 }
 
 HaslContext *
@@ -279,27 +304,11 @@
 }
 
 void
-hasl_context_set_allowed_mechanisms(HaslContext *ctx,
-                                    const char *allowed_mechanisms)
-{
+hasl_context_set_allowed_mechanisms(HaslContext *ctx, const char *mechanisms) {
 	g_return_if_fail(HASL_IS_CONTEXT(ctx));
-	g_return_if_fail(allowed_mechanisms != NULL);
-
-	if(g_strcmp0(allowed_mechanisms, ctx->allowed_mechanisms) != 0) {
-		g_free(ctx->allowed_mechanisms);
-		ctx->allowed_mechanisms = g_strdup(allowed_mechanisms);
 
-		ctx->available_mechanisms = g_strsplit_set(ctx->allowed_mechanisms,
-		                                           ", ", -1);
-		ctx->available_mechanisms_count = g_strv_length(ctx->available_mechanisms);
-
-		/* Since the allowed mechanisms has been changed, we need to reset
-		 * our index counter as well.
-		 */
-		ctx->current_mechanism_index = -1;
-
-		g_object_notify_by_pspec(G_OBJECT(ctx),
-		                         properties[PROP_ALLOWED_MECHANISMS]);
+	if(g_set_str(&ctx->allowed_mechanisms, mechanisms)) {
+		g_object_notify_by_pspec(G_OBJECT(ctx), properties[PROP_ALLOWED_MECHANISMS]);
 	}
 }
 
@@ -459,11 +468,60 @@
 }
 
 const char *
+hasl_context_get_server_mechanisms(HaslContext *ctx) {
+	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);
+
+	return ctx->server_mechanisms;
+}
+
+void
+hasl_context_set_server_mechanisms(HaslContext *ctx, const char *mechanisms) {
+	g_return_if_fail(HASL_IS_CONTEXT(ctx));
+
+	if(g_set_str(&ctx->server_mechanisms, mechanisms)) {
+		g_object_notify_by_pspec(G_OBJECT(ctx),
+		                         properties[PROP_SERVER_MECHANISMS]);
+	}
+}
+
+const char *
 hasl_context_next(HaslContext *ctx) {
 	g_return_val_if_fail(HASL_IS_CONTEXT(ctx), NULL);
 
+	/* If we don't have any available mechanisms check if the user set any,
+	 * otherwise fall back to the server advertised ones. If we don't have
+	 * either, we just return null.
+	 */
 	if(ctx->available_mechanisms == NULL) {
-		return NULL;
+		const char *mechanisms = ctx->allowed_mechanisms;
+
+		/* No client side mechanisms, try the server advertised ones. */
+		if(mechanisms == NULL || mechanisms[0] == '\0') {
+			mechanisms = ctx->server_mechanisms;
+		}
+
+		/* No server advertised mechanisms, we fall back to supported below. */
+		if(mechanisms == NULL || mechanisms[0] == '\0') {
+			mechanisms = NULL;
+		}
+
+		/* If we have mechanisms use them, otherwise fall back to the supported
+		 * mechanisms.
+		 */
+		if(mechanisms != NULL) {
+			/* We have mechanisms, so tokenize them and initialize everything. */
+			ctx->available_mechanisms = g_strsplit_set(mechanisms, ", ", -1);
+			ctx->available_mechanisms_count = g_strv_length(ctx->available_mechanisms);
+		} else {
+			char *supported = hasl_context_get_supported_mechanisms(ctx);
+
+			ctx->available_mechanisms = g_strsplit_set(supported, ", ", -1);
+			g_free(supported);
+
+			ctx->available_mechanisms_count = g_strv_length(ctx->available_mechanisms);
+		}
+
+		ctx->current_mechanism_index = -1;
 	}
 
 	/* Now loop through the allowed mechanism until we find one that is
diff --git a/hasl/haslcontext.h b/hasl/haslcontext.h
--- a/hasl/haslcontext.h
+++ b/hasl/haslcontext.h
@@ -47,13 +47,20 @@
  * // After you connect to the server, you can create your context and start
  * // the authentication process.
  * HaslContext *context = NULL;
- * char *mechanism = NULL;
+ * const char *mechanism = NULL;
  *
  * context = hasl_context_new();
  * hasl_context_set_username(context, "alice");
  * hasl_context_set_password(context, "hunter2");
+ *
+ * // If the user specified any mechanisms you can set them like so.
  * hasl_context_set_allowed_mechanisms(context, "PLAIN,EXTERNAL");
  *
+ * // If the server advertised any mechanisms you can set them like so.
+ * hasl_context_set_server_mechanisms(context, server_mechanisms);
+ *
+ * // Next will use allowed mechanisms if set, otherwise it will fallback to
+ * // server mechanisms. If neither are set, it will return NULL.
  * mechanism = hasl_context_next(context);
  *
  * // Attempt your authentication using the mechanism name stored in
@@ -154,17 +161,18 @@
 
 /**
  * hasl_context_set_allowed_mechanisms:
- * @ctx: The instance.
- * @allowed_mechanisms: The list of mechanisms.
+ * @ctx: the instance
+ * @mechanisms: (nullable): the new list of mechanisms
  *
  * Sets the list of mechanism that are allowed for @ctx.
  *
- * @allowed_mechanisms can be white space or comma separated.
+ * @mechanisms can be separated by white space, commas, or a combination of
+ * both.
  *
  * Since: 0.1
  */
 HASL_AVAILABLE_IN_0_1
-void hasl_context_set_allowed_mechanisms(HaslContext *ctx, const char *allowed_mechanisms);
+void hasl_context_set_allowed_mechanisms(HaslContext *ctx, const char *mechanisms);
 
 /**
  * hasl_context_get_authzid:
@@ -367,12 +375,51 @@
 const char *hasl_context_get_current_mechanism(HaslContext *ctx);
 
 /**
- * hasl_context_next:
- * @ctx: The instance.
+ * hasl_context_get_server_mechanisms:
+ * @ctx: the instance
+ *
+ * Gets the server mechanisms from a context.
+ *
+ * Returns: (transfer none) (nullable): The mechanisms that the server
+ *          advertised.
+ *
+ * Since: 0.4
+ */
+HASL_AVAILABLE_IN_0_4
+const char *hasl_context_get_server_mechanisms(HaslContext *ctx);
+
+/**
+ * hasl_context_set_server_mechanisms:
+ * @ctx: the instance
+ * @mechanisms: (nullable): the new mechanisms
+ *
+ * Sets the mechanisms that the server has advertised.
+ *
+ * These mechanisms will be used if [property@Context:allowed-mechanisms] is
+ * not set.
  *
- * Ask the context to move to the next possible mechanism. The context will
- * create an instance of the mechanism to use internally and return the name
- * that should be given to the server.
+ * @mechanisms can be separated by white space, commas, or a combination of
+ * both.
+ *
+ * Since: 0.4
+ */
+HASL_AVAILABLE_IN_0_4
+void hasl_context_set_server_mechanisms(HaslContext *ctx, const char *mechanisms);
+
+/**
+ * hasl_context_next:
+ * @ctx: the instance
+ *
+ * Ask the context to move to the next possible mechanism.
+ *
+ * If @ctx is in a new or reset state via [method@Context.done], it will build
+ * a list of mechanisms to attempt. It will prefer
+ * [property@Context:allowed-mechanisms] over
+ * [property@Context:server-mechanisms], falling back to
+ * [method@Context.get_supported_mechanisms] if necessary.
+ *
+ * The context will create an instance of the next mechanism to try which it
+ * will use internally and return the name that should be given to the server.
  *
  * Returns: (nullable): The name of the next mechanism to try or %NULL if all
  *          mechanisms have been exhausted.
diff --git a/hasl/tests/test-context.c b/hasl/tests/test-context.c
--- a/hasl/tests/test-context.c
+++ b/hasl/tests/test-context.c
@@ -38,44 +38,54 @@
 	HaslContext *ctx = NULL;
 	char *allowed_mechanisms = NULL;
 	char *authzid = NULL;
+	char *password = NULL;
+	char *server_mechanisms = NULL;
 	char *username = NULL;
-	char *password = NULL;
+	gboolean allow_clear_text = FALSE;
 	gboolean tls = FALSE;
-	gboolean allow_clear_text = FALSE;
 
 	ctx = g_object_new(
 		HASL_TYPE_CONTEXT,
+		"allow-clear-text", TRUE,
 		"allowed-mechanisms", "PLAIN",
 		"authzid", "authzid",
+		"password", "password",
+		"server-mechanisms", "EXTERNAL",
+		"tls", TRUE,
 		"username", "username",
-		"password", "password",
-		"tls", TRUE,
-		"allow-clear-text", TRUE,
 		NULL);
 
 	g_assert_true(HASL_IS_CONTEXT(ctx));
 
 	g_object_get(
 		G_OBJECT(ctx),
+		"allow-clear-text", &allow_clear_text,
 		"allowed-mechanisms", &allowed_mechanisms,
 		"authzid", &authzid,
-		"username", &username,
 		"password", &password,
+		"server-mechanisms", &server_mechanisms,
 		"tls", &tls,
-		"allow-clear-text", &allow_clear_text,
+		"username", &username,
 		NULL);
 
+	g_assert_true(allow_clear_text);
+
 	g_assert_cmpstr(allowed_mechanisms, ==, "PLAIN");
+	g_clear_pointer(&allowed_mechanisms, g_free);
+
 	g_assert_cmpstr(authzid, ==, "authzid");
-	g_assert_cmpstr(username, ==, "username");
+	g_clear_pointer(&authzid, g_free);
+
 	g_assert_cmpstr(password, ==, "password");
+	g_clear_pointer(&password, g_free);
+
+	g_assert_cmpstr(server_mechanisms, ==, "EXTERNAL");
+	g_clear_pointer(&server_mechanisms, g_free);
+
 	g_assert_true(tls);
-	g_assert_true(allow_clear_text);
 
-	g_clear_pointer(&allowed_mechanisms, g_free);
-	g_clear_pointer(&authzid, g_free);
+	g_assert_cmpstr(username, ==, "username");
 	g_clear_pointer(&username, g_free);
-	g_clear_pointer(&password, g_free);
 
 	g_clear_object(&ctx);
 }
@@ -195,23 +205,6 @@
 }
 
 static void
-test_hasl_context_next_null_supported(void) {
-	HaslContext *context = NULL;
-	const char *next = NULL;
-
-	context = hasl_context_new();
-
-	next = hasl_context_next(context);
-	g_assert_null(next);
-
-	/* Make sure we don't move past the last item. */
-	next = hasl_context_next(context);
-	g_assert_null(next);
-
-	g_clear_object(&context);
-}
-
-static void
 test_hasl_context_next_plain_and_external(void) {
 	HaslContext *context = NULL;
 	const char *next = NULL;
@@ -284,6 +277,94 @@
 }
 
 static void
+test_hasl_context_next_uses_allowed_mechanisms(void) {
+	HaslContext *context = NULL;
+	const char *next = NULL;
+
+	context = hasl_context_new();
+	hasl_context_set_allowed_mechanisms(context, "EXTERNAL");
+
+	next = hasl_context_next(context);
+	g_assert_cmpstr(next, ==, "EXTERNAL");
+
+	g_assert_finalize_object(context);
+}
+
+static void
+test_hasl_context_next_uses_supported(void) {
+	HaslContext *context = NULL;
+	const char *next = NULL;
+
+	context = hasl_context_new();
+
+	/* We don't want to hard code what mechanism should be first here so we
+	 * just check that we got a mechanism.
+	 */
+	next = hasl_context_next(context);
+	g_assert_nonnull(next);
+
+	g_assert_finalize_object(context);
+}
+
+static void
+test_hasl_context_next_uses_server_mechanisms(void) {
+	HaslContext *context = NULL;
+	const char *next = NULL;
+
+	context = hasl_context_new();
+	hasl_context_set_server_mechanisms(context, "EXTERNAL");
+
+	next = hasl_context_next(context);
+	g_assert_cmpstr(next, ==, "EXTERNAL");
+
+	g_assert_finalize_object(context);
+}
+
+static void
+test_hasl_context_next_prefers_allowed_mechanisms(void) {
+	HaslContext *context = NULL;
+	const char *next = NULL;
+
+	/* Build the context with server and allowed mechanisms. */
+	context = hasl_context_new();
+	hasl_context_set_username(context, "homer");
+	hasl_context_set_password(context, "0dy$$3y");
+	hasl_context_set_allow_clear_text(context, TRUE);
+	hasl_context_set_allowed_mechanisms(context, "EXTERNAL");
+	hasl_context_set_server_mechanisms(context, "ANONYMOUS");
+
+	/* Verify that allowed are preferred. */
+	next = hasl_context_next(context);
+	g_assert_cmpstr(next, ==, "EXTERNAL");
+
+	/* Reset the context and remove allowed. */
+	hasl_context_done(context);
+	hasl_context_set_allowed_mechanisms(context, NULL);
+
+	/* Verify that the server mechanisms are returned. */
+	next = hasl_context_next(context);
+	g_assert_cmpstr(next, ==, "ANONYMOUS");
+
+	/* Reset the context and change the allowed mechanisms. */
+	hasl_context_done(context);
+	hasl_context_set_allowed_mechanisms(context, "PLAIN");
+
+	/* Verify that the allowed mechanisms are preferred again. */
+	next = hasl_context_next(context);
+	g_assert_cmpstr(next, ==, "PLAIN");
+
+	/* Reset the context and remove the server mechanisms. */
+	hasl_context_done(context);
+	hasl_context_set_server_mechanisms(context, NULL);
+
+	/* Verify that the allowed mechanisms are still returned. */
+	next = hasl_context_next(context);
+	g_assert_cmpstr(next, ==, "PLAIN");
+
+	g_assert_finalize_object(context);
+}
+
+static void
 test_hasl_context_done_new(void) {
 	HaslContext *context = NULL;
 
@@ -327,6 +408,30 @@
 	g_assert_finalize_object(context);
 }
 
+static void
+test_hasl_context_allowed_mechanisms_null(void) {
+	HaslContext *context = hasl_context_new();
+
+	/* If NULL isn't allowed by a g_return_if_fail then a g_warning will be
+	 * output which will cause the test to fail.
+	 */
+	hasl_context_set_allowed_mechanisms(context, NULL);
+
+	g_assert_finalize_object(context);
+}
+
+static void
+test_hasl_context_server_mechanisms_null(void) {
+	HaslContext *context = hasl_context_new();
+
+	/* If NULL isn't allowed by a g_return_if_fail then a g_warning will be
+	 * output which will cause the test to fail.
+	 */
+	hasl_context_set_server_mechanisms(context, NULL);
+
+	g_assert_finalize_object(context);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -350,17 +455,29 @@
 	g_test_add_func("/hasl/context/current-mechanism",
 	                test_hasl_context_current_mechanism);
 
-	g_test_add_func("/hasl/context/next/null_supported",
-	                test_hasl_context_next_null_supported);
 	g_test_add_func("/hasl/context/next/plain_and_external",
 	                test_hasl_context_next_plain_and_external);
 	g_test_add_func("/hasl/context/next/plain_not_possible",
 	                test_hasl_context_next_plain_not_possible);
 	g_test_add_func("/hasl/context/next/plain_not_possible_only_mechanism",
 	                test_hasl_context_next_plain_not_possible_only_mechanism);
+	g_test_add_func("/hasl/context/next/uses-allowed-mechanisms",
+	                test_hasl_context_next_uses_allowed_mechanisms);
+
+	g_test_add_func("/hasl/context/next/uses_supported",
+	                test_hasl_context_next_uses_supported);
+	g_test_add_func("/hasl/context/next/uses-server-mechanisms",
+	                test_hasl_context_next_uses_server_mechanisms);
+	g_test_add_func("/hasl/context/next/prefers-allowed-mechanisms",
+	                test_hasl_context_next_prefers_allowed_mechanisms);
 
 	g_test_add_func("/hasl/context/done/new", test_hasl_context_done_new);
 	g_test_add_func("/hasl/context/done/used", test_hasl_context_done_used);
 
+	g_test_add_func("/hasl/context/allowed-mechanisms/null",
+	                test_hasl_context_allowed_mechanisms_null);
+	g_test_add_func("/hasl/context/server-mechanisms/null",
+	                test_hasl_context_server_mechanisms_null);
+
 	return g_test_run();
 }
