Blame SOURCES/0006-Dispatch-style-protocol-switching-for-transport.patch

4be148
From 606e2ccc0a2546a23761f910482a55c5bf0f98ac Mon Sep 17 00:00:00 2001
4be148
From: "Robbie Harwood (frozencemetery)" <rharwood@club.cc.cmu.edu>
4be148
Date: Fri, 16 Aug 2013 14:48:55 -0400
4be148
Subject: [PATCH 06/13] Dispatch-style protocol switching for transport
4be148
4be148
Switch to using per-transport-type functions when a socket that we're
4be148
using to communicate with a server becomes readable or writable, and add
4be148
them as pointers to the connection state.  The functions are passed the
4be148
name of the realm of the server being contacted, as we expect to need
4be148
this in the near future.
4be148
4be148
[nalin@redhat.com: replace macros with typedefs]
4be148
[nalin@redhat.com: compare transports with TCP_OR_UDP rather than with 0]
4be148
4be148
ticket: 7929
4be148
---
4be148
 src/lib/krb5/os/changepw.c   |   6 +-
4be148
 src/lib/krb5/os/os-proto.h   |   1 +
4be148
 src/lib/krb5/os/sendto_kdc.c | 297 ++++++++++++++++++++++++-------------------
4be148
 3 files changed, 171 insertions(+), 133 deletions(-)
4be148
4be148
diff --git a/src/lib/krb5/os/changepw.c b/src/lib/krb5/os/changepw.c
4be148
index a1c9885..0ee427d 100644
4be148
--- a/src/lib/krb5/os/changepw.c
4be148
+++ b/src/lib/krb5/os/changepw.c
4be148
@@ -261,9 +261,9 @@ change_set_password(krb5_context context,
4be148
         callback_info.pfn_cleanup = kpasswd_sendto_msg_cleanup;
4be148
         krb5_free_data_contents(callback_ctx.context, &chpw_rep);
4be148
 
4be148
-        code = k5_sendto(callback_ctx.context, NULL, &sl, strategy,
4be148
-                         &callback_info, &chpw_rep, ss2sa(&remote_addr),
4be148
-                         &addrlen, NULL, NULL, NULL);
4be148
+        code = k5_sendto(callback_ctx.context, NULL, &creds->server->realm,
4be148
+                         &sl, strategy, &callback_info, &chpw_rep,
4be148
+                         ss2sa(&remote_addr), &addrlen, NULL, NULL, NULL);
4be148
         if (code) {
4be148
             /*
4be148
              * Here we may want to switch to TCP on some errors.
4be148
diff --git a/src/lib/krb5/os/os-proto.h b/src/lib/krb5/os/os-proto.h
4be148
index f23dda5..e60ccd0 100644
4be148
--- a/src/lib/krb5/os/os-proto.h
4be148
+++ b/src/lib/krb5/os/os-proto.h
4be148
@@ -115,6 +115,7 @@ int _krb5_use_dns_kdc (krb5_context);
4be148
 int _krb5_conf_boolean (const char *);
4be148
 
4be148
 krb5_error_code k5_sendto(krb5_context context, const krb5_data *message,
4be148
+                          const krb5_data *realm,
4be148
                           const struct serverlist *addrs,
4be148
                           k5_transport_strategy strategy,
4be148
                           struct sendto_callback_info *callback_info,
4be148
diff --git a/src/lib/krb5/os/sendto_kdc.c b/src/lib/krb5/os/sendto_kdc.c
4be148
index c6aae8e..28f1c4d 100644
4be148
--- a/src/lib/krb5/os/sendto_kdc.c
4be148
+++ b/src/lib/krb5/os/sendto_kdc.c
4be148
@@ -96,11 +96,18 @@ struct outgoing_message {
4be148
     unsigned char msg_len_buf[4];
4be148
 };
4be148
 
4be148
+struct conn_state;
4be148
+typedef krb5_boolean fd_handler_fn(krb5_context context,
4be148
+                                   const krb5_data *realm,
4be148
+                                   struct conn_state *conn,
4be148
+                                   struct select_state *selstate);
4be148
+
4be148
 struct conn_state {
4be148
     SOCKET fd;
4be148
     enum conn_states state;
4be148
-    int (*service)(krb5_context context, struct conn_state *,
4be148
-                   struct select_state *, int);
4be148
+    fd_handler_fn *service_connect;
4be148
+    fd_handler_fn *service_write;
4be148
+    fd_handler_fn *service_read;
4be148
     struct remote_address addr;
4be148
     struct incoming_message in;
4be148
     struct outgoing_message out;
4be148
@@ -409,9 +416,9 @@ krb5_sendto_kdc(krb5_context context, const krb5_data *message,
4be148
         return retval;
4be148
 
4be148
     err = 0;
4be148
-    retval = k5_sendto(context, message, &servers, strategy, NULL, reply,
4be148
-                       NULL, NULL, &server_used, check_for_svc_unavailable,
4be148
-                       &err;;
4be148
+    retval = k5_sendto(context, message, realm, &servers, strategy, NULL,
4be148
+                       reply, NULL, NULL, &server_used,
4be148
+                       check_for_svc_unavailable, &err;;
4be148
     if (retval == KRB5_KDC_UNREACH) {
4be148
         if (err == KDC_ERR_SVC_UNAVAILABLE) {
4be148
             retval = KRB5KDC_ERR_SVC_UNAVAILABLE;
4be148
@@ -457,10 +464,10 @@ cleanup:
4be148
  *   connections already in progress
4be148
  */
4be148
 
4be148
-static int service_tcp_fd(krb5_context context, struct conn_state *conn,
4be148
-                          struct select_state *selstate, int ssflags);
4be148
-static int service_udp_fd(krb5_context context, struct conn_state *conn,
4be148
-                          struct select_state *selstate, int ssflags);
4be148
+static fd_handler_fn service_tcp_connect;
4be148
+static fd_handler_fn service_tcp_write;
4be148
+static fd_handler_fn service_tcp_read;
4be148
+static fd_handler_fn service_udp_read;
4be148
 
4be148
 /* Set up the actual message we will send across the underlying transport to
4be148
  * communicate the payload message, using one or both of state->out.sgbuf. */
4be148
@@ -505,9 +512,13 @@ add_connection(struct conn_state **conns, k5_transport transport,
4be148
     state->server_index = server_index;
4be148
     SG_SET(&state->out.sgbuf[1], NULL, 0);
4be148
     if (transport == TCP) {
4be148
-        state->service = service_tcp_fd;
4be148
+        state->service_connect = service_tcp_connect;
4be148
+        state->service_write = service_tcp_write;
4be148
+        state->service_read = service_tcp_read;
4be148
     } else {
4be148
-        state->service = service_udp_fd;
4be148
+        state->service_connect = NULL;
4be148
+        state->service_write = NULL;
4be148
+        state->service_read = service_udp_read;
4be148
 
4be148
         if (*udpbufp == NULL) {
4be148
             *udpbufp = malloc(MAX_DGRAM_SIZE);
4be148
@@ -788,9 +799,13 @@ maybe_send(krb5_context context, struct conn_state *conn,
4be148
 }
4be148
 
4be148
 static void
4be148
-kill_conn(struct conn_state *conn, struct select_state *selstate)
4be148
+kill_conn(krb5_context context, struct conn_state *conn,
4be148
+          struct select_state *selstate)
4be148
 {
4be148
+    if (socktype_for_transport(conn->addr.transport) == SOCK_STREAM)
4be148
+        TRACE_SENDTO_KDC_TCP_DISCONNECT(context, &conn->addr);
4be148
     cm_remove_fd(selstate, conn->fd);
4be148
+
4be148
     closesocket(conn->fd);
4be148
     conn->fd = INVALID_SOCKET;
4be148
     conn->state = FAILED;
4be148
@@ -814,136 +829,157 @@ get_so_error(int fd)
4be148
     return sockerr;
4be148
 }
4be148
 
4be148
-/* Process events on a TCP socket.  Return 1 if we get a complete reply. */
4be148
-static int
4be148
-service_tcp_fd(krb5_context context, struct conn_state *conn,
4be148
-               struct select_state *selstate, int ssflags)
4be148
+/* Perform next step in sending.  Return true on usable data. */
4be148
+static krb5_boolean
4be148
+service_dispatch(krb5_context context, const krb5_data *realm,
4be148
+                 struct conn_state *conn, struct select_state *selstate,
4be148
+                 int ssflags)
4be148
 {
4be148
-    int e = 0;
4be148
-    ssize_t nwritten, nread;
4be148
-    SOCKET_WRITEV_TEMP tmp;
4be148
-    struct incoming_message *in = &conn->in;
4be148
-    struct outgoing_message *out = &conn->out;
4be148
-
4be148
     /* Check for a socket exception. */
4be148
-    if (ssflags & SSF_EXCEPTION)
4be148
-        goto kill_conn;
4be148
+    if (ssflags & SSF_EXCEPTION) {
4be148
+        kill_conn(context, conn, selstate);
4be148
+        return FALSE;
4be148
+    }
4be148
 
4be148
     switch (conn->state) {
4be148
     case CONNECTING:
4be148
-        /* Check whether the connection succeeded. */
4be148
-        e = get_so_error(conn->fd);
4be148
-        if (e) {
4be148
-            TRACE_SENDTO_KDC_TCP_ERROR_CONNECT(context, &conn->addr, e);
4be148
-            goto kill_conn;
4be148
-        }
4be148
-        conn->state = WRITING;
4be148
+        assert(conn->service_connect != NULL);
4be148
+        return conn->service_connect(context, realm, conn, selstate);
4be148
+    case WRITING:
4be148
+        assert(conn->service_write != NULL);
4be148
+        return conn->service_write(context, realm, conn, selstate);
4be148
+    case READING:
4be148
+        assert(conn->service_read != NULL);
4be148
+        return conn->service_read(context, realm, conn, selstate);
4be148
+    default:
4be148
+        abort();
4be148
+    }
4be148
+}
4be148
 
4be148
-        /* Record this connection's timeout for service_fds. */
4be148
-        if (get_curtime_ms(&conn->endtime) == 0)
4be148
-            conn->endtime += 10000;
4be148
+/* Initialize TCP transport. */
4be148
+static krb5_boolean
4be148
+service_tcp_connect(krb5_context context, const krb5_data *realm,
4be148
+                    struct conn_state *conn, struct select_state *selstate)
4be148
+{
4be148
+    /* Check whether the connection succeeded. */
4be148
+    int e = get_so_error(conn->fd);
4be148
 
4be148
-        /* Fall through. */
4be148
-    case WRITING:
4be148
-        TRACE_SENDTO_KDC_TCP_SEND(context, &conn->addr);
4be148
-        nwritten = SOCKET_WRITEV(conn->fd, out->sgp, out->sg_count, tmp);
4be148
-        if (nwritten < 0) {
4be148
-            TRACE_SENDTO_KDC_TCP_ERROR_SEND(context, &conn->addr,
4be148
-                                            SOCKET_ERRNO);
4be148
-            goto kill_conn;
4be148
+    if (e) {
4be148
+        TRACE_SENDTO_KDC_TCP_ERROR_CONNECT(context, &conn->addr, e);
4be148
+        kill_conn(context, conn, selstate);
4be148
+        return FALSE;
4be148
+    }
4be148
+
4be148
+    conn->state = WRITING;
4be148
+
4be148
+    /* Record this connection's timeout for service_fds. */
4be148
+    if (get_curtime_ms(&conn->endtime) == 0)
4be148
+        conn->endtime += 10000;
4be148
+
4be148
+    return service_tcp_write(context, realm, conn, selstate);
4be148
+}
4be148
+
4be148
+/* Sets conn->state to READING when done. */
4be148
+static krb5_boolean
4be148
+service_tcp_write(krb5_context context, const krb5_data *realm,
4be148
+                  struct conn_state *conn, struct select_state *selstate)
4be148
+{
4be148
+    ssize_t nwritten;
4be148
+    SOCKET_WRITEV_TEMP tmp;
4be148
+
4be148
+    TRACE_SENDTO_KDC_TCP_SEND(context, &conn->addr);
4be148
+    nwritten = SOCKET_WRITEV(conn->fd, conn->out.sgp, conn->out.sg_count, tmp);
4be148
+    if (nwritten < 0) {
4be148
+        TRACE_SENDTO_KDC_TCP_ERROR_SEND(context, &conn->addr, SOCKET_ERRNO);
4be148
+        kill_conn(context, conn, selstate);
4be148
+        return FALSE;
4be148
+    }
4be148
+    while (nwritten) {
4be148
+        sg_buf *sgp = conn->out.sgp;
4be148
+        if ((size_t)nwritten < SG_LEN(sgp)) {
4be148
+            SG_ADVANCE(sgp, (size_t)nwritten);
4be148
+            nwritten = 0;
4be148
+        } else {
4be148
+            nwritten -= SG_LEN(sgp);
4be148
+            conn->out.sgp++;
4be148
+            conn->out.sg_count--;
4be148
         }
4be148
-        while (nwritten) {
4be148
-            sg_buf *sgp = out->sgp;
4be148
-            if ((size_t) nwritten < SG_LEN(sgp)) {
4be148
-                SG_ADVANCE(sgp, (size_t) nwritten);
4be148
-                nwritten = 0;
4be148
-            } else {
4be148
-                nwritten -= SG_LEN(sgp);
4be148
-                out->sgp++;
4be148
-                out->sg_count--;
4be148
-            }
4be148
+    }
4be148
+    if (conn->out.sg_count == 0) {
4be148
+        /* Done writing, switch to reading. */
4be148
+        cm_read(selstate, conn->fd);
4be148
+        conn->state = READING;
4be148
+    }
4be148
+    return FALSE;
4be148
+}
4be148
+
4be148
+/* Return true on usable data. */
4be148
+static krb5_boolean
4be148
+service_tcp_read(krb5_context context, const krb5_data *realm,
4be148
+                 struct conn_state *conn, struct select_state *selstate)
4be148
+{
4be148
+    ssize_t nread;
4be148
+    int e = 0;
4be148
+    struct incoming_message *in = &conn->in;
4be148
+
4be148
+    if (in->bufsizebytes_read == 4) {
4be148
+        /* Reading data.  */
4be148
+        nread = SOCKET_READ(conn->fd, &in->buf[in->pos], in->n_left);
4be148
+        if (nread <= 0) {
4be148
+            e = nread ? SOCKET_ERRNO : ECONNRESET;
4be148
+            TRACE_SENDTO_KDC_TCP_ERROR_RECV(context, &conn->addr, e);
4be148
+            kill_conn(context, conn, selstate);
4be148
+            return FALSE;
4be148
         }
4be148
-        if (out->sg_count == 0) {
4be148
-            /* Done writing, switch to reading. */
4be148
-            cm_read(selstate, conn->fd);
4be148
-            conn->state = READING;
4be148
-            in->bufsizebytes_read = 0;
4be148
-            in->bufsize = 0;
4be148
-            in->pos = 0;
4be148
-            in->buf = NULL;
4be148
-            in->n_left = 0;
4be148
+        in->n_left -= nread;
4be148
+        in->pos += nread;
4be148
+        if (in->n_left <= 0)
4be148
+            return TRUE;
4be148
+    } else {
4be148
+        /* Reading length.  */
4be148
+        nread = SOCKET_READ(conn->fd, in->bufsizebytes + in->bufsizebytes_read,
4be148
+                            4 - in->bufsizebytes_read);
4be148
+        if (nread <= 0) {
4be148
+            e = nread ? SOCKET_ERRNO : ECONNRESET;
4be148
+            TRACE_SENDTO_KDC_TCP_ERROR_RECV_LEN(context, &conn->addr, e);
4be148
+            kill_conn(context, conn, selstate);
4be148
+            return FALSE;
4be148
         }
4be148
-        return 0;
4be148
-
4be148
-    case READING:
4be148
+        in->bufsizebytes_read += nread;
4be148
         if (in->bufsizebytes_read == 4) {
4be148
-            /* Reading data.  */
4be148
-            nread = SOCKET_READ(conn->fd, &in->buf[in->pos], in->n_left);
4be148
-            if (nread <= 0) {
4be148
-                e = nread ? SOCKET_ERRNO : ECONNRESET;
4be148
-                TRACE_SENDTO_KDC_TCP_ERROR_RECV(context, &conn->addr, e);
4be148
-                goto kill_conn;
4be148
-            }
4be148
-            in->n_left -= nread;
4be148
-            in->pos += nread;
4be148
-            if (in->n_left <= 0)
4be148
-                return 1;
4be148
-        } else {
4be148
-            /* Reading length.  */
4be148
-            nread = SOCKET_READ(conn->fd,
4be148
-                                in->bufsizebytes + in->bufsizebytes_read,
4be148
-                                4 - in->bufsizebytes_read);
4be148
-            if (nread <= 0) {
4be148
-                e = nread ? SOCKET_ERRNO : ECONNRESET;
4be148
-                TRACE_SENDTO_KDC_TCP_ERROR_RECV_LEN(context, &conn->addr, e);
4be148
-                goto kill_conn;
4be148
+            unsigned long len = load_32_be(in->bufsizebytes);
4be148
+            /* Arbitrary 1M cap.  */
4be148
+            if (len > 1 * 1024 * 1024) {
4be148
+                kill_conn(context, conn, selstate);
4be148
+                return FALSE;
4be148
             }
4be148
-            in->bufsizebytes_read += nread;
4be148
-            if (in->bufsizebytes_read == 4) {
4be148
-                unsigned long len = load_32_be(in->bufsizebytes);
4be148
-                /* Arbitrary 1M cap.  */
4be148
-                if (len > 1 * 1024 * 1024)
4be148
-                    goto kill_conn;
4be148
-                in->bufsize = in->n_left = len;
4be148
-                in->pos = 0;
4be148
-                in->buf = malloc(len);
4be148
-                if (in->buf == NULL)
4be148
-                    goto kill_conn;
4be148
+            in->bufsize = in->n_left = len;
4be148
+            in->pos = 0;
4be148
+            in->buf = malloc(len);
4be148
+            if (in->buf == NULL) {
4be148
+                kill_conn(context, conn, selstate);
4be148
+                return FALSE;
4be148
             }
4be148
         }
4be148
-        break;
4be148
-
4be148
-    default:
4be148
-        abort();
4be148
     }
4be148
-    return 0;
4be148
-
4be148
-kill_conn:
4be148
-    TRACE_SENDTO_KDC_TCP_DISCONNECT(context, &conn->addr);
4be148
-    kill_conn(conn, selstate);
4be148
-    return 0;
4be148
+    return FALSE;
4be148
 }
4be148
 
4be148
-/* Process events on a UDP socket.  Return 1 if we get a reply. */
4be148
-static int
4be148
-service_udp_fd(krb5_context context, struct conn_state *conn,
4be148
-               struct select_state *selstate, int ssflags)
4be148
+/* Process events on a UDP socket.  Return true if we get a reply. */
4be148
+static krb5_boolean
4be148
+service_udp_read(krb5_context context, const krb5_data *realm,
4be148
+                 struct conn_state *conn, struct select_state *selstate)
4be148
 {
4be148
     int nread;
4be148
 
4be148
-    if (!(ssflags & (SSF_READ|SSF_EXCEPTION)))
4be148
-        abort();
4be148
-    if (conn->state != READING)
4be148
-        abort();
4be148
-
4be148
     nread = recv(conn->fd, conn->in.buf, conn->in.bufsize, 0);
4be148
     if (nread < 0) {
4be148
         TRACE_SENDTO_KDC_UDP_ERROR_RECV(context, &conn->addr, SOCKET_ERRNO);
4be148
-        kill_conn(conn, selstate);
4be148
-        return 0;
4be148
+        kill_conn(context, conn, selstate);
4be148
+        return FALSE;
4be148
     }
4be148
     conn->in.pos = nread;
4be148
-    return 1;
4be148
+    return TRUE;
4be148
 }
4be148
 
4be148
 /* Return the maximum of endtime and the endtime fields of all currently active
4be148
@@ -965,7 +1001,7 @@ get_endtime(time_ms endtime, struct conn_state *conns)
4be148
 static krb5_boolean
4be148
 service_fds(krb5_context context, struct select_state *selstate,
4be148
             time_ms interval, struct conn_state *conns,
4be148
-            struct select_state *seltemp,
4be148
+            struct select_state *seltemp, const krb5_data *realm,
4be148
             int (*msg_handler)(krb5_context, const krb5_data *, void *),
4be148
             void *msg_handler_data, struct conn_state **winner_out)
4be148
 {
4be148
@@ -977,7 +1013,7 @@ service_fds(krb5_context context, struct select_state *selstate,
4be148
 
4be148
     e = get_curtime_ms(&endtime);
4be148
     if (e)
4be148
-        return 1;
4be148
+        return TRUE;
4be148
     endtime += interval;
4be148
 
4be148
     e = 0;
4be148
@@ -991,7 +1027,7 @@ service_fds(krb5_context context, struct select_state *selstate,
4be148
 
4be148
         if (selret == 0)
4be148
             /* Timeout, return to caller.  */
4be148
-            return 0;
4be148
+            return FALSE;
4be148
 
4be148
         /* Got something on a socket, process it.  */
4be148
         for (state = conns; state != NULL; state = state->next) {
4be148
@@ -1003,7 +1039,7 @@ service_fds(krb5_context context, struct select_state *selstate,
4be148
             if (!ssflags)
4be148
                 continue;
4be148
 
4be148
-            if (state->service(context, state, selstate, ssflags)) {
4be148
+            if (service_dispatch(context, realm, state, selstate, ssflags)) {
4be148
                 int stop = 1;
4be148
 
4be148
                 if (msg_handler != NULL) {
4be148
@@ -1014,14 +1050,14 @@ service_fds(krb5_context context, struct select_state *selstate,
4be148
 
4be148
                 if (stop) {
4be148
                     *winner_out = state;
4be148
-                    return 1;
4be148
+                    return TRUE;
4be148
                 }
4be148
             }
4be148
         }
4be148
     }
4be148
     if (e != 0)
4be148
-        return 1;
4be148
-    return 0;
4be148
+        return TRUE;
4be148
+    return FALSE;
4be148
 }
4be148
 
4be148
 /*
4be148
@@ -1052,7 +1088,8 @@ service_fds(krb5_context context, struct select_state *selstate,
4be148
 
4be148
 krb5_error_code
4be148
 k5_sendto(krb5_context context, const krb5_data *message,
4be148
-          const struct serverlist *servers, k5_transport_strategy strategy,
4be148
+          const krb5_data *realm, const struct serverlist *servers,
4be148
+          k5_transport_strategy strategy,
4be148
           struct sendto_callback_info* callback_info, krb5_data *reply,
4be148
           struct sockaddr *remoteaddr, socklen_t *remoteaddrlen,
4be148
           int *server_used,
4be148
@@ -1098,7 +1135,7 @@ k5_sendto(krb5_context context, const krb5_data *message,
4be148
             if (maybe_send(context, state, message, sel_state, callback_info))
4be148
                 continue;
4be148
             done = service_fds(context, sel_state, 1000, conns, seltemp,
4be148
-                               msg_handler, msg_handler_data, &winner);
4be148
+                               realm, msg_handler, msg_handler_data, &winner);
4be148
         }
4be148
     }
4be148
 
4be148
@@ -1110,13 +1147,13 @@ k5_sendto(krb5_context context, const krb5_data *message,
4be148
         if (maybe_send(context, state, message, sel_state, callback_info))
4be148
             continue;
4be148
         done = service_fds(context, sel_state, 1000, conns, seltemp,
4be148
-                           msg_handler, msg_handler_data, &winner);
4be148
+                           realm, msg_handler, msg_handler_data, &winner);
4be148
     }
4be148
 
4be148
     /* Wait for two seconds at the end of the first pass. */
4be148
     if (!done) {
4be148
         done = service_fds(context, sel_state, 2000, conns, seltemp,
4be148
-                           msg_handler, msg_handler_data, &winner);
4be148
+                           realm, msg_handler, msg_handler_data, &winner);
4be148
     }
4be148
 
4be148
     /* Make remaining passes over all of the connections. */
4be148
@@ -1126,14 +1163,14 @@ k5_sendto(krb5_context context, const krb5_data *message,
4be148
             if (maybe_send(context, state, message, sel_state, callback_info))
4be148
                 continue;
4be148
             done = service_fds(context, sel_state, 1000, conns, seltemp,
4be148
-                               msg_handler, msg_handler_data, &winner);
4be148
+                               realm, msg_handler, msg_handler_data, &winner);
4be148
             if (sel_state->nfds == 0)
4be148
                 break;
4be148
         }
4be148
         /* Wait for the delay backoff at the end of this pass. */
4be148
         if (!done) {
4be148
             done = service_fds(context, sel_state, delay, conns, seltemp,
4be148
-                               msg_handler, msg_handler_data, &winner);
4be148
+                               realm, msg_handler, msg_handler_data, &winner);
4be148
         }
4be148
         if (sel_state->nfds == 0)
4be148
             break;
4be148
-- 
4be148
2.1.0
4be148