# HG changeset patch
# Node ID 4f5c0388daaa
# Parent  4350ec6c9fc6
diff --git a/hasl/haslcontext.c b/hasl/haslcontext.c
--- a/hasl/haslcontext.c
+++ b/hasl/haslcontext.c
@@ -19,6 +19,7 @@
 
 #include "haslcontext.h"
 
+#include "haslmechanismanonymous.h"
 #include "haslmechanismexternal.h"
 #include "haslmechanismplain.h"
 
@@ -133,6 +134,8 @@
 	 */
 	ctx->current_mechanism_index = -1;
 
+	hasl_context_add_mechanism(ctx, "ANONYMOUS",
+	                           HASL_TYPE_MECHANISM_ANONYMOUS);
 	hasl_context_add_mechanism(ctx, "EXTERNAL", HASL_TYPE_MECHANISM_EXTERNAL);
 	hasl_context_add_mechanism(ctx, "PLAIN", HASL_TYPE_MECHANISM_PLAIN);
 }
diff --git a/hasl/haslmechanismanonymous.h b/hasl/haslmechanismanonymous.h
new file mode 100644
--- /dev/null
+++ b/hasl/haslmechanismanonymous.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 Hasl Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef HASL_MECHANISM_ANONYMOUS_H
+#define HASL_MECHANISM_ANONYMOUS_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <hasl/haslcontext.h>
+#include <hasl/haslmechanism.h>
+
+G_BEGIN_DECLS
+
+#define HASL_MECHANISM_ANONYMOUS_DOMAIN (g_quark_from_static_string("hasl-mechanism-anonymous"))
+
+/**
+ * HaslMechanismAnonymous:
+ *
+ * This mechanism implements the ANONYMOUS method described in
+ * [RFC 4505](https://www.rfc-editor.org/rfc/rfc4505). It uses the value of
+ * [property@Context:username] as the token.
+ *
+ * This mechanism is not secure, so you must set
+ * [property@Context:allow-clear-text] to %TRUE if [property@Context:tls] is
+ * %FALSE.
+ *
+ * Since: 0.2.0
+ */
+
+#define HASL_TYPE_MECHANISM_ANONYMOUS (hasl_mechanism_anonymous_get_type())
+G_DECLARE_FINAL_TYPE(HaslMechanismAnonymous, hasl_mechanism_anonymous, HASL,
+                     MECHANISM_ANONYMOUS, HaslMechanism)
+
+G_END_DECLS
+
+#endif /* HASL_MECHANISM_ANONYMOUS_H */
diff --git a/hasl/haslmechanismanonymous.c b/hasl/haslmechanismanonymous.c
new file mode 100644
--- /dev/null
+++ b/hasl/haslmechanismanonymous.c
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 Hasl Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n-lib.h>
+
+#include <stringprep.h>
+
+#include "haslmechanismanonymous.h"
+
+#include "haslcore.h"
+
+struct _HaslMechanismAnonymous {
+	HaslMechanism parent;
+};
+
+G_DEFINE_TYPE(HaslMechanismAnonymous, hasl_mechanism_anonymous,
+              HASL_TYPE_MECHANISM)
+
+/******************************************************************************
+ * HaslMechanism Implementation
+ *****************************************************************************/
+static gboolean
+hasl_mechanism_anonymous_possible(G_GNUC_UNUSED HaslMechanism *mechanism,
+                                  HaslContext *context,
+                                  GError **error)
+{
+	const char *value = NULL;
+
+	value = hasl_context_get_username(context);
+	if(value == NULL || value[0] == '\0') {
+		g_set_error_literal(error, HASL_MECHANISM_ANONYMOUS_DOMAIN, 0,
+		                    _("missing username"));
+
+		return FALSE;
+	}
+
+	if(!hasl_context_get_allow_clear_text(context)) {
+		if(!hasl_context_get_tls(context)) {
+			g_set_error_literal(error, HASL_MECHANISM_ANONYMOUS_DOMAIN, 0,
+			                    _("plain text is not allowed without TLS"));
+
+			return FALSE;
+		}
+	}
+
+	return TRUE;
+}
+
+static HaslMechanismResult
+hasl_mechanism_anonymous_step(G_GNUC_UNUSED HaslMechanism *mechanism,
+                              HaslContext *ctx,
+                              G_GNUC_UNUSED const guint8 *server_in,
+                              G_GNUC_UNUSED gsize server_in_length,
+                              guint8 **client_out,
+                              gsize *client_out_length,
+                              GError **error)
+{
+	GByteArray *data = NULL;
+	const char *username = NULL;
+	char buffer[1024]; /* Up to 1020 bytes is allowed per the RFC. */
+	int res = 0;
+
+	username = hasl_context_get_username(ctx);
+	if(username == NULL || username[0] == '\0') {
+		g_set_error_literal(error, HASL_MECHANISM_ANONYMOUS_DOMAIN, 0,
+		                    _("no username provided"));
+
+		return HASL_MECHANISM_RESULT_ERROR;
+	}
+
+	/* Copy the username into the buffer. */
+	g_strlcpy(buffer, username, sizeof(buffer));
+
+	/* Use the stringprep trace profile per the RFC. */
+	res = stringprep(buffer, sizeof(buffer), 0, stringprep_trace);
+	if(res != STRINGPREP_OK) {
+		g_set_error(error, HASL_MECHANISM_ANONYMOUS_DOMAIN, 0,
+		            _("stringprep failed: %s"), stringprep_strerror(res));
+
+		return HASL_MECHANISM_RESULT_ERROR;
+	}
+
+	/* We have a saslpreped username, so we can build our payload. */
+	data = g_byte_array_new();
+	g_byte_array_append(data, (guint8 *)buffer, strlen(buffer));
+
+	/* Set our out variables and return. */
+	if(client_out_length != NULL) {
+		*client_out_length = data->len;
+	}
+
+	*client_out = g_byte_array_free(data, FALSE);
+
+	return HASL_MECHANISM_RESULT_SUCCESS;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+hasl_mechanism_anonymous_init(G_GNUC_UNUSED HaslMechanismAnonymous *anonymous)
+{
+}
+
+static void
+hasl_mechanism_anonymous_class_init(HaslMechanismAnonymousClass *klass) {
+	HaslMechanismClass *mechanism_class = HASL_MECHANISM_CLASS(klass);
+
+	mechanism_class->possible = hasl_mechanism_anonymous_possible;
+	mechanism_class->step = hasl_mechanism_anonymous_step;
+}
diff --git a/hasl/meson.build b/hasl/meson.build
--- a/hasl/meson.build
+++ b/hasl/meson.build
@@ -1,6 +1,7 @@
 HASL_SOURCES = [
   'haslcontext.c',
   'haslmechanism.c',
+  'haslmechanismanonymous.c',
   'haslmechanismexternal.c',
   'haslmechanismplain.c',
 ]
@@ -9,6 +10,7 @@
   'haslcontext.h',
   'haslcore.h',
   'haslmechanism.h',
+  'haslmechanismanonymous.h',
   'haslmechanismexternal.h',
   'haslmechanismplain.h',
 ]
@@ -60,7 +62,7 @@
   HASL_BUILT_HEADERS,
   c_args : ['-DHASL_COMPILATION', '-DG_LOG_USE_STRUCTURED',
             '-DG_LOG_DOMAIN="Hasl"'],
-  dependencies: [glib, gobject],
+  dependencies: [glib, gobject, libidn],
   include_directories : toplevel_inc,
   install : true,
   version: HASL_LIB_VERSION)
@@ -102,7 +104,7 @@
     include_directories : [toplevel_inc, include_directories('.')],
     link_with : libhasl,
     sources : [HASL_GENERATED_TARGETS, HASL_BUILT_HEADERS],
-    dependencies : [glib, gobject]
+    dependencies : [glib, gobject, libidn]
 )
 
 meson.override_dependency('hasl', hasl_dep)
diff --git a/hasl/tests/meson.build b/hasl/tests/meson.build
--- a/hasl/tests/meson.build
+++ b/hasl/tests/meson.build
@@ -2,6 +2,7 @@
   'context',
   'full',
   'mechanism',
+  'mechanism-anonymous',
   'mechanism-external',
   'mechanism-plain',
 ]
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
@@ -122,7 +122,7 @@
 	ctx = hasl_context_new();
 	supported_mechanisms = hasl_context_get_supported_mechanisms(ctx);
 
-	g_assert_cmpstr(supported_mechanisms, ==, "EXTERNAL PLAIN");
+	g_assert_cmpstr(supported_mechanisms, ==, "ANONYMOUS EXTERNAL PLAIN");
 
 	g_free(supported_mechanisms);
 	g_clear_object(&ctx);
@@ -146,7 +146,8 @@
 	/* Now get the list and check it. */
 	supported_mechanisms = hasl_context_get_supported_mechanisms(ctx);
 
-	g_assert_cmpstr(supported_mechanisms, ==, "DEBUG EXTERNAL PLAIN TEST");
+	g_assert_cmpstr(supported_mechanisms, ==,
+	                "ANONYMOUS DEBUG EXTERNAL PLAIN TEST");
 
 	g_free(supported_mechanisms);
 	g_clear_object(&ctx);
diff --git a/hasl/tests/test-mechanism-anonymous.c b/hasl/tests/test-mechanism-anonymous.c
new file mode 100644
--- /dev/null
+++ b/hasl/tests/test-mechanism-anonymous.c
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2023 Hasl Developers
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <hasl.h>
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+test_hasl_mechanism_anonymous_error_test(HaslContext *context) {
+	HaslMechanism *mechanism = NULL;
+	HaslMechanismResult result = 0;
+	GError *error = NULL;
+	guint8 *client_out = NULL;
+	gsize client_out_length = 0;
+
+	mechanism = g_object_new(HASL_TYPE_MECHANISM_ANONYMOUS, NULL);
+
+	result = hasl_mechanism_step(mechanism, context, NULL, 0, &client_out,
+	                             &client_out_length, &error);
+
+	g_assert_error(error, HASL_MECHANISM_ANONYMOUS_DOMAIN, 0);
+	g_clear_error(&error);
+
+	g_assert_cmpint(result, ==, HASL_MECHANISM_RESULT_ERROR);
+
+	g_assert_null(client_out);
+	g_assert_cmpint(client_out_length, ==, 0);
+
+	g_clear_object(&mechanism);
+}
+
+static void
+test_hasl_mechanism_anonymous_possible(HaslContext *context, gboolean expected,
+                                       gboolean should_error)
+{
+	HaslMechanism *mechanism = NULL;
+	GError *error = NULL;
+	gboolean ret = FALSE;
+
+	mechanism = g_object_new(HASL_TYPE_MECHANISM_ANONYMOUS, NULL);
+
+	ret = hasl_mechanism_possible(mechanism, context, &error);
+
+	if(should_error) {
+		g_assert_error(error, HASL_MECHANISM_ANONYMOUS_DOMAIN, 0);
+		g_clear_error(&error);
+	} else {
+		g_assert_no_error(error);
+	}
+
+	g_assert_true(ret == expected);
+
+	g_clear_object(&mechanism);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_hasl_mechanism_anonymous_new(void) {
+	HaslMechanism *mechanism = NULL;
+
+	mechanism = g_object_new(HASL_TYPE_MECHANISM_ANONYMOUS, NULL);
+
+	g_assert_true(HASL_IS_MECHANISM_ANONYMOUS(mechanism));
+
+	g_clear_object(&mechanism);
+}
+
+static void
+test_hasl_mechanism_anonymous_username_required_null(void) {
+	HaslContext *context = NULL;
+
+	context = hasl_context_new();
+	hasl_context_set_password(context, "hunter2");
+
+	test_hasl_mechanism_anonymous_error_test(context);
+
+	g_clear_object(&context);
+}
+
+static void
+test_hasl_mechanism_anonymous_username_required_empty(void) {
+	HaslContext *context = NULL;
+
+	context = hasl_context_new();
+	hasl_context_set_username(context, "");
+	hasl_context_set_password(context, "hunter2");
+
+	test_hasl_mechanism_anonymous_error_test(context);
+
+	g_clear_object(&context);
+}
+
+static void
+test_hasl_mechanism_anonymous_possible_empty_context(void) {
+	HaslContext *context = hasl_context_new();
+
+	test_hasl_mechanism_anonymous_possible(context, FALSE, TRUE);
+
+	g_clear_object(&context);
+}
+
+static void
+test_hasl_mechanism_anonymous_possible_username(void) {
+	HaslContext *context = hasl_context_new();
+
+	hasl_context_set_tls(context, TRUE);
+	hasl_context_set_username(context, "alice");
+
+	test_hasl_mechanism_anonymous_possible(context, TRUE, FALSE);
+
+	g_clear_object(&context);
+}
+
+static void
+test_hasl_mechanism_anonymous_possible_allow_plain_in_clear_without_tls(void) {
+	HaslContext *context = hasl_context_new();
+
+	hasl_context_set_username(context, "alice");
+	hasl_context_set_allow_clear_text(context, TRUE);
+
+	test_hasl_mechanism_anonymous_possible(context, TRUE, FALSE);
+
+	g_clear_object(&context);
+}
+
+static void
+test_hasl_mechanism_anonymous_possible_allow_plain_in_clear_with_tls(void) {
+	HaslContext *context = hasl_context_new();
+
+	hasl_context_set_username(context, "alice");
+	hasl_context_set_allow_clear_text(context, TRUE);
+	hasl_context_set_tls(context, TRUE);
+
+	test_hasl_mechanism_anonymous_possible(context, TRUE, FALSE);
+
+	g_clear_object(&context);
+}
+
+static void
+test_hasl_mechanism_anonymous_possible_allow_plain_in_clear_required(void) {
+	HaslContext *context = hasl_context_new();
+
+	hasl_context_set_username(context, "alice");
+	hasl_context_set_tls(context, FALSE);
+
+	test_hasl_mechanism_anonymous_possible(context, FALSE, TRUE);
+
+	g_clear_object(&context);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/hasl/mechanism-anonymous/new",
+	                test_hasl_mechanism_anonymous_new);
+
+	g_test_add_func("/hasl/mechanism-anonymous/username-required/null",
+	                test_hasl_mechanism_anonymous_username_required_null);
+	g_test_add_func("/hasl/mechanism-anonymous/username-required/empty",
+	                test_hasl_mechanism_anonymous_username_required_empty);
+
+	g_test_add_func("/hasl/mechanism-anonymous/possible/empty-context",
+	                test_hasl_mechanism_anonymous_possible_empty_context);
+	g_test_add_func("/hasl/mechanism-anonymous/possible/username",
+	                test_hasl_mechanism_anonymous_possible_username);
+
+	g_test_add_func("/hasl/mechanism-anonymous/possible/allow-plain-in-clear-without-tls",
+	                test_hasl_mechanism_anonymous_possible_allow_plain_in_clear_without_tls);
+	g_test_add_func("/hasl/mechanism-anonymous/possible/allow-plain-in-clear-with-tls",
+	                test_hasl_mechanism_anonymous_possible_allow_plain_in_clear_with_tls);
+	g_test_add_func("/hasl/mechanism-anonymous/possible/allow-plain-in-clear-required",
+	                test_hasl_mechanism_anonymous_possible_allow_plain_in_clear_required);
+
+	return g_test_run();
+}
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -37,6 +37,7 @@
 ###############################################################################
 glib = dependency('glib-2.0', version : '>= 2.70')
 gobject = dependency('gobject-2.0', version : '>= 2.70')
+libidn = dependency('libidn', version : '>= 1.41')
 
 ###############################################################################
 # NLS
diff --git a/po/POTFILES b/po/POTFILES
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -5,5 +5,6 @@
 hasl/tests/test-context.c
 hasl/tests/test-full.c
 hasl/tests/test-mechanism.c
+hasl/tests/test-mechanism-anonymous.c
 hasl/tests/test-mechanism-external.c
 hasl/tests/test-mechanism-plain.c
