]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
imap-urlauth: Allow connections from services other than IMAP.
authorStephan Bosch <stephan@rename-it.nl>
Mon, 26 Sep 2016 07:25:32 +0000 (09:25 +0200)
committerTimo Sirainen <timo.sirainen@dovecot.fi>
Mon, 11 Dec 2017 13:44:18 +0000 (15:44 +0200)
The imap-urlauth service detects the new submission service and assigns the appropriate privileges.
The dovecot-token authentication mechanism provides information on which service connected to it.

12 files changed:
src/auth/mech-dovecot-token.c
src/imap-urlauth/imap-urlauth-client.c
src/imap-urlauth/imap-urlauth-client.h
src/imap-urlauth/imap-urlauth-login.c
src/imap-urlauth/imap-urlauth-worker.c
src/imap-urlauth/imap-urlauth.c
src/imap/imap-client.c
src/lib-imap-urlauth/imap-urlauth-connection.c
src/lib-imap-urlauth/imap-urlauth-connection.h
src/lib-imap-urlauth/imap-urlauth-private.h
src/lib-imap-urlauth/imap-urlauth.c
src/lib-imap-urlauth/imap-urlauth.h

index 16e62bf4c5c415be32848f089b58cb55c084ae14..ed0b8015479d5c8a7dc89546edfac42caf017cc2 100644 (file)
@@ -54,6 +54,7 @@ mech_dovecot_token_auth_continue(struct auth_request *request,
                if (auth_token != NULL &&
                    strcmp(auth_token, valid_token) == 0) {
                        request->passdb_success = TRUE;
+                       auth_request_set_field(request, "userdb_client_service", service, "");
                        auth_request_success(request, NULL, 0);
                } else {
                        auth_request_fail(request);
index cda6bf2b91da22b2dac6c79282cf35d60fb3de31..9f9404120090167f487c3b81f43cce4a0b2228ef 100644 (file)
@@ -45,8 +45,8 @@ static int client_worker_connect(struct client *client);
 static void client_worker_disconnect(struct client *client);
 static void client_worker_input(struct client *client);
 
-int client_create(const char *username, int fd_in, int fd_out,
-                 const struct imap_urlauth_settings *set,
+int client_create(const char *service, const char *username,
+                 int fd_in, int fd_out, const struct imap_urlauth_settings *set,
                  struct client **client_r)
 {
        struct client *client;
@@ -86,8 +86,8 @@ int client_create(const char *username, int fd_in, int fd_out,
                }
        }
 
-       if (username != NULL)
-               client->username = i_strdup(username);
+       client->username = i_strdup(username);
+       client->service = i_strdup(service);
 
        client->output = o_stream_create_fd(fd_out, (size_t)-1);
 
@@ -126,7 +126,7 @@ void client_send_line(struct client *client, const char *fmt, ...)
 
 static int client_worker_connect(struct client *client)
 {
-       static const char handshake[] = "VERSION\timap-urlauth-worker\t1\t0\n";
+       static const char handshake[] = "VERSION\timap-urlauth-worker\t2\t0\n";
        const char *socket_path;
        ssize_t ret;
        unsigned char data;
@@ -221,6 +221,8 @@ client_worker_input_line(struct client *client, const char *response)
                str_append(str, "ACCESS\t");
                if (client->username != NULL)
                        str_append_tabescaped(str, client->username);
+               str_append(str, "\t");
+               str_append_tabescaped(str, client->service);
                if (client->set->mail_debug)
                        str_append(str, "\tdebug");
                if (array_count(&client->access_apps) > 0) {
@@ -338,8 +340,8 @@ void client_destroy(struct client *client, const char *reason)
 
        fd_close_maybe_stdio(&client->fd_in, &client->fd_out);
 
-       if (client->username != NULL)
-               i_free(client->username);
+       i_free(client->username);
+       i_free(client->service);
        array_free(&client->access_apps);
        i_free(client);
 
index 41b905606af7962220aaa6e26cdecdec304782c4..408c3bc90f64db6f779a7659eeee4a8df08e1bab 100644 (file)
@@ -19,7 +19,7 @@ struct client {
        struct istream *ctrl_input;
        struct timeout *to_idle;
 
-       char *username;
+       char *username, *service;
        ARRAY_TYPE(const_string) access_apps;
 
        /* settings: */
@@ -33,8 +33,8 @@ struct client {
 extern struct client *imap_urlauth_clients;
 extern unsigned int imap_urlauth_client_count;
 
-int client_create(const char *username, int fd_in, int fd_out,
-                 const struct imap_urlauth_settings *set,
+int client_create(const char *service, const char *username,
+                 int fd_in, int fd_out, const struct imap_urlauth_settings *set,
                  struct client **client_r);
 void client_destroy(struct client *client, const char *reason);
 
index ef00ce6bf7570b7b78613223691abe30ce5880ae..7d10b2b5fb8232d18a68728a2d9cb239b8ecc290 100644 (file)
@@ -13,7 +13,7 @@
 #include "client-common.h"
 #include "imap-urlauth-login-settings.h"
 
-#define IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION 1
+#define IMAP_URLAUTH_PROTOCOL_MAJOR_VERSION 2
 #define IMAP_URLAUTH_PROTOCOL_MINOR_VERSION 0
 
 struct imap_urlauth_client {
@@ -41,7 +41,7 @@ imap_urlauth_client_auth_result(struct client *client,
 
 static void imap_urlauth_client_handle_input(struct client *client)
 {
-#define AUTH_ARG_COUNT 5
+#define AUTH_ARG_COUNT 6
        struct imap_urlauth_client *uauth_client =
                (struct imap_urlauth_client *)client;
        struct net_unix_cred cred;
@@ -67,15 +67,22 @@ static void imap_urlauth_client_handle_input(struct client *client)
                return;
 
        /* read authentication info from input;
-          "AUTH"\t<session-pid>\t<auth-username>\t<session_id>\t<token> */
+          "AUTH"\t<service>\t<session-pid>\t<auth-username>\t<session_id>\t<token> */
        args = t_strsplit_tabescaped(line);
        if (str_array_length(args) < AUTH_ARG_COUNT ||
-           strcmp(args[0], "AUTH") != 0 || str_to_pid(args[1], &pid) < 0) {
+           strcmp(args[0], "AUTH") != 0 || str_to_pid(args[2], &pid) < 0) {
                i_error("IMAP URLAUTH client sent unexpected AUTH input: %s", line);
                client_destroy(client, "Disconnected: Unexpected input");
                return;
        }
 
+       /* only imap and submission have direct access to urlauth service */
+       if (strcmp(args[1], "imap") != 0 && strcmp(args[1], "submission") != 0) {
+               i_error("IMAP URLAUTH accessed from inappropriate service: %s", args[1]);
+               client_destroy(client, "Disconnected: Unexpected input");
+               return;
+       }
+
        /* verify session pid if possible */
        if (net_getunixcred(client->fd, &cred) == 0 &&
            cred.pid != (pid_t)-1 && pid != cred.pid) {
@@ -91,8 +98,8 @@ static void imap_urlauth_client_handle_input(struct client *client)
                string_t *init_resp;
                unsigned int i;
 
-               str_append(auth_data, "imap");
-               for (i = 1; i < AUTH_ARG_COUNT; i++) {
+               str_append(auth_data, args[1]);
+               for (i = 2; i < AUTH_ARG_COUNT; i++) {
                        str_append_c(auth_data, '\0');
                        str_append(auth_data, args[i]);
                }
index 08ad3017c02c2a58b6600a135a9801c381449231..67a5c954f41b6a5e5a86979516873a58983c66b2 100644 (file)
@@ -42,7 +42,7 @@
 #define IS_STANDALONE() \
         (getenv(MASTER_IS_PARENT_ENV) == NULL)
 
-#define IMAP_URLAUTH_WORKER_PROTOCOL_MAJOR_VERSION 1
+#define IMAP_URLAUTH_WORKER_PROTOCOL_MAJOR_VERSION 2
 #define IMAP_URLAUTH_WORKER_PROTOCOL_MINOR_VERSION 0
 
 struct client {
@@ -55,7 +55,7 @@ struct client {
        struct ostream *output, *ctrl_output;
        struct timeout *to_idle;
 
-       char *access_user;
+       char *access_user, *access_service;
        ARRAY_TYPE(string) access_apps;
 
        struct mail_storage_service_user *service_user;
@@ -255,6 +255,7 @@ static void client_destroy(struct client *client)
        if (client->service_user != NULL)
                mail_storage_service_user_unref(&client->service_user);
        i_free(client->access_user);
+       i_free(client->access_service);
        array_foreach_modifiable(&client->access_apps, app)
                i_free(*app);
        array_free(&client->access_apps);
@@ -636,14 +637,16 @@ client_handle_user_command(struct client *client, const char *cmd,
        config.url_host = set->imap_urlauth_host;
        config.url_port = set->imap_urlauth_port;
        config.access_user = client->access_user;
+       config.access_service = client->access_service;
        config.access_anonymous = client->access_anonymous;
        config.access_applications =
                (const void *)array_get(&client->access_apps, &count);
                
        client->urlauth_ctx = imap_urlauth_init(client->mail_user, &config);
        if (client->debug) {
-               i_debug("Providing access to user account `%s' on behalf of `%s'",
-                       mail_user->username, client->access_user);
+               i_debug("Providing access to user account `%s' on behalf of user `%s' "
+                       "using service `%s'", mail_user->username, client->access_user,
+                       client->access_service);
        }
 
        i_set_failure_prefix("imap-urlauth[%s](%s->%s): ",
@@ -854,13 +857,14 @@ static void client_ctrl_input(struct client *client)
                return;
        }
        args++;
-       if (*args == NULL) {
+       if (args[0] == NULL || args[1] == NULL) {
                i_error("Invalid ACCESS command: %s", str_sanitize(line, 80));
                client_abort(client, "Control session aborted: Invalid command");
                return;
        }
 
        i_assert(client->access_user == NULL);
+       i_assert(client->access_service == NULL);
        if (**args != '\0') {
                client->access_user = i_strdup(*args);
                client->access_anonymous = FALSE;
@@ -868,6 +872,9 @@ static void client_ctrl_input(struct client *client)
                client->access_user = i_strdup("anonymous");
                client->access_anonymous = TRUE;
        }
+       args++;
+       client->access_service = i_strdup(*args);
+
        i_set_failure_prefix("imap-urlauth[%s](%s): ",
                             my_pid, client->access_user);
 
@@ -912,8 +919,8 @@ static void client_ctrl_input(struct client *client)
        o_stream_set_flush_callback(client->output, client_output, client);
 
        if (client->debug) {
-               i_debug("Worker activated for access by user %s",
-                       client->access_user);
+               i_debug("Worker activated for access by user `%s' using service `%s'",
+                       client->access_user, client->access_service);
        }
 }
 
index 5ef31ef5d7897f126f64aa734a5a0e0bc0cff7ab..ca0ca97cccb68bb22307d3084bfd6197136531b0 100644 (file)
@@ -47,6 +47,7 @@ The imap-urlauth service thus consists of three separate stages:
 #include "lib-signals.h"
 #include "ioloop.h"
 #include "buffer.h"
+#include "array.h"
 #include "istream.h"
 #include "ostream.h"
 #include "path-util.h"
@@ -102,11 +103,12 @@ static void imap_urlauth_die(void)
 }
 
 static int
-client_create_from_input(const char *username, int fd_in, int fd_out)
+client_create_from_input(const char *service, const char *username,
+               int fd_in, int fd_out)
 {
        struct client *client;
 
-       if (client_create(username, fd_in, fd_out,
+       if (client_create(service, username, fd_in, fd_out,
                          imap_urlauth_settings, &client) < 0)
                return -1;
 
@@ -123,7 +125,7 @@ static void main_stdio_run(const char *username)
        if (username == NULL)
                i_fatal("USER environment missing");
 
-       (void)client_create_from_input(username, STDIN_FILENO, STDOUT_FILENO);
+       (void)client_create_from_input("", username, STDIN_FILENO, STDOUT_FILENO);
 }
 
 static void
@@ -133,6 +135,9 @@ login_client_connected(const struct master_login_client *client,
        const char *msg = "NO\n";
        struct auth_user_reply reply;
        struct net_unix_cred cred;
+       const char *const *fields;
+       const char *service = NULL;
+       unsigned int count, i;
 
        auth_user_fields_parse(extra_fields, pool_datastack_create(), &reply);
 
@@ -149,10 +154,27 @@ login_client_connected(const struct master_login_client *client,
                return;
        }
 
+       fields = array_get(&reply.extra_fields, &count);
+       for (i = 0; i < count; i++) {
+               if (strncmp(fields[i], "client_service=", 15) == 0) {
+                       service = fields[i] + 15;
+                       break;
+               }
+       }
+
+       if (service == NULL) {
+               i_error("Auth did not yield required client_service field (BUG).");
+               if (write(client->fd, msg, strlen(msg)) < 0) {
+                       /* ignored */
+               }
+               net_disconnect(client->fd);
+               return;
+       }
+
        if (reply.anonymous)
                username = NULL;
 
-       if (client_create_from_input(username, client->fd, client->fd) < 0)
+       if (client_create_from_input(service, username, client->fd, client->fd) < 0)
                net_disconnect(client->fd);
 }
 
index e8b28160644bc3a1bc6c3e11cce434761c62fedf..2a778cf681ba51c1a34624fdecf8b00dc4e8a90a 100644 (file)
@@ -65,8 +65,9 @@ static void client_init_urlauth(struct client *client)
        config.socket_path = t_strconcat(client->user->set->base_dir,
                                         "/"IMAP_URLAUTH_SOCKET_NAME, NULL);
        config.session_id = client->session_id;
-       config.access_anonymous = client->user->anonymous;
        config.access_user = client->user->username;
+       config.access_service = "imap";
+       config.access_anonymous = client->user->anonymous;
 
        client->urlauth_ctx = imap_urlauth_init(client->user, &config);
 }
index 396e8b79ec2e787c109933bbebf4a97c12ca7143..13b56924322b607d55e1e9820212cbadb40f0b74 100644 (file)
@@ -56,7 +56,7 @@ struct imap_urlauth_target {
 struct imap_urlauth_connection {
        int refcount;
 
-       char *path, *session_id;
+       char *path, *service, *session_id;
        struct mail_user *user;
 
        int fd;
@@ -87,7 +87,7 @@ struct imap_urlauth_connection {
 
 #define IMAP_URLAUTH_RESPONSE_TIMEOUT_MSECS 2*60*1000
 
-#define IMAP_URLAUTH_HANDSHAKE "VERSION\timap-urlauth\t1\t0\n"
+#define IMAP_URLAUTH_HANDSHAKE "VERSION\timap-urlauth\t2\t0\n"
 
 #define IMAP_URLAUTH_MAX_INLINE_LITERAL_SIZE (1024*32)
 
@@ -105,14 +105,15 @@ static void imap_urlauth_connection_fail
        (struct imap_urlauth_connection *conn);
 
 struct imap_urlauth_connection *
-imap_urlauth_connection_init(const char *path, struct mail_user *user,
-                            const char *session_id,
+imap_urlauth_connection_init(const char *path, const char *service,
+                            struct mail_user *user, const char *session_id,
                             unsigned int idle_timeout_msecs)
 {
        struct imap_urlauth_connection *conn;
 
        conn = i_new(struct imap_urlauth_connection, 1);
        conn->refcount = 1;
+       conn->service = i_strdup(service);
        conn->path = i_strdup(path);
        if (session_id != NULL)
                conn->session_id = i_strdup(session_id);
@@ -132,6 +133,7 @@ void imap_urlauth_connection_deinit(struct imap_urlauth_connection **_conn)
        imap_urlauth_connection_abort(conn, NULL);
 
        i_free(conn->path);
+       i_free(conn->service);
        if (conn->session_id != NULL)
                i_free(conn->session_id);
 
@@ -891,7 +893,7 @@ imap_urlauth_connection_do_connect(struct imap_urlauth_connection *conn)
 
        if (conn->user->auth_token == NULL) {
                i_error("imap-urlauth: cannot authenticate because no auth token "
-                       "is available for this session (standalone IMAP?).");
+                       "is available for this session (running standalone?).");
                imap_urlauth_connection_abort(conn, NULL);
                return -1;
        }
@@ -917,7 +919,8 @@ imap_urlauth_connection_do_connect(struct imap_urlauth_connection *conn)
        conn->state = IMAP_URLAUTH_STATE_AUTHENTICATING;
 
        str = t_str_new(128);
-       str_printfa(str, IMAP_URLAUTH_HANDSHAKE"AUTH\t%s\t", my_pid);
+       str_printfa(str, IMAP_URLAUTH_HANDSHAKE"AUTH\t%s\t%s\t",
+               conn->service, my_pid);
        str_append_tabescaped(str, conn->user->username);
        str_append_c(str, '\t');
        if (conn->session_id != NULL)
index afe4ab7a0bb8b7bcedeecaeb13eeef0bc2d9407a..b60186a2c74b55305cf87104a91402fa8606c851 100644 (file)
@@ -11,8 +11,8 @@ imap_urlauth_request_callback_t(struct imap_urlauth_fetch_reply *reply,
 /* If reconnect_callback is specified, it's called when connection is lost.
    If the callback returns FALSE, reconnection isn't attempted. */
 struct imap_urlauth_connection *
-imap_urlauth_connection_init(const char *path, struct mail_user *user,
-                            const char *session_id,
+imap_urlauth_connection_init(const char *path, const char *service,
+                            struct mail_user *user, const char *session_id,
                             unsigned int idle_timeout_msecs);
 void imap_urlauth_connection_deinit(struct imap_urlauth_connection **conn);
 
index 3dcf798e9ec2a0f859a34d94549ee1a783d5b7d7..3b3622c8deb8fce488b479b21f018fff796bed4d 100644 (file)
@@ -11,6 +11,7 @@ struct imap_urlauth_context {
        in_port_t url_port;
 
        char *access_user;
+       char *access_service;
        const char **access_applications;
 
        bool access_anonymous:1;
index 410939981468879c2d07ffc1600bb2c72c936ff6..39fff7bd071d0a75253a6e75ddcf4e83b7ace87f 100644 (file)
@@ -46,6 +46,7 @@ imap_urlauth_init(struct mail_user *user,
                uctx->access_user = i_strdup("anonymous");
        else
                uctx->access_user = i_strdup(config->access_user);
+       uctx->access_service = i_strdup(config->access_service);
        uctx->access_anonymous = config->access_anonymous;
        if (config->access_applications != NULL &&
            *config->access_applications != NULL) {
@@ -59,7 +60,7 @@ imap_urlauth_init(struct mail_user *user,
 
        if (config->socket_path != NULL) {
                uctx->conn = imap_urlauth_connection_init(config->socket_path,
-                                       user, config->session_id, timeout);
+                                       config->access_service, user, config->session_id, timeout);
        }
        return uctx;
 }
@@ -74,6 +75,7 @@ void imap_urlauth_deinit(struct imap_urlauth_context **_uctx)
                imap_urlauth_connection_deinit(&uctx->conn);
        i_free(uctx->url_host);
        i_free(uctx->access_user);
+       i_free(uctx->access_service);
        i_free(uctx->access_applications);
        i_free(uctx);
 }
@@ -117,8 +119,8 @@ imap_urlauth_internal_verify(const char *rumpurl,
 }
 
 static bool
-access_applications_have_access(struct imap_url *url,
-                               const char *const *access_applications)
+access_applications_have_access(struct imap_urlauth_context *uctx,
+                               struct imap_url *url, const char *const *access_applications)
 {
        const char *const *application;
 
@@ -131,16 +133,17 @@ access_applications_have_access(struct imap_url *url,
                bool have_userid = FALSE;
                size_t len = strlen(app);
 
-               if (app[len-1] == '+') {
+               if (app[len-1] == '+')
                        have_userid = TRUE;
-                       app = t_strndup(app, len-1);
-               }
 
-               if (strcasecmp(url->uauth_access_application, app) == 0) {
-                       if (have_userid)
-                               return url->uauth_access_user != NULL;
-                       else
+               if (strncasecmp(url->uauth_access_application, app, len-1) == 0) {
+                       if (!have_userid) {
+                               /* this access application must have no userid */
                                return url->uauth_access_user == NULL;
+                       }
+
+                       /* this access application must have a userid */
+                       return (!uctx->access_anonymous && url->uauth_access_user != NULL);
                }
        }
        return FALSE;
@@ -151,51 +154,60 @@ imap_urlauth_check_access(struct imap_urlauth_context *uctx,
                          struct imap_url *url, bool ignore_unknown,
                          const char **error_r)
 {
+       const char *userid;
+
        if (url->uauth_access_application == NULL) {
                *error_r = "URL is missing URLAUTH";
                return FALSE;
        }
 
-       if (strcasecmp(url->uauth_access_application, "user") == 0) {
-               /* user+<access_user> */
-               if (uctx->access_anonymous ||
-                   strcasecmp(url->uauth_access_user, uctx->access_user) != 0)  {
-                       if (uctx->access_anonymous) {
-                               *error_r = t_strdup_printf(
-                                       "No 'user+%s' access allowed for anonymous user",
-                                       url->uauth_access_user);
-                       } else {
-                               *error_r = t_strdup_printf("No 'user+%s' access allowed for user %s",
-                                       url->uauth_access_user, uctx->access_user);
-                       }
-                       return FALSE;
-               }
-       } else if (strcasecmp(url->uauth_access_application, "authuser") == 0) {
-               /* authuser */
-               if (uctx->access_anonymous) {
-                       *error_r = "No 'authuser' access allowed for anonymous user";
-                       return FALSE;
+       if (strcmp(uctx->access_service, "imap") == 0) {
+               /* these access types are only allowed if URL is accessed through imap */
+               if (strcasecmp(url->uauth_access_application, "user") == 0) {
+                       /* user+<access_user> */
+                       if (!uctx->access_anonymous ||
+                                 strcasecmp(url->uauth_access_user, uctx->access_user) == 0)
+                               return TRUE;
+               } else if (strcasecmp(url->uauth_access_application, "authuser") == 0) {
+                       /* authuser */
+                       if (!uctx->access_anonymous)
+                               return TRUE;
+               } else if (strcasecmp(url->uauth_access_application, "anonymous") == 0) {
+                       /* anonymous */
+                       return TRUE;
+               } else if (ignore_unknown || access_applications_have_access
+                       (uctx, url, uctx->access_applications)) {
+                       return TRUE;
                }
-       } else if (strcasecmp(url->uauth_access_application, "anonymous") == 0) {
-               /* anonymous */
-       } else if (!ignore_unknown &&
-                  !access_applications_have_access(url, uctx->access_applications)) {
-               const char *userid = url->uauth_access_user == NULL ? "" :
-                       t_strdup_printf("+%s", url->uauth_access_user);
-
-               if (uctx->access_anonymous) {
+       } else if (strcmp(uctx->access_service, "submission") == 0) {
+               /* accessed directly through submission service */
+
+               if (strcasecmp(url->uauth_access_application, "submit") != 0) {
+                       userid = url->uauth_access_user == NULL ? "" :
+                               t_strdup_printf("+%s", url->uauth_access_user);
                        *error_r = t_strdup_printf(
-                               "No '%s%s' access allowed for anonymous user",
+                               "No '%s%s' access allowed for submission service",
                                url->uauth_access_application, userid);
-               } else {
-                       *error_r = t_strdup_printf(
-                               "No '%s%s' access allowed for user %s",
-                               url->uauth_access_application, userid, uctx->access_user);
+                       return FALSE;
+               } else if (!uctx->access_anonymous &&
+                       strcasecmp(url->uauth_access_user, uctx->access_user) == 0) {
+                       return TRUE;
                }
-               return FALSE;
        }
 
-       return TRUE;
+       userid = url->uauth_access_user == NULL ? "" :
+               t_strdup_printf("+%s", url->uauth_access_user);
+
+       if (uctx->access_anonymous) {
+               *error_r = t_strdup_printf(
+                       "No '%s%s' access allowed for anonymous user",
+                       url->uauth_access_application, userid);
+       } else {
+               *error_r = t_strdup_printf(
+                       "No '%s%s' access allowed for user %s",
+                       url->uauth_access_application, userid, uctx->access_user);
+       }
+       return FALSE;
 }
 
 static bool
index 2c48163053df8704e1685bea58b2b0515b5d5201..b93271b5af10b3a9861aa0d1415b2df07a638c27 100644 (file)
@@ -16,8 +16,13 @@ struct imap_urlauth_config {
        const char *socket_path;
        const char *session_id;
 
+       /* the user who is requesting access to URLAUTHs */
        const char *access_user;
+       /* ... is using this service (i.e. imap or submission) */
+       const char *access_service;
+       /* ... represents these applications */
        const char *const *access_applications;
+       /* ... is anonymous? */
        bool access_anonymous;
 };