[Dovecot] PATCH: mysql authentication
Here's a patch that implements mysql authentication. I started with the pgsql files and tweaked them to use mysql instead. It works for me, but there might be a couple of memory leaks. I'm welcome to suggestions on how to clean it up so it can be committed. Enjoy! Matt diff -u -r --new-file work/dovecot-0.99.10/doc/dovecot-mysql.conf work.patched/dovecot-0.99.10/doc/dovecot-mysql.conf --- work/dovecot-0.99.10/doc/dovecot-mysql.conf Wed Dec 31 18:00:00 1969 +++ work.patched/dovecot-0.99.10/doc/dovecot-mysql.conf Fri Aug 15 13:43:05 2003 @@ -0,0 +1,70 @@ +# For the mysql passdb module, you'll need a database with a table that +# contains fields for at least the userid and password. If you want to +# use the user@domain syntax, you might want to have a separate domain +# field as well. +# +# If your users all have the same uig/gid, and have predictable home +# directories, you can use the static userdb module to generate the home +# dir based on the userid and domain. In this case, you won't need fields +# for home, uid, or gid in the database. +# +# If you prefer to use the mysql userdb module, you'll want to add fields +# for home, uid, and gid. Here is an example table: +# +# CREATE TABLE users ( +# userid VARCHAR(128) NOT NULL, +# password VARCHAR(64) NOT NULL, +# home VARCHAR(256) NOT NULL, +# uid INTEGER NOT NULL, +# gid INTEGER NOT NULL, +# active CHAR(1) DEFAULT 'Y' NOT NULL +# ); + +db_host = localhost +db_port = 3306 +#db_unix_socket = /var/tmp/mysql.sock +db = users +db_user = dovecot-db +db_passwd = opensesame +db_client_flags = 0 + +# Default password scheme. +# +# Currently supported schemes include PLAIN, PLAIN-MD5, DIGEST-MD5, and CRYPT. +# +#default_pass_scheme = PLAIN-MD5 + +# Query to retrieve the password. +# +# The query should return one row, one column. If more than one row or column +# is returned, authentication will automatically fail. +# +# Available substitutions: +# %u = entire userid +# %n = user part of user@domain +# %d = domain part of user@domain +# +# Example: +# password_query = SELECT password FROM users WHERE userid = '%n' AND domain = '%d' +# password_query = SELECT password FROM users WHERE userid = '%u' AND active = 'Y' +# +#password_query = SELECT password FROM users WHERE userid = '%u' + +# Query to retrieve the user information. +# +# The query must return only one row. The columns to return are: +# home - Home directory +# mail - MAIL environment +# system_user - System user name (for initgroups()) +# uid - System UID +# gid - System GID +# +# Either home or mail is required. uid and gid are required. If more than one +# row is returned or there's missing fields, login will automatically fail. +# +# Examples +# user_query = SELECT home, uid, gid FROM users WHERE userid = '%n' AND domain = '%d' +# user_query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%u' +# user_query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%u' +# +#user_query = SELECT home, uid, gid FROM users WHERE userid = '%u' diff -u -r --new-file work/dovecot-0.99.10/src/auth/Makefile.am work.patched/dovecot-0.99.10/src/auth/Makefile.am --- work/dovecot-0.99.10/src/auth/Makefile.am Sun May 18 07:26:28 2003 +++ work.patched/dovecot-0.99.10/src/auth/Makefile.am Fri Aug 15 16:47:21 2003 @@ -19,6 +19,7 @@ auth-module.c \ db-ldap.c \ db-pgsql.c \ + db-mysql.c \ db-passwd-file.c \ login-connection.c \ main.c \ @@ -38,6 +39,7 @@ passdb-shadow.c \ passdb-vpopmail.c \ passdb-pgsql.c \ + passdb-mysql.c \ password-scheme.c \ userdb.c \ userdb-ldap.c \ @@ -45,7 +47,8 @@ userdb-passwd-file.c \ userdb-static.c \ userdb-vpopmail.c \ - userdb-pgsql.c + userdb-pgsql.c \ + userdb-mysql.c noinst_HEADERS = \ auth-login-interface.h \ @@ -54,6 +57,7 @@ auth-module.h \ db-ldap.h \ db-pgsql.h \ + db-mysql.h \ db-passwd-file.h \ common.h \ login-connection.h \ diff -u -r --new-file work/dovecot-0.99.10/src/auth/db-mysql.c work.patched/dovecot-0.99.10/src/auth/db-mysql.c --- work/dovecot-0.99.10/src/auth/db-mysql.c Wed Dec 31 18:00:00 1969 +++ work.patched/dovecot-0.99.10/src/auth/db-mysql.c Fri Aug 22 17:56:44 2003 @@ -0,0 +1,181 @@ +/* Copyright (C) 2003 Alex Howansky, Timo Sirainen */ + +#include "config.h" +#undef HAVE_CONFIG_H + +#if defined(PASSDB_MYSQL) || defined(USERDB_MYSQL) + +#include "common.h" +#include "network.h" +#include "str.h" +#include "settings.h" +#include "db-mysql.h" + +#include <limits.h> +#include <stddef.h> +#include <stdlib.h> + +#define DEF(type, name) { type, #name, offsetof(struct mysql_settings, name) } + +static struct setting_def setting_defs[] = { + DEF(SET_STR, db_host), + DEF(SET_STR, db_port), + DEF(SET_STR, db_unix_socket), + DEF(SET_STR, db), + DEF(SET_STR, db_user), + DEF(SET_STR, db_passwd), + DEF(SET_STR, db_client_flags), + DEF(SET_STR, password_query), + DEF(SET_STR, user_query), + DEF(SET_STR, default_pass_scheme) +}; + +struct mysql_settings default_mysql_settings = { + MEMBER(db_host) "localhost", + MEMBER(db_port) "0", + MEMBER(db_unix_socket) "/var/tmp/mysql.sock", + MEMBER(db) "email_accounts", + MEMBER(db_user) "dovecot", + MEMBER(db_passwd) "changeme", + MEMBER(db_client_flags) "0", + MEMBER(password_query) "SELECT password FROM users WHERE userid = '%u'", + MEMBER(user_query) "SELECT home, uid, gid FROM users WHERE userid = '%u'", + MEMBER(default_pass_scheme) "PLAIN-MD5" +}; + +static struct mysql_connection *mysql_connections = NULL; + +static int mysql_conn_open(struct mysql_connection *conn); +static void mysql_conn_close(struct mysql_connection *conn); + +void db_mysql_query(struct mysql_connection *conn, const char *query, + struct mysql_request *request) +{ + MYSQL_RES *res; + int failed; + + if (!conn->connected) { + if (!mysql_conn_open(conn)) { + request->callback(conn, request, NULL); + return; + } + } + + if (verbose_debug) + i_info("MYSQL: Performing query: %s", query); + + if (mysql_query(conn->mysql, query)) + i_info("MYSQL: Error executing query \"%s\": %s", query, + mysql_error(conn->mysql)); + + if ((res = mysql_store_result(conn->mysql))) + failed = FALSE; + else { + i_info("MYSQL: Error retrieving results: %s", + mysql_error(conn->mysql)); + failed = TRUE; + } + + request->callback(conn, request, failed ? NULL : res); + mysql_free_result(res); + i_free(request); +} + +static int mysql_conn_open(struct mysql_connection *conn) +{ + if (conn->connected) + return TRUE; + + if (conn->mysql == NULL) { + conn->mysql = mysql_init(NULL); + if (conn->mysql == NULL) { + i_error("MYSQL: mysql_init failed"); + return FALSE; + } + + if (!mysql_real_connect(conn->mysql, conn->set.db_host, + conn->set.db_user, conn->set.db_passwd, + conn->set.db, + atoi(conn->set.db_port), + conn->set.db_unix_socket, + strtoul(conn->set.db_client_flags, + NULL, 10))) { + i_error("MYSQL: Can't connect to database %s: %s", + conn->set.db, mysql_error(conn->mysql)); + return FALSE; + } + } + + conn->connected = TRUE; + return TRUE; +} + +static void mysql_conn_close(struct mysql_connection *conn) +{ + conn->connected = FALSE; + + if (conn->mysql != NULL) { + mysql_close(conn->mysql); + conn->mysql = NULL; + } +} + +static struct mysql_connection *mysql_conn_find(const char *config_path) +{ + struct mysql_connection *conn; + + for (conn = mysql_connections; conn != NULL; conn = conn->next) { + if (strcmp(conn->config_path, config_path) == 0) + return conn; + } + + return NULL; +} + +static const char *parse_setting(const char *key, const char *value, + void *context) +{ + struct mysql_connection *conn = context; + + return parse_setting_from_defs(conn->pool, setting_defs, + &conn->set, key, value); +} + +struct mysql_connection *db_mysql_init(const char *config_path) +{ + struct mysql_connection *conn; + pool_t pool; + + conn = mysql_conn_find(config_path); + if (conn != NULL) { + conn->refcount++; + return conn; + } + + pool = pool_alloconly_create("mysql_connection", 1024); + conn = p_new(pool, struct mysql_connection, 1); + conn->pool = pool; + + conn->refcount = 1; + + conn->config_path = p_strdup(pool, config_path); + conn->set = default_mysql_settings; + settings_read(config_path, parse_setting, conn); + + (void)mysql_conn_open(conn); + + conn->next = mysql_connections; + mysql_connections = conn; + return conn; +} + +void db_mysql_unref(struct mysql_connection *conn) +{ + if (--conn->refcount > 0) + return; + + mysql_conn_close(conn); + pool_unref(conn->pool); +} + +#endif diff -u -r --new-file work/dovecot-0.99.10/src/auth/db-mysql.h work.patched/dovecot-0.99.10/src/auth/db-mysql.h --- work/dovecot-0.99.10/src/auth/db-mysql.h Wed Dec 31 18:00:00 1969 +++ work.patched/dovecot-0.99.10/src/auth/db-mysql.h Fri Aug 15 13:40:56 2003 @@ -0,0 +1,51 @@ +#ifndef __DB_MYSQL_H +#define __DB_MYSQL_H + +#include <mysql.h> + +struct mysql_connection; +struct mysql_request; + +typedef void mysql_query_callback_t(struct mysql_connection *conn, + struct mysql_request *request, + MYSQL_RES *res); + +struct mysql_settings { + const char *db_host; + const char *db_port; + const char *db_unix_socket; + const char *db; + const char *db_user; + const char *db_passwd; + const char *db_client_flags; + const char *password_query; + const char *user_query; + const char *default_pass_scheme; +}; + +struct mysql_connection { + struct mysql_connection *next; + + pool_t pool; + int refcount; + + char *config_path; + struct mysql_settings set; + + MYSQL *mysql; + + unsigned int connected:1; +}; + +struct mysql_request { + mysql_query_callback_t *callback; + void *context; +}; + +void db_mysql_query(struct mysql_connection *conn, const char *query, + struct mysql_request *request); + +struct mysql_connection *db_mysql_init(const char *config_path); +void db_mysql_unref(struct mysql_connection *conn); + +#endif diff -u -r --new-file work/dovecot-0.99.10/src/auth/master-connection.c work.patched/dovecot-0.99.10/src/auth/master-connection.c --- work/dovecot-0.99.10/src/auth/master-connection.c Mon May 26 10:27:13 2003 +++ work.patched/dovecot-0.99.10/src/auth/master-connection.c Fri Aug 22 17:56:18 2003 @@ -55,7 +55,7 @@ reply.virtual_user_idx = reply_add(buf, user->virtual_user); reply.mail_idx = reply_add(buf, user->mail); - p = strstr(user->home, "/./"); + p = user->home ? strstr(user->home, "/./") : NULL; if (p == NULL) { reply.home_idx = reply_add(buf, user->home); reply.chroot_idx = reply_add(buf, NULL); diff -u -r --new-file work/dovecot-0.99.10/src/auth/passdb-mysql.c work.patched/dovecot-0.99.10/src/auth/passdb-mysql.c --- work/dovecot-0.99.10/src/auth/passdb-mysql.c Wed Dec 31 18:00:00 1969 +++ work.patched/dovecot-0.99.10/src/auth/passdb-mysql.c Fri Aug 15 14:24:44 2003 @@ -0,0 +1,171 @@ +/* Copyright (C) 2003 Alex Howansky, Timo Sirainen */ + +#include "config.h" +#undef HAVE_CONFIG_H + +#ifdef PASSDB_MYSQL + +#include "common.h" +#include "str.h" +#include "strescape.h" +#include "var-expand.h" +#include "password-scheme.h" +#include "db-mysql.h" +#include "passdb.h" + +#include <mysql.h> +#include <stdlib.h> +#include <string.h> + +struct passdb_mysql_connection { + struct mysql_connection *conn; +}; + +struct passdb_mysql_request { + struct mysql_request request; + + enum passdb_credentials credentials; + union { + verify_plain_callback_t *verify_plain; + lookup_credentials_callback_t *lookup_credentials; + } callback; + + char password[1]; +}; + +static struct passdb_mysql_connection *passdb_mysql_conn; + +static void mysql_handle_request(struct mysql_connection *conn, + struct mysql_request *request, MYSQL_RES *res) +{ + struct passdb_mysql_request *mysql_request = + (struct passdb_mysql_request *) request; + struct auth_request *auth_request = request->context; + const char *user, *password, *scheme; + int ret = 0; + + user = auth_request->user; + password = NULL; + + if (res != NULL) { + if (mysql_num_rows(res) == 0) { + if (verbose) + i_info("mysql(%s): Unknown user", user); + } else if (mysql_num_rows(res) > 1) { + i_error("mysql(%s): Multiple matches for user", user); + } else if (mysql_num_fields(res) != 1) { + i_error("mysql(%s): Password query returned " + "more than one field", user); + } else { + MYSQL_ROW row; + + row = mysql_fetch_row(res); + if (row) + password = t_strdup(row[0]); /* XXX binary? */ + } + } + + scheme = password_get_scheme(&password); + if (scheme == NULL) { + scheme = conn->set.default_pass_scheme; + i_assert(scheme != NULL); + } + + if (mysql_request->credentials != -1) { + passdb_handle_credentials(mysql_request->credentials, + user, password, scheme, + mysql_request->callback.lookup_credentials, + auth_request); + return; + } + + /* verify plain */ + if (password == NULL) { + mysql_request->callback.verify_plain(PASSDB_RESULT_USER_UNKNOWN, + auth_request); + return; + } + + ret = password_verify(mysql_request->password, password, + scheme, user); + if (ret < 0) + i_error("mysql(%s): Unknown password scheme %s", user, scheme); + else if (ret == 0) { + if (verbose) + i_info("mysql(%s): Password mismatch", user); + } + + mysql_request->callback.verify_plain(ret > 0 ? PASSDB_RESULT_OK : + PASSDB_RESULT_PASSWORD_MISMATCH, + auth_request); +} + +static void mysql_lookup_pass(struct auth_request *auth_request, + struct mysql_request *mysql_request) +{ + struct mysql_connection *conn = passdb_mysql_conn->conn; + const char *query; + string_t *str; + + str = t_str_new(512); + var_expand(str, conn->set.password_query, + str_escape(auth_request->user), NULL); + query = str_c(str); + + mysql_request->callback = mysql_handle_request; + mysql_request->context = auth_request; + + db_mysql_query(conn, query, mysql_request); +} + +static void +mysql_verify_plain(struct auth_request *request, const char *password, + verify_plain_callback_t *callback) +{ + struct passdb_mysql_request *mysql_request; + + mysql_request = i_malloc(sizeof(struct passdb_mysql_request) + + strlen(password)); + mysql_request->credentials = -1; + mysql_request->callback.verify_plain = callback; + strcpy(mysql_request->password, password); + + mysql_lookup_pass(request, &mysql_request->request); +} + +static void mysql_lookup_credentials(struct auth_request *request, + enum passdb_credentials credentials, + lookup_credentials_callback_t *callback) +{ + struct passdb_mysql_request *mysql_request; + + mysql_request = i_new(struct passdb_mysql_request, 1); + mysql_request->credentials = credentials; + mysql_request->callback.lookup_credentials = callback; + + mysql_lookup_pass(request, &mysql_request->request); +} + +static void passdb_mysql_init(const char *args) +{ + struct mysql_connection *conn; + + passdb_mysql_conn = i_new(struct passdb_mysql_connection, 1); + passdb_mysql_conn->conn = conn = db_mysql_init(args); +} + +static void passdb_mysql_deinit(void) +{ + db_mysql_unref(passdb_mysql_conn->conn); + i_free(passdb_mysql_conn); +} + +struct passdb_module passdb_mysql = { + passdb_mysql_init, + passdb_mysql_deinit, + + mysql_verify_plain, + mysql_lookup_credentials +}; + +#endif diff -u -r --new-file work/dovecot-0.99.10/src/auth/passdb.c work.patched/dovecot-0.99.10/src/auth/passdb.c --- work/dovecot-0.99.10/src/auth/passdb.c Sun May 18 07:26:28 2003 +++ work.patched/dovecot-0.99.10/src/auth/passdb.c Fri Aug 15 11:39:31 2003 @@ -110,6 +110,10 @@ if (strcasecmp(name, "pgsql") == 0) passdb = &passdb_pgsql; #endif +#ifdef PASSDB_MYSQL + if (strcasecmp(name, "mysql") == 0) + passdb = &passdb_mysql; +#endif #ifdef HAVE_MODULES passdb_module = passdb != NULL ? NULL : auth_module_open(name); if (passdb_module != NULL) { diff -u -r --new-file work/dovecot-0.99.10/src/auth/passdb.h work.patched/dovecot-0.99.10/src/auth/passdb.h --- work/dovecot-0.99.10/src/auth/passdb.h Thu Mar 20 09:46:33 2003 +++ work.patched/dovecot-0.99.10/src/auth/passdb.h Fri Aug 15 16:45:43 2003 @@ -58,6 +58,7 @@ extern struct passdb_module passdb_vpopmail; extern struct passdb_module passdb_ldap; extern struct passdb_module passdb_pgsql; +extern struct passdb_module passdb_mysql; void passdb_init(void); void passdb_deinit(void); diff -u -r --new-file work/dovecot-0.99.10/src/auth/userdb-mysql.c work.patched/dovecot-0.99.10/src/auth/userdb-mysql.c --- work/dovecot-0.99.10/src/auth/userdb-mysql.c Wed Dec 31 18:00:00 1969 +++ work.patched/dovecot-0.99.10/src/auth/userdb-mysql.c Fri Aug 22 17:53:56 2003 @@ -0,0 +1,148 @@ +/* Copyright (C) 2003 Alex Howansky, Timo Sirainen */ + +#include "config.h" +#undef HAVE_CONFIG_H + +#ifdef USERDB_MYSQL + +#include "common.h" +#include "str.h" +#include "strescape.h" +#include "var-expand.h" +#include "db-mysql.h" +#include "userdb.h" + +#include <mysql.h> +#include <stdlib.h> +#include <string.h> + +struct userdb_mysql_connection { + struct mysql_connection *conn; +}; + +struct userdb_mysql_request { + struct mysql_request request; + userdb_callback_t *userdb_callback; + + char username[1]; /* variable width */ +}; + +static struct userdb_mysql_connection *userdb_mysql_conn; + +static int is_result_valid(MYSQL_RES *res) +{ + + if (res == NULL) { + i_error("MYSQL: Query failed"); + return FALSE; + } + + if (mysql_num_rows(res) == 0) { + if (verbose) + i_error("MYSQL: Authenticated user not found"); + return FALSE; + } + + /* XXX */ + if (mysql_num_fields(res) < 3) { + i_error("MYSQL: Not enough fields returned"); + return FALSE; + } +/* XXX + if (PQfnumber(res, "uid") == -1) { + i_error("MYSQL: User query did not return 'uid' field"); + return FALSE; + } + + if (PQfnumber(res, "gid") == -1) { + i_error("MYSQL: User query did not return 'gid' field"); + return FALSE; + } +*/ + + return TRUE; +} + +static const char *my_get_str(MYSQL_RES *res, MYSQL_ROW row, const char *field) +{ + int i, n_fields; + unsigned long *lengths; + MYSQL_FIELD *fields; + + n_fields = mysql_num_fields(res); + lengths = mysql_fetch_lengths(res); + fields = mysql_fetch_fields(res); + for (i = 0; i < n_fields; i++) + if (strcmp(field, fields[i].name) == 0) + return (const char *) lengths[i] == 0 ? + NULL : t_strdup(row[i]); + + return NULL; +} + +static void mysql_handle_request(struct mysql_connection *conn __attr_unused__, + struct mysql_request *request, MYSQL_RES *res) +{ + struct userdb_mysql_request *urequest = + (struct userdb_mysql_request *) request; + struct user_data user; + MYSQL_ROW row; + + if (res != NULL && is_result_valid(res) && + (row = mysql_fetch_row(res))) { + memset(&user, 0, sizeof(user)); + user.virtual_user = urequest->username; + user.system_user = my_get_str(res, row, "system_user"); + user.home = my_get_str(res, row, "home"); + user.mail = my_get_str(res, row, "mail"); + user.uid = atoi(my_get_str(res, row, "uid")); /* XXX leak */ + user.gid = atoi(my_get_str(res, row, "gid")); /* XXX leak */ + urequest->userdb_callback(&user, request->context); + } else { + urequest->userdb_callback(NULL, request->context); + } +} + +static void userdb_mysql_lookup(const char *user, userdb_callback_t *callback, + void *context) +{ + struct mysql_connection *conn = userdb_mysql_conn->conn; + struct userdb_mysql_request *request; + const char *query; + string_t *str; + + str = t_str_new(512); + var_expand(str, conn->set.user_query, str_escape(user), NULL); + query = str_c(str); + + request = i_malloc(sizeof(struct userdb_mysql_request) + strlen(user)); + request->request.callback = mysql_handle_request; + request->request.context = context; + request->userdb_callback = callback; + strcpy(request->username, user); + + db_mysql_query(conn, query, &request->request); +} + +static void userdb_mysql_init(const char *args) +{ + struct mysql_connection *conn; + + userdb_mysql_conn = i_new(struct userdb_mysql_connection, 1); + userdb_mysql_conn->conn = conn = db_mysql_init(args); +} + +static void userdb_mysql_deinit(void) +{ + db_mysql_unref(userdb_mysql_conn->conn); + i_free(userdb_mysql_conn); +} + +struct userdb_module userdb_mysql = { + userdb_mysql_init, + userdb_mysql_deinit, + + userdb_mysql_lookup +}; + +#endif diff -u -r --new-file work/dovecot-0.99.10/src/auth/userdb.c work.patched/dovecot-0.99.10/src/auth/userdb.c --- work/dovecot-0.99.10/src/auth/userdb.c Sun May 18 07:26:28 2003 +++ work.patched/dovecot-0.99.10/src/auth/userdb.c Fri Aug 22 13:07:21 2003 @@ -49,6 +49,10 @@ if (strcasecmp(name, "pgsql") == 0) userdb = &userdb_pgsql; #endif +#ifdef USERDB_MYSQL + if (strcasecmp(name, "mysql") == 0) + userdb = &userdb_mysql; +#endif #ifdef HAVE_MODULES userdb_module = userdb != NULL ? NULL : auth_module_open(name); if (userdb_module != NULL) { diff -u -r --new-file work/dovecot-0.99.10/src/auth/userdb.h work.patched/dovecot-0.99.10/src/auth/userdb.h --- work/dovecot-0.99.10/src/auth/userdb.h Sun May 18 07:26:28 2003 +++ work.patched/dovecot-0.99.10/src/auth/userdb.h Fri Aug 22 13:07:53 2003 @@ -29,6 +29,7 @@ extern struct userdb_module userdb_vpopmail; extern struct userdb_module userdb_ldap; extern struct userdb_module userdb_pgsql; +extern struct userdb_module userdb_mysql; void userdb_init(void); void userdb_deinit(void);
On Saturday, Aug 30, 2003, at 02:16 Europe/Helsinki, Matthew Reimer wrote:
Here's a patch that implements mysql authentication. I started with the pgsql files and tweaked them to use mysql instead. It works for me, but there might be a couple of memory leaks. I'm welcome to suggestions on how to clean it up so it can be committed.
Thank you. I'll look at it more closely later, but it looked fine with a quick look.
It would be nice to be able to use asynchronous database lookups. I'm not sure how easy that is with MySQL, of if it's possible at all. With PostgreSQL it looked annoyingly difficult so I haven't done it yet.
Synchronous calls anyway mean that there's only one SQL statement executing at a time and that may slow down the authentication if there's a _lot_ of users logging in constantly. If that's a problem, growing auth_count should help at least some. It specifies the number of auth processes that respond to the authentication queries.
And some day I'll probably move the sql stuff into separate lib-sql and have only a single sql authenticator..
participants (2)
-
Matthew Reimer
-
Timo Sirainen