From b9b43664ce455ecad8db1531a6e5e68f43e6fe45 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 13 May 2020 23:42:09 +0200
Subject: [PATCH] Use system hostname verifier

---
 .../quasseldroid/service/QuasselService.kt    |  2 +-
 .../ssl/QuasselHostnameVerifier.kt            | 21 ++-----
 .../ssl/custom/QuasselHostnameManager.kt      | 14 +++--
 .../quasseldroid/ui/chat/ChatActivity.kt      |  6 +-
 .../libquassel/connection/CoreConnection.kt   |  5 +-
 .../connection/QuasselSecurityException.kt    | 11 ++--
 .../de/kuschku/libquassel/session/Session.kt  |  5 +-
 .../session/manager/ConnectionInfo.kt         |  2 +-
 .../ssl/BrowserCompatibleHostnameVerifier.kt  | 63 -------------------
 .../ssl/TrustAllHostnameVerifier.kt           | 28 ---------
 .../X509CertificateHelper.kt}                 | 11 ++--
 .../de/kuschku/libquassel/ssl/X509Helper.kt   |  6 +-
 .../libquassel/util/nio/WrappedChannel.kt     | 26 ++++----
 13 files changed, 54 insertions(+), 146 deletions(-)
 delete mode 100644 lib/src/main/java/de/kuschku/libquassel/ssl/BrowserCompatibleHostnameVerifier.kt
 delete mode 100644 lib/src/main/java/de/kuschku/libquassel/ssl/TrustAllHostnameVerifier.kt
 rename lib/src/main/java/de/kuschku/libquassel/{connection/HostnameVerifier.kt => ssl/X509CertificateHelper.kt} (74%)

diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt
index 9ae229812..0f7685708 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselService.kt
@@ -28,7 +28,6 @@ import androidx.core.app.RemoteInput
 import androidx.lifecycle.Observer
 import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
 import de.kuschku.libquassel.connection.ConnectionState
-import de.kuschku.libquassel.connection.HostnameVerifier
 import de.kuschku.libquassel.connection.SocketAddress
 import de.kuschku.libquassel.protocol.*
 import de.kuschku.libquassel.quassel.BufferInfo
@@ -68,6 +67,7 @@ import de.kuschku.quasseldroid.util.ui.LocaleHelper
 import io.reactivex.subjects.BehaviorSubject
 import org.threeten.bp.Instant
 import javax.inject.Inject
+import javax.net.ssl.HostnameVerifier
 import javax.net.ssl.X509TrustManager
 
 class QuasselService : DaggerLifecycleService(),
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt
index 0bc75e3c1..85b30d7a4 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/QuasselHostnameVerifier.kt
@@ -19,25 +19,16 @@
 
 package de.kuschku.quasseldroid.ssl
 
-import de.kuschku.libquassel.connection.HostnameVerifier
-import de.kuschku.libquassel.connection.QuasselSecurityException
-import de.kuschku.libquassel.connection.SocketAddress
-import de.kuschku.libquassel.ssl.BrowserCompatibleHostnameVerifier
 import de.kuschku.quasseldroid.ssl.custom.QuasselHostnameManager
-import java.security.cert.X509Certificate
-import javax.net.ssl.SSLException
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.HttpsURLConnection
+import javax.net.ssl.SSLSession
 
 class QuasselHostnameVerifier(
   private val hostnameManager: QuasselHostnameManager,
-  private val hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier()
+  private val hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
 ) : HostnameVerifier {
-  override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) {
-    try {
-      if (!hostnameManager.isValid(address, chain)) {
-        hostnameVerifier.checkValid(address, chain)
-      }
-    } catch (e: SSLException) {
-      throw QuasselSecurityException.Hostname(chain, address, e)
-    }
+  override fun verify(hostname: String?, session: SSLSession?): Boolean {
+    return hostnameManager.verify(hostname, session) || hostnameVerifier.verify(hostname, session)
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt b/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt
index 6320434c6..e0b5f2d3f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ssl/custom/QuasselHostnameManager.kt
@@ -19,17 +19,19 @@
 
 package de.kuschku.quasseldroid.ssl.custom
 
-import de.kuschku.libquassel.connection.SocketAddress
+import de.kuschku.libquassel.ssl.toJavaCertificate
 import de.kuschku.quasseldroid.persistence.dao.SslHostnameWhitelistDao
 import de.kuschku.quasseldroid.util.helper.sha1Fingerprint
-import java.security.cert.X509Certificate
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSession
 
 class QuasselHostnameManager(
   private val hostnameWhitelist: SslHostnameWhitelistDao
-) {
-  fun isValid(address: SocketAddress, chain: Array<out X509Certificate>): Boolean {
-    val leafCertificate = chain.firstOrNull() ?: return false
-    val whitelistEntry = hostnameWhitelist.find(leafCertificate.sha1Fingerprint, address.host)
+) : HostnameVerifier {
+  override fun verify(hostname: String?, session: SSLSession?): Boolean {
+    val chain = session?.peerCertificateChain?.toJavaCertificate()
+    val leafCertificate = chain?.firstOrNull() ?: return false
+    val whitelistEntry = hostnameWhitelist.find(leafCertificate.sha1Fingerprint, hostname ?: "")
     return whitelistEntry != null
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
index f36613752..2e117103f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt
@@ -526,7 +526,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
                   .show()
               } else {
                 val leafCertificate = it.certificateChain?.firstOrNull()
-                if (leafCertificate == null) {
+                if (leafCertificate == null || it is QuasselSecurityException.NoCertificate) {
                   // No certificate exists in the chain
                   MaterialDialog.Builder(this)
                     .title(R.string.label_error_certificate)
@@ -584,7 +584,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
                         .show()
                     }
                     // Certificate is in any other way invalid
-                    it is QuasselSecurityException.Certificate -> {
+                    it is QuasselSecurityException.Certificate   -> {
                       MaterialDialog.Builder(this)
                         .title(R.string.label_error_certificate)
                         .content(
@@ -630,7 +630,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
                         .show()
                     }
                     // Certificate not valid for this hostname
-                    it is QuasselSecurityException.Hostname    -> {
+                    it is QuasselSecurityException.WrongHostname -> {
                       MaterialDialog.Builder(this)
                         .title(R.string.label_error_certificate)
                         .content(
diff --git a/lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt b/lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt
index f941732ef..26c84e6ed 100644
--- a/lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/connection/CoreConnection.kt
@@ -29,7 +29,6 @@ import de.kuschku.libquassel.protocol.primitive.serializer.VariantListSerializer
 import de.kuschku.libquassel.quassel.ProtocolFeature
 import de.kuschku.libquassel.quassel.QuasselFeatures
 import de.kuschku.libquassel.session.ProtocolHandler
-import de.kuschku.libquassel.ssl.BrowserCompatibleHostnameVerifier
 import de.kuschku.libquassel.ssl.TrustManagers
 import de.kuschku.libquassel.util.Optional
 import de.kuschku.libquassel.util.compatibility.CompatibilityUtils
@@ -50,6 +49,8 @@ import java.lang.Thread.UncaughtExceptionHandler
 import java.net.Socket
 import java.net.SocketException
 import java.nio.ByteBuffer
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.HttpsURLConnection
 import javax.net.ssl.SSLSession
 import javax.net.ssl.X509TrustManager
 
@@ -60,7 +61,7 @@ class CoreConnection(
   private val features: Features = Features(clientData.clientFeatures, QuasselFeatures.empty()),
   private val handlerService: HandlerService = JavaHandlerService(),
   private val trustManager: X509TrustManager = TrustManagers.default(),
-  private val hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier()
+  private val hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
 ) : Thread(), Closeable {
   companion object {
     private const val TAG = "CoreConnection"
diff --git a/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt b/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt
index a08e28042..79aaeb8c1 100644
--- a/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/connection/QuasselSecurityException.kt
@@ -31,11 +31,14 @@ sealed class QuasselSecurityException(
     cause: Exception
   ) : QuasselSecurityException(certificateChain, cause)
 
-  class Hostname(
+  class WrongHostname(
     certificateChain: Array<out X509Certificate>?,
-    val address: SocketAddress,
-    cause: Exception
-  ) : QuasselSecurityException(certificateChain, cause)
+    val address: SocketAddress
+  ) : QuasselSecurityException(certificateChain, null)
+
+  class NoCertificate(
+    val address: SocketAddress
+  ) : QuasselSecurityException(emptyArray(), null)
 
   object NoSsl : QuasselSecurityException(emptyArray(), null)
 }
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
index d1549dd04..eb0ec7ada 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/Session.kt
@@ -26,7 +26,6 @@ import de.kuschku.libquassel.protocol.message.SignalProxyMessage
 import de.kuschku.libquassel.quassel.ExtendedFeature
 import de.kuschku.libquassel.quassel.QuasselFeatures
 import de.kuschku.libquassel.quassel.syncables.*
-import de.kuschku.libquassel.ssl.BrowserCompatibleHostnameVerifier
 import de.kuschku.libquassel.ssl.TrustManagers
 import de.kuschku.libquassel.util.compatibility.HandlerService
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
@@ -37,6 +36,8 @@ import io.reactivex.Observable
 import io.reactivex.subjects.BehaviorSubject
 import io.reactivex.subjects.PublishSubject
 import org.threeten.bp.Instant
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.HttpsURLConnection
 import javax.net.ssl.X509TrustManager
 
 class Session(
@@ -44,7 +45,7 @@ class Session(
   private var userData: Pair<String, String>,
   requireSsl: Boolean = false,
   trustManager: X509TrustManager = TrustManagers.default(),
-  hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier(),
+  hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(),
   clientData: ClientData = ClientData.DEFAULT,
   private val handlerService: HandlerService = JavaHandlerService(),
   heartBeatFactory: () -> HeartBeatRunner = ::JavaHeartBeatRunner,
diff --git a/lib/src/main/java/de/kuschku/libquassel/session/manager/ConnectionInfo.kt b/lib/src/main/java/de/kuschku/libquassel/session/manager/ConnectionInfo.kt
index f93e7de18..da961d408 100644
--- a/lib/src/main/java/de/kuschku/libquassel/session/manager/ConnectionInfo.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/session/manager/ConnectionInfo.kt
@@ -19,9 +19,9 @@
 
 package de.kuschku.libquassel.session.manager
 
-import de.kuschku.libquassel.connection.HostnameVerifier
 import de.kuschku.libquassel.connection.SocketAddress
 import de.kuschku.libquassel.protocol.ClientData
+import javax.net.ssl.HostnameVerifier
 import javax.net.ssl.X509TrustManager
 
 data class ConnectionInfo(
diff --git a/lib/src/main/java/de/kuschku/libquassel/ssl/BrowserCompatibleHostnameVerifier.kt b/lib/src/main/java/de/kuschku/libquassel/ssl/BrowserCompatibleHostnameVerifier.kt
deleted file mode 100644
index d4aa2d6ac..000000000
--- a/lib/src/main/java/de/kuschku/libquassel/ssl/BrowserCompatibleHostnameVerifier.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Quasseldroid - Quassel client for Android
- *
- * Copyright (c) 2020 Janne Mareike Koschinski
- * Copyright (c) 2020 The Quassel Project
- *
- * This program is free software: you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 3 as published
- * by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package de.kuschku.libquassel.ssl
-
-import de.kuschku.libquassel.connection.HostnameVerifier
-import de.kuschku.libquassel.connection.SocketAddress
-import de.kuschku.libquassel.ssl.X509Helper.hostnames
-import java.net.IDN
-import java.security.cert.X509Certificate
-import javax.net.ssl.SSLException
-
-class BrowserCompatibleHostnameVerifier : HostnameVerifier {
-  override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) {
-    val leafCertificate = chain.firstOrNull() ?: throw SSLException("No Certificate found")
-    val hostnames = hostnames(leafCertificate).toList()
-    if (hostnames.none { matches(it, address.host) })
-      throw SSLException("Hostname does not match")
-  }
-
-  private fun matches(name: String, host: String): Boolean {
-    // First we normalize both by removing trailing dots (absolute DNS names), splitting into DNS
-    // labels, and punycoding all unicode parts.
-    val normalizedName = name.trimEnd('.').split('.').map(IDN::toASCII)
-    val normalizedHost = host.trimEnd('.').split('.').map(IDN::toASCII)
-
-    // Only if both have the same number of DNS labels they can match
-    if (normalizedHost.size != normalizedName.size) return false
-
-    // Hosts with size of zero are invalid
-    if (normalizedHost.isEmpty()) return false
-
-    val both = normalizedName.zip(normalizedHost)
-
-    // The first label has to either match exactly, or be *
-    if (!both.take(1).all { (target, actual) ->
-        target.equals(actual, ignoreCase = true) || target == "*"
-      }) return false
-
-    // All other labels have to match exactly.
-    if (!both.drop(1).all { (target, actual) ->
-        target.equals(actual, ignoreCase = true)
-      }) return false
-
-    return true
-  }
-}
diff --git a/lib/src/main/java/de/kuschku/libquassel/ssl/TrustAllHostnameVerifier.kt b/lib/src/main/java/de/kuschku/libquassel/ssl/TrustAllHostnameVerifier.kt
deleted file mode 100644
index 75562629b..000000000
--- a/lib/src/main/java/de/kuschku/libquassel/ssl/TrustAllHostnameVerifier.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Quasseldroid - Quassel client for Android
- *
- * Copyright (c) 2020 Janne Mareike Koschinski
- * Copyright (c) 2020 The Quassel Project
- *
- * This program is free software: you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 3 as published
- * by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package de.kuschku.libquassel.ssl
-
-import de.kuschku.libquassel.connection.HostnameVerifier
-import de.kuschku.libquassel.connection.SocketAddress
-import java.security.cert.X509Certificate
-
-class TrustAllHostnameVerifier : HostnameVerifier {
-  override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) = Unit
-}
diff --git a/lib/src/main/java/de/kuschku/libquassel/connection/HostnameVerifier.kt b/lib/src/main/java/de/kuschku/libquassel/ssl/X509CertificateHelper.kt
similarity index 74%
rename from lib/src/main/java/de/kuschku/libquassel/connection/HostnameVerifier.kt
rename to lib/src/main/java/de/kuschku/libquassel/ssl/X509CertificateHelper.kt
index 24c9e8d16..ca88664e4 100644
--- a/lib/src/main/java/de/kuschku/libquassel/connection/HostnameVerifier.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/ssl/X509CertificateHelper.kt
@@ -17,12 +17,9 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel.connection
+package de.kuschku.libquassel.ssl
 
-import java.security.cert.X509Certificate
-import javax.net.ssl.SSLException
+import javax.security.cert.X509Certificate
 
-interface HostnameVerifier {
-  @Throws(SSLException::class)
-  fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>)
-}
+fun X509Certificate.toJavaCertificate() = X509Helper.convert(this)
+fun Array<X509Certificate>.toJavaCertificate() = X509Helper.convert(this)
diff --git a/lib/src/main/java/de/kuschku/libquassel/ssl/X509Helper.kt b/lib/src/main/java/de/kuschku/libquassel/ssl/X509Helper.kt
index 42307e8bf..51412f517 100644
--- a/lib/src/main/java/de/kuschku/libquassel/ssl/X509Helper.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/ssl/X509Helper.kt
@@ -23,9 +23,8 @@ import java.io.ByteArrayInputStream
 import java.security.cert.CertificateFactory
 import java.security.cert.X509Certificate
 
-// FIXME: re-read RFC and check it's actually secure
 object X509Helper {
-  val certificateFactory = CertificateFactory.getInstance("X.509")
+  private val certificateFactory = CertificateFactory.getInstance("X.509")
 
   fun hostnames(certificate: X509Certificate): Sequence<String> =
     (sequenceOf(certificate.subjectX500Principal.commonName) + subjectAlternativeNames(certificate))
@@ -48,6 +47,9 @@ object X509Helper {
   fun convert(certificate: javax.security.cert.X509Certificate) =
     certificateFactory.generateCertificate(ByteArrayInputStream(certificate.encoded)) as? X509Certificate
 
+  fun convert(chain: Array<out javax.security.cert.X509Certificate>): Array<X509Certificate>? =
+    chain.map { convert(it) ?: return null }.toTypedArray()
+
 
   val COMMON_NAME = Regex("""(?:^|,\s?)(?:CN=("(?:[^"]|"")+"|[^,]+))""")
   val ORGANIZATION = Regex("""(?:^|,\s?)(?:O=("(?:[^"]|"")+"|[^,]+))""")
diff --git a/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt b/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt
index 878ea7fc8..9b539999c 100644
--- a/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/util/nio/WrappedChannel.kt
@@ -19,8 +19,9 @@
 
 package de.kuschku.libquassel.util.nio
 
-import de.kuschku.libquassel.connection.HostnameVerifier
+import de.kuschku.libquassel.connection.QuasselSecurityException
 import de.kuschku.libquassel.connection.SocketAddress
+import de.kuschku.libquassel.ssl.X509Helper
 import de.kuschku.libquassel.util.compatibility.CompatibilityUtils
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
 import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel
@@ -35,12 +36,8 @@ import java.nio.channels.ReadableByteChannel
 import java.nio.channels.WritableByteChannel
 import java.security.GeneralSecurityException
 import java.security.NoSuchAlgorithmException
-import java.security.cert.X509Certificate
 import java.util.zip.InflaterInputStream
-import javax.net.ssl.SSLContext
-import javax.net.ssl.SSLSocket
-import javax.net.ssl.SSLSocketFactory
-import javax.net.ssl.X509TrustManager
+import javax.net.ssl.*
 
 class WrappedChannel private constructor(
   private val socket: Socket,
@@ -91,8 +88,10 @@ class WrappedChannel private constructor(
   }
 
   @Throws(GeneralSecurityException::class, IOException::class)
-  fun withSSL(certificateManager: X509TrustManager, hostnameVerifier: HostnameVerifier,
-              address: SocketAddress): WrappedChannel {
+  fun withSSL(certificateManager: X509TrustManager,
+              hostnameVerifier: HostnameVerifier,
+              address: SocketAddress
+  ): WrappedChannel {
     val tlsVersion = try {
       selectBestTlsVersion(SSLContext.getDefault().defaultSSLParameters.protocols)
     } catch (e: NoSuchAlgorithmException) {
@@ -111,10 +110,13 @@ class WrappedChannel private constructor(
     val socket = factory.createSocket(socket, address.host, address.port, true) as SSLSocket
     socket.useClientMode = true
     socket.addHandshakeCompletedListener {
-      hostnameVerifier.checkValid(
-        address,
-        socket.session.peerCertificates.map { it as X509Certificate }.toTypedArray()
-      )
+      if (socket.session.peerCertificateChain.isEmpty()) {
+        throw QuasselSecurityException.NoCertificate(address)
+      }
+      if (!hostnameVerifier.verify(address.host, socket.session)) {
+        throw QuasselSecurityException.WrongHostname(X509Helper.convert(socket.session.peerCertificateChain),
+                                                     address)
+      }
     }
     socket.startHandshake()
     return ofSocket(socket)
-- 
GitLab