[glib-networking/mcatanzaro/tls-thread] progress



commit feb0751798b995f3ad07f236d3cb63512ccdcfa8
Author: Michael Catanzaro <mcatanzaro gnome org>
Date:   Mon Dec 16 10:22:47 2019 -0600

    progress

 tls/base/gtlsconnection-base.c           | 170 ++++++++++++++++++++++++++++---
 tls/base/gtlsconnection-base.h           |   1 +
 tls/base/gtlsoperationsthread-base.c     |  61 ++++++++---
 tls/base/gtlsoperationsthread-base.h     |  14 ++-
 tls/gnutls/gtlsoperationsthread-gnutls.c |  89 ++++++++++++++++
 5 files changed, 304 insertions(+), 31 deletions(-)
---
diff --git a/tls/base/gtlsconnection-base.c b/tls/base/gtlsconnection-base.c
index 406a31d..c214342 100644
--- a/tls/base/gtlsconnection-base.c
+++ b/tls/base/gtlsconnection-base.c
@@ -54,7 +54,7 @@
  *    communications.
  *  • Implements GDtlsConnection and GDatagramBased, for DTLS and datagram
  *    communications.
- *  • Implements GInitable for failable initialisation.
+ *  • Implements GInitable for failable initialization.
  */
 
 typedef struct
@@ -95,6 +95,7 @@ typedef struct
   GTlsCertificate       *peer_certificate;
   GTlsCertificateFlags   peer_certificate_errors;
 
+  /* FIXME: remove */
   GMutex                 verify_certificate_mutex;
   GCond                  verify_certificate_condition;
   gboolean               peer_certificate_accepted;
@@ -124,6 +125,7 @@ typedef struct
    * future operations). ever_handshaked indicates that TLS has been
    * successfully negotiated at some point.
    */
+  /* FIXME: remove a few of these */
   gboolean       need_handshake;
   gboolean       need_finish_handshake;
   gboolean       sync_handshake_in_progress;
@@ -1445,6 +1447,85 @@ g_tls_connection_base_handshake_thread_verify_certificate (GTlsConnectionBase *t
   return accepted;
 }
 
+static gboolean /* FIXME rename */
+op_thread_handshake (GTlsConnectionBase  *tls,
+                     gint64               timeout,
+                     GCancellable        *cancellable,
+                     GError             **error)
+{
+  GTlsConnectionBasePrivate *priv = g_tls_connection_base_get_instance_private (tls);
+  GTlsConnectionBaseClass *tls_class = G_TLS_CONNECTION_BASE_GET_CLASS (tls);
+  gint64 start_time;
+
+  g_tls_log_debug (tls, "TLS handshake starts");
+
+  start_time = g_get_monotonic_time ();
+
+  priv->started_handshake = FALSE;
+  priv->missing_requested_client_certificate = FALSE;
+
+  if (!claim_op (tls, G_TLS_CONNECTION_BASE_OP_HANDSHAKE,
+                 timeout, cancellable, error))
+    {
+      g_tls_log_debug (tls, "TLS handshake failed: claiming op failed");
+      return FALSE;
+    }
+
+  g_clear_error (&priv->handshake_error);
+
+  if (priv->ever_handshaked && !priv->need_handshake)
+    {
+      GTlsConnectionBaseStatus status;
+
+      /* FIXME: no longer handshake thread */
+      if (tls_class->handshake_thread_safe_renegotiation_status (tls) != 
G_TLS_SAFE_RENEGOTIATION_SUPPORTED_BY_PEER)
+        {
+          g_set_error_literal (error, G_TLS_ERROR, G_TLS_ERROR_MISC,
+                               _("Peer does not support safe renegotiation"));
+          g_tls_log_debug (tls, "TLS handshake failed: peer does not support safe renegotiation");
+          return FALSE;
+        }
+
+      /* Adjust the timeout for the next operation in the sequence. */
+      if (timeout > 0)
+        {
+          timeout -= (g_get_monotonic_time () - start_time);
+          if (timeout <= 0)
+            timeout = 1;
+        }
+
+      /* FIXME: no longer handshake thread */
+      status = tls_class->handshake_thread_request_rehandshake (tls, timeout, cancellable, error);
+      if (status != G_TLS_CONNECTION_BASE_OK)
+        {
+          g_tls_log_debug (tls, "TLS handshake failed: %s", *error ? (*error)->message : "no error");
+          return FALSE;
+        }
+    }
+
+  /* Adjust the timeout for the next operation in the sequence. */
+  if (timeout > 0)
+    {
+      timeout -= (g_get_monotonic_time () - start_time);
+      if (timeout <= 0)
+        timeout = 1;
+    }
+
+  priv->started_handshake = TRUE;
+  g_tls_operations_thread_base_handshake (priv->thread, timeout, cancellable, error);
+  priv->need_handshake = FALSE;
+
+  if (error && *error)
+    {
+      g_tls_log_debug (tls, "TLS handshake failed: %s", (*error)->message);
+      return FALSE;
+    }
+
+  priv->ever_handshaked = TRUE;
+  g_tls_log_debug (tls, "TLS handshake succeeded");
+  return TRUE;
+}
+
 static void
 handshake_thread (GTask        *task,
                   gpointer      object,
@@ -1583,7 +1664,7 @@ crank_sync_handshake_context (GTlsConnectionBase *tls,
   g_mutex_unlock (&priv->op_mutex);
 }
 
-static gboolean
+static gboolean /* FIXME remove */
 finish_handshake (GTlsConnectionBase  *tls,
                   GTask               *task,
                   GError             **error)
@@ -1647,6 +1728,70 @@ finish_handshake (GTlsConnectionBase  *tls,
   return FALSE;
 }
 
+static gboolean /* FIXME rename */
+finish_op_thread_handshake (GTlsConnectionBase  *tls,
+                            gboolean             success,
+                            GError             **error)
+{
+  GTlsConnectionBasePrivate *priv = g_tls_connection_base_get_instance_private (tls);
+  GTlsConnectionBaseClass *tls_class = G_TLS_CONNECTION_BASE_GET_CLASS (tls);
+  gchar *original_negotiated_protocol;
+  GError *my_error = NULL;
+
+  g_tls_log_debug (tls, "finishing TLS handshake");
+
+  original_negotiated_protocol = g_steal_pointer (&priv->negotiated_protocol);
+
+  if (success)
+    {
+      if (tls_class->is_session_resumed && tls_class->is_session_resumed (tls))
+        {
+          /* Because this session was resumed, we skipped certificate
+           * verification on this handshake, so we missed our earlier
+           * chance to set peer_certificate and peer_certificate_errors.
+           * Do so here instead.
+           *
+           * The certificate has already been accepted, so we don't do
+           * anything with the result here.
+           */
+          g_mutex_lock (&priv->verify_certificate_mutex);
+          update_peer_certificate_and_compute_errors (tls);
+          priv->peer_certificate_examined = TRUE;
+          priv->peer_certificate_accepted = TRUE;
+          g_mutex_unlock (&priv->verify_certificate_mutex);
+        }
+
+      /* FIXME: Return an error from the handshake thread instead? */
+      if (priv->peer_certificate && !priv->peer_certificate_accepted)
+        {
+          g_set_error_literal (&my_error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
+                               _("Unacceptable TLS certificate"));
+        }
+    }
+
+  if (tls_class->complete_handshake)
+    {
+      /* If we already have an error, ignore further errors. */
+      tls_class->complete_handshake (tls, &priv->negotiated_protocol, my_error ? NULL : &my_error);
+
+      if (g_strcmp0 (original_negotiated_protocol, priv->negotiated_protocol) != 0)
+        g_object_notify (G_OBJECT (tls), "negotiated-protocol");
+    }
+  g_free (original_negotiated_protocol);
+
+  if (my_error && priv->started_handshake)
+    priv->handshake_error = g_error_copy (my_error);
+
+  if (!my_error) {
+    g_tls_log_debug (tls, "TLS handshake has finished successfully");
+    return TRUE;
+  }
+
+  g_tls_log_debug (tls, "TLS handshake has finished with error: %s", my_error->message);
+  g_propagate_error (error, my_error);
+  return FALSE;
+}
+
 static gboolean
 g_tls_connection_base_handshake (GTlsConnection   *conn,
                                  GCancellable     *cancellable,
@@ -1655,9 +1800,7 @@ g_tls_connection_base_handshake (GTlsConnection   *conn,
   GTlsConnectionBase *tls = G_TLS_CONNECTION_BASE (conn);
   GTlsConnectionBasePrivate *priv = g_tls_connection_base_get_instance_private (tls);
   GTlsConnectionBaseClass *tls_class = G_TLS_CONNECTION_BASE_GET_CLASS (tls);
-  GTask *task;
   gboolean success;
-  gint64 *timeout = NULL;
   GError *my_error = NULL;
 
   g_tls_log_debug (tls, "Starting synchronous TLS handshake");
@@ -1670,20 +1813,15 @@ g_tls_connection_base_handshake (GTlsConnection   *conn,
   if (tls_class->prepare_handshake)
     tls_class->prepare_handshake (tls, priv->advertised_protocols);
 
-  task = g_task_new (conn, cancellable, sync_handshake_thread_completed, NULL);
-  g_task_set_source_tag (task, g_tls_connection_base_handshake);
-  g_task_set_name (task, "[glib-networking] g_tls_connection_base_handshake");
-  g_task_set_return_on_cancel (task, TRUE);
+  success = op_thread_handshake (tls, -1 /* blocking */, cancellable, error);
 
-  timeout = g_new0 (gint64, 1);
-  *timeout = -1; /* blocking */
-  g_task_set_task_data (task, timeout, g_free);
+  g_mutex_lock (&priv->op_mutex);
+  priv->sync_handshake_in_progress = FALSE;
+  g_mutex_unlock (&priv->op_mutex);
 
-  g_task_run_in_thread (task, handshake_thread);
-  crank_sync_handshake_context (tls, cancellable);
+  g_main_context_wakeup (priv->handshake_context);
 
-  success = finish_handshake (tls, task, &my_error);
-  g_object_unref (task);
+  success = finish_op_thread_handshake (tls, success, &my_error);
 
   g_main_context_pop_thread_default (priv->handshake_context);
   g_clear_pointer (&priv->handshake_context, g_main_context_unref);
@@ -1873,7 +2011,7 @@ do_implicit_handshake (GTlsConnectionBase  *tls,
   GTlsConnectionBaseClass *tls_class = G_TLS_CONNECTION_BASE_GET_CLASS (tls);
   gint64 *thread_timeout = NULL;
 
-  g_tls_log_debug (tls, "Implcit TLS handshaking starts");
+  g_tls_log_debug (tls, "Implicit TLS handshaking starts");
 
   /* We have op_mutex */
 
diff --git a/tls/base/gtlsconnection-base.h b/tls/base/gtlsconnection-base.h
index 5e89b09..fa155f3 100644
--- a/tls/base/gtlsconnection-base.h
+++ b/tls/base/gtlsconnection-base.h
@@ -63,6 +63,7 @@ struct _GTlsConnectionBaseClass
 
   GTlsOperationsThreadBase   *(*create_op_thread)           (GTlsConnectionBase   *tls);
 
+  /* FIXME: deal with all the handshaking stuff */
   void                        (*prepare_handshake)          (GTlsConnectionBase   *tls,
                                                              gchar               **advertised_protocols);
   GTlsSafeRenegotiationStatus (*handshake_thread_safe_renegotiation_status)
diff --git a/tls/base/gtlsoperationsthread-base.c b/tls/base/gtlsoperationsthread-base.c
index 85db976..825e6c2 100644
--- a/tls/base/gtlsoperationsthread-base.c
+++ b/tls/base/gtlsoperationsthread-base.c
@@ -78,6 +78,7 @@ typedef struct {
 } GTlsOperationsThreadBasePrivate;
 
 typedef enum {
+  G_TLS_THREAD_OP_HANDSHAKE,
   G_TLS_THREAD_OP_READ,
   G_TLS_THREAD_OP_READ_MESSAGE,
   G_TLS_THREAD_OP_WRITE,
@@ -138,6 +139,14 @@ static GParamSpec *obj_properties[LAST_PROP];
 
 G_DEFINE_TYPE_WITH_PRIVATE (GTlsOperationsThreadBase, g_tls_operations_thread_base, G_TYPE_OBJECT)
 
+GTlsConnectionBase *
+g_tls_operations_thread_base_get_connection (GTlsOperationsThreadBase *self)
+{
+  GTlsOperationsThreadBasePrivate *priv = g_tls_operations_thread_base_get_instance_private (self);
+
+  return priv->connection;
+}
+
 static GTlsThreadOperation *
 g_tls_thread_operation_new (GTlsThreadOperationType   type,
                             GTlsOperationsThreadBase *thread,
@@ -164,13 +173,13 @@ g_tls_thread_operation_new (GTlsThreadOperationType   type,
   switch (type)
     {
     case G_TLS_THREAD_OP_READ:
-      /* fallthrough */
       op->io_condition = G_IO_IN;
       break;
     case G_TLS_THREAD_OP_WRITE:
-      /* fallthrough */
       op->io_condition = G_IO_OUT;
       break;
+    case G_TLS_THREAD_OP_HANDSHAKE:
+      /* fallthrough */
     case G_TLS_THREAD_OP_CLOSE:
       op->io_condition = G_IO_IN | G_IO_OUT;
       break;
@@ -181,6 +190,7 @@ g_tls_thread_operation_new (GTlsThreadOperationType   type,
   return op;
 }
 
+#if 0
 static GTlsThreadOperation *
 g_tls_thread_operation_new_async (GTlsThreadOperationType   type,
                                   GTlsOperationsThreadBase *thread,
@@ -205,6 +215,7 @@ g_tls_thread_operation_new_async (GTlsThreadOperationType   type,
 
   return op;
 }
+#endif
 
 static GTlsThreadOperation *
 g_tls_thread_operation_new_with_input_vectors (GTlsOperationsThreadBase *thread,
@@ -293,14 +304,6 @@ wait_for_op_completion (GTlsThreadOperation *op)
   g_mutex_unlock (&op->finished_mutex);
 }
 
-GTlsConnectionBase *
-g_tls_operations_thread_base_get_connection (GTlsOperationsThreadBase *self)
-{
-  GTlsOperationsThreadBasePrivate *priv = g_tls_operations_thread_base_get_instance_private (self);
-
-  return priv->connection;
-}
-
 static GTlsConnectionBaseStatus
 execute_sync_op (GTlsOperationsThreadBase *self,
                  GTlsThreadOperation      *op /* owned */,
@@ -333,6 +336,7 @@ execute_sync_op (GTlsOperationsThreadBase *self,
   return result;
 }
 
+#if 0
 static void
 execute_async_op (GTlsOperationsThreadBase *self,
                   GTlsThreadOperation      *op)
@@ -341,9 +345,34 @@ execute_async_op (GTlsOperationsThreadBase *self,
 
   g_assert (op->task);
 
+  /* FIXME: Design flaw? Here the queue owns the ops only for async tasks.
+   * But it doesn't free them when destroyed (though there should not be any
+   * when destroyed anyway?). It's confusing to have both owned and unowned ops
+   * stored in the same queue. Do we need ops to be refcounted?
+   */
   g_async_queue_push (priv->queue, g_steal_pointer (&op));
   g_main_context_wakeup (priv->op_thread_context);
 }
+#endif
+
+GTlsConnectionBaseStatus
+g_tls_operations_thread_base_handshake (GTlsOperationsThreadBase  *self,
+                                        gint64                     timeout,
+                                        GCancellable              *cancellable,
+                                        GError                   **error)
+{
+  GTlsOperationsThreadBasePrivate *priv = g_tls_operations_thread_base_get_instance_private (self);
+  GTlsThreadOperation *op;
+
+  op = g_tls_thread_operation_new (G_TLS_THREAD_OP_HANDSHAKE,
+                                   self,
+                                   priv->connection,
+                                   NULL, 0,
+                                   timeout,
+                                   cancellable);
+
+  return execute_sync_op (self, g_steal_pointer (&op), NULL, error);
+}
 
 GTlsConnectionBaseStatus
 g_tls_operations_thread_base_read (GTlsOperationsThreadBase  *self,
@@ -795,8 +824,13 @@ process_op (GAsyncQueue         *queue,
 
   switch (op->type)
     {
+    case G_TLS_THREAD_OP_HANDSHAKE:
+      op->result = base_class->handshake_fn (op->thread,
+                                             op->timeout,
+                                             op->cancellable,
+                                             &op->error);
+      break;
     case G_TLS_THREAD_OP_READ:
-      g_assert (base_class->read_fn);
       op->result = base_class->read_fn (op->thread,
                                         op->data, op->size,
                                         &op->count,
@@ -812,7 +846,6 @@ process_op (GAsyncQueue         *queue,
                                                 &op->error);
       break;
     case G_TLS_THREAD_OP_WRITE:
-      g_assert (base_class->write_fn);
       op->result = base_class->write_fn (op->thread,
                                          op->data, op->size,
                                          &op->count,
@@ -828,7 +861,6 @@ process_op (GAsyncQueue         *queue,
                                                  &op->error);
       break;
     case G_TLS_THREAD_OP_CLOSE:
-      g_assert (base_class->close_fn);
       op->result = base_class->close_fn (op->thread,
                                          op->cancellable,
                                          &op->error);
@@ -882,6 +914,9 @@ finished:
         g_task_return_error (op->task, op->error);
       else
         g_task_return_int (op->task, op->result);
+
+      /* The op is owned only for async ops, not for sync ops. */
+      g_tls_thread_operation_free (op);
     }
   else /* sync op */
     {
diff --git a/tls/base/gtlsoperationsthread-base.h b/tls/base/gtlsoperationsthread-base.h
index ea99778..63f7a34 100644
--- a/tls/base/gtlsoperationsthread-base.h
+++ b/tls/base/gtlsoperationsthread-base.h
@@ -38,6 +38,11 @@ struct _GTlsOperationsThreadBaseClass
 {
   GObjectClass parent_class;
 
+  GTlsConnectionBaseStatus    (*handshake_fn)               (GTlsOperationsThreadBase  *self,
+                                                             gint64                     timeout,
+                                                             GCancellable              *cancellable,
+                                                             GError                   **error);
+
   GTlsConnectionBaseStatus    (*read_fn)                    (GTlsOperationsThreadBase  *self,
                                                              void                      *buffer,
                                                              gsize                      size,
@@ -64,13 +69,18 @@ struct _GTlsOperationsThreadBaseClass
                                                              GCancellable              *cancellable,
                                                              GError                   **error);
 
-  GTlsConnectionBaseStatus    (*close_fn)                   (GTlsOperationsThreadBase  *tls,
+  GTlsConnectionBaseStatus    (*close_fn)                   (GTlsOperationsThreadBase  *self,
                                                              GCancellable              *cancellable,
                                                              GError                   **error);
 };
 
 /* FIXME: remove? */
-GTlsConnectionBase       *g_tls_operations_thread_base_get_connection (GTlsOperationsThreadBase *self);
+GTlsConnectionBase       *g_tls_operations_thread_base_get_connection (GTlsOperationsThreadBase  *self);
+
+GTlsConnectionBaseStatus  g_tls_operations_thread_base_handshake      (GTlsOperationsThreadBase  *self,
+                                                                       gint64                     timeout,
+                                                                       GCancellable              
*cancellable,
+                                                                       GError                   **error);
 
 GTlsConnectionBaseStatus  g_tls_operations_thread_base_read           (GTlsOperationsThreadBase  *self,
                                                                        void                      *buffer,
diff --git a/tls/gnutls/gtlsoperationsthread-gnutls.c b/tls/gnutls/gtlsoperationsthread-gnutls.c
index 7920e4d..11b1bc9 100644
--- a/tls/gnutls/gtlsoperationsthread-gnutls.c
+++ b/tls/gnutls/gtlsoperationsthread-gnutls.c
@@ -39,6 +39,8 @@ struct _GTlsOperationsThreadGnutls {
   gnutls_session_t         session;
 };
 
+static gnutls_priority_t priority;
+
 G_DEFINE_TYPE (GTlsOperationsThreadGnutls, g_tls_operations_thread_gnutls, G_TYPE_TLS_OPERATIONS_THREAD_BASE)
 
 static GTlsConnectionBaseStatus
@@ -200,6 +202,90 @@ end_gnutls_io (GTlsOperationsThreadGnutls  *self,
     status = end_gnutls_io (self, direction, ret, err, errmsg);       \
   } while (status == G_TLS_CONNECTION_BASE_TRY_AGAIN);
 
+static void
+initialize_gnutls_priority (void)
+{
+  const gchar *priority_override;
+  const gchar *error_pos = NULL;
+  int ret;
+
+  g_assert (!priority);
+
+  priority_override = g_getenv ("G_TLS_GNUTLS_PRIORITY");
+  if (priority_override)
+    {
+      ret = gnutls_priority_init2 (&priority, priority_override, &error_pos, 0);
+      if (ret != GNUTLS_E_SUCCESS)
+        g_warning ("Failed to set GnuTLS session priority with beginning at %s: %s", error_pos, 
gnutls_strerror (ret));
+      return;
+    }
+
+  ret = gnutls_priority_init2 (&priority, "%COMPAT:-VERS-TLS1.1:-VERS-TLS1.0", &error_pos, 
GNUTLS_PRIORITY_INIT_DEF_APPEND);
+  if (ret != GNUTLS_E_SUCCESS)
+    g_warning ("Failed to set GnuTLS session priority with error beginning at %s: %s", error_pos, 
gnutls_strerror (ret));
+}
+
+static void
+set_handshake_priority (GTlsOperationsThreadGnutls *self)
+{
+  int ret;
+
+  g_assert (priority);
+
+  ret = gnutls_priority_set (self->session, priority);
+  if (ret != GNUTLS_E_SUCCESS)
+    g_warning ("Failed to set GnuTLS session priority: %s", gnutls_strerror (ret));
+}
+
+static GTlsConnectionBaseStatus
+g_tls_operations_thread_gnutls_handshake (GTlsOperationsThreadBase *base,
+                                          gint64                    timeout,
+                                          GCancellable             *cancellable,
+                                          GError                  **error)
+{
+  GTlsOperationsThreadGnutls *self = G_TLS_OPERATIONS_THREAD_GNUTLS (base);
+  GTlsConnectionBase *tls;
+  GTlsConnectionBaseStatus status;
+  int ret;
+
+  tls = g_tls_operations_thread_base_get_connection (base);
+
+  if (!g_tls_connection_base_ever_handshaked (tls))
+    set_handshake_priority (self);
+
+  if (timeout > 0)
+    {
+      unsigned int timeout_ms;
+
+      /* Convert from microseconds to milliseconds, but ensure the timeout
+       * remains positive. */
+      timeout_ms = (timeout + 999) / 1000;
+
+      gnutls_handshake_set_timeout (self->session, timeout_ms);
+      gnutls_dtls_set_timeouts (self->session, 1000 /* default */, timeout_ms);
+    }
+
+  BEGIN_GNUTLS_IO (self, G_IO_IN | G_IO_OUT, cancellable);
+  ret = gnutls_handshake (self->session);
+  if (ret == GNUTLS_E_GOT_APPLICATION_DATA)
+    {
+      guint8 buf[1024];
+
+      /* Got app data while waiting for rehandshake; buffer it and try again */
+      ret = gnutls_record_recv (self->session, buf, sizeof (buf));
+      if (ret > -1)
+        {
+          /* FIXME: no longer belongs in GTlsConnectionBase? */
+          g_tls_connection_base_handshake_thread_buffer_application_data (tls, buf, ret);
+          ret = GNUTLS_E_AGAIN;
+        }
+    }
+  END_GNUTLS_IO (self, G_IO_IN | G_IO_OUT, ret, status,
+                 _("Error performing TLS handshake"), error);
+
+  return status;
+}
+
 static GTlsConnectionBaseStatus
 g_tls_operations_thread_gnutls_read (GTlsOperationsThreadBase  *base,
                                      void                      *buffer,
@@ -404,11 +490,14 @@ g_tls_operations_thread_gnutls_class_init (GTlsOperationsThreadGnutlsClass *klas
 
   gobject_class->constructed   = g_tls_operations_thread_gnutls_constructed;
 
+  base_class->handshake_fn     = g_tls_operations_thread_gnutls_handshake;
   base_class->read_fn          = g_tls_operations_thread_gnutls_read;
   base_class->read_message_fn  = g_tls_operations_thread_gnutls_read_message;
   base_class->write_fn         = g_tls_operations_thread_gnutls_write;
   base_class->write_message_fn = g_tls_operations_thread_gnutls_write_message;
   base_class->close_fn         = g_tls_operations_thread_gnutls_close;
+
+  initialize_gnutls_priority ();
 }
 
 GTlsOperationsThreadBase *


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]