From d27f40d9b1cb2436f435e40cf952dbd19ed6d463 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sat, 7 Mar 2015 00:42:14 +1100 Subject: [PATCH 1/8] Add SNI support to client and server sockets. --- lib/pure/net.nim | 39 ++++++++++++++++++++++++--------------- lib/wrappers/openssl.nim | 28 ++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/lib/pure/net.nim b/lib/pure/net.nim index bed751542..f7fcea06d 100644 --- a/lib/pure/net.nim +++ b/lib/pure/net.nim @@ -81,6 +81,22 @@ type TReadLineResult: ReadLineResult, TSOBool: SOBool, PSocket: Socket, TSocketImpl: SocketImpl].} +type + IpAddressFamily* {.pure.} = enum ## Describes the type of an IP address + IPv6, ## IPv6 address + IPv4 ## IPv4 address + + TIpAddress* = object ## stores an arbitrary IP address + case family*: IpAddressFamily ## the type of the IP address (IPv4 or IPv6) + of IpAddressFamily.IPv6: + address_v6*: array[0..15, uint8] ## Contains the IP address in bytes in + ## case of IPv6 + of IpAddressFamily.IPv4: + address_v4*: array[0..3, uint8] ## Contains the IP address in bytes in + ## case of IPv4 + +proc parseIpAddress*(address_str: string): TIpAddress + proc isDisconnectionError*(flags: set[SocketFlag], lastError: OSErrorCode): bool = ## Determines whether ``lastError`` is a disconnection error. Only does this @@ -487,7 +503,7 @@ proc setSockOpt*(socket: Socket, opt: SOBool, value: bool, level = SOL_SOCKET) { setSockOptInt(socket.fd, cint(level), toCInt(opt), valuei) proc connect*(socket: Socket, address: string, port = Port(0), - af: Domain = AF_INET) {.tags: [ReadIOEffect].} = + af: Domain = AF_INET) {.tags: [ReadIOEffect, RootEffect].} = ## Connects socket to ``address``:``port``. ``Address`` can be an IP address or a ## host name. If ``address`` is a host name, this function will try each IP ## of that host name. ``htons`` is already performed on ``port`` so you must @@ -511,6 +527,13 @@ proc connect*(socket: Socket, address: string, port = Port(0), when defined(ssl): if socket.isSSL: + try: + discard parseIpAddress(address) + except ValueError: + # Discard result in case OpenSSL version doesn't support SNI, or we're + # not using TLSv1+ + discard SSL_set_tlsext_host_name(socket.sslHandle, address) + let ret = SSLConnect(socket.sslHandle) socketError(socket, ret) @@ -969,20 +992,6 @@ proc isSsl*(socket: Socket): bool = proc getFd*(socket: Socket): SocketHandle = return socket.fd ## Returns the socket's file descriptor -type - IpAddressFamily* {.pure.} = enum ## Describes the type of an IP address - IPv6, ## IPv6 address - IPv4 ## IPv4 address - - TIpAddress* = object ## stores an arbitrary IP address - case family*: IpAddressFamily ## the type of the IP address (IPv4 or IPv6) - of IpAddressFamily.IPv6: - address_v6*: array[0..15, uint8] ## Contains the IP address in bytes in - ## case of IPv6 - of IpAddressFamily.IPv4: - address_v4*: array[0..3, uint8] ## Contains the IP address in bytes in - ## case of IPv4 - proc IPv4_any*(): TIpAddress = ## Returns the IPv4 any address, which can be used to listen on all available ## network adapters diff --git a/lib/wrappers/openssl.nim b/lib/wrappers/openssl.nim index 29fe3a921..dfc08a2bd 100644 --- a/lib/wrappers/openssl.nim +++ b/lib/wrappers/openssl.nim @@ -141,6 +141,14 @@ const SSL_CTRL_GET_MAX_CERT_LIST* = 50 SSL_CTRL_SET_MAX_CERT_LIST* = 51 #* Allow SSL_write(..., n) to return r with 0 < r < n (i.e. report success # * when just a single record has been written): * + SSL_CTRL_SET_TLSEXT_SERVERNAME_CB = 53 + SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG = 54 + SSL_CTRL_SET_TLSEXT_HOSTNAME = 55 + TLSEXT_NAMETYPE_host_name* = 0 + SSL_TLSEXT_ERR_OK* = 0 + SSL_TLSEXT_ERR_ALERT_WARNING* = 1 + SSL_TLSEXT_ERR_ALERT_FATAL* = 2 + SSL_TLSEXT_ERR_NOACK* = 3 SSL_MODE_ENABLE_PARTIAL_WRITE* = 1 #* Make it possible to retry SSL_write() with changed buffer location # * (buffer contents must stay the same!); this is not the default to avoid # * the misconception that non-blocking SSL_write() behaves like @@ -296,9 +304,27 @@ proc CRYPTO_malloc_init*() = proc SSL_CTX_ctrl*(ctx: SslCtx, cmd: cInt, larg: int, parg: pointer): int{. cdecl, dynlib: DLLSSLName, importc.} +proc SSL_CTX_callback_ctrl(ctx: SslCtx, typ: cInt, fp: PFunction): int{. + cdecl, dynlib: DLLSSLName, importc.} + proc SSLCTXSetMode*(ctx: SslCtx, mode: int): int = result = SSL_CTX_ctrl(ctx, SSL_CTRL_MODE, mode, nil) +proc SSL_ctrl*(ssl: SslPtr, cmd: cInt, larg: int, parg: pointer): int{. + cdecl, dynlib: DLLSSLName, importc.} + +proc SSL_set_tlsext_host_name*(ssl: SslPtr, name: cstring): int = + result = SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name) + +proc SSL_get_servername*(ssl: SslPtr, typ: cInt = TLSEXT_NAMETYPE_host_name): cstring {.cdecl, dynlib: DLLSSLName, importc.} + +proc SSL_CTX_set_tlsext_servername_callback*(ctx: SslCtx, cb: PFunction): int = + result = SSL_CTX_callback_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, cb) + +proc SSL_CTX_set_tlsext_servername_arg*(ctx: SslCtx, arg: pointer): int = + result = SSL_CTX_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG, 0, arg) + + proc bioNew*(b: PBIO_METHOD): BIO{.cdecl, dynlib: DLLUtilName, importc: "BIO_new".} proc bioFreeAll*(b: BIO){.cdecl, dynlib: DLLUtilName, importc: "BIO_free_all".} proc bioSMem*(): PBIO_METHOD{.cdecl, dynlib: DLLUtilName, importc: "BIO_s_mem".} @@ -341,8 +367,6 @@ else: dynlib: DLLSSLName, importc.} proc SslSetFd*(s: PSSL, fd: cInt): cInt{.cdecl, dynlib: DLLSSLName, importc.} - proc SslCtrl*(ssl: PSSL, cmd: cInt, larg: int, parg: Pointer): int{.cdecl, - dynlib: DLLSSLName, importc.} proc SslCTXCtrl*(ctx: PSSL_CTX, cmd: cInt, larg: int, parg: Pointer): int{. cdecl, dynlib: DLLSSLName, importc.} From 5978625c12c705a23ea4d2975162f55b160074ea Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sat, 7 Mar 2015 12:37:52 +1100 Subject: [PATCH 2/8] Make IP address checking more obvious, and add tests for the net module. --- lib/pure/net.nim | 16 ++++++++++++--- tests/stdlib/tnet.nim | 47 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/stdlib/tnet.nim diff --git a/lib/pure/net.nim b/lib/pure/net.nim index f7fcea06d..5361994f8 100644 --- a/lib/pure/net.nim +++ b/lib/pure/net.nim @@ -95,6 +95,7 @@ type address_v4*: array[0..3, uint8] ## Contains the IP address in bytes in ## case of IPv4 +proc isIpAddress*(address_str: string): bool proc parseIpAddress*(address_str: string): TIpAddress proc isDisconnectionError*(flags: set[SocketFlag], @@ -527,9 +528,8 @@ proc connect*(socket: Socket, address: string, port = Port(0), when defined(ssl): if socket.isSSL: - try: - discard parseIpAddress(address) - except ValueError: + # RFC3546 for SNI specifies that IP addresses are not allowed. + if not isIpAddress(address): # Discard result in case OpenSSL version doesn't support SNI, or we're # not using TLSv1+ discard SSL_set_tlsext_host_name(socket.sslHandle, address) @@ -1259,3 +1259,13 @@ proc parseIpAddress*(address_str: string): TIpAddress = return parseIPv6Address(address_str) else: return parseIPv4Address(address_str) + + +proc isIpAddress*(address_str: string): bool = + ## Checks if a string is an IP address + ## Returns true if it is, false otherwise + try: + discard parseIpAddress(address_str) + except ValueError: + return false + return true diff --git a/tests/stdlib/tnet.nim b/tests/stdlib/tnet.nim new file mode 100644 index 000000000..e8ada05e7 --- /dev/null +++ b/tests/stdlib/tnet.nim @@ -0,0 +1,47 @@ +import net +import unittest + +suite "isIpAddress tests": + test "127.0.0.1 is valid": + check isIpAddress("127.0.0.1") == true + + test "ipv6 localhost is valid": + check isIpAddress("::1") == true + + test "fqdn is not an ip address": + check isIpAddress("example.com") == false + + test "random string is not an ipaddress": + check isIpAddress("foo bar") == false + + test "5127.0.0.1 is invalid": + check isIpAddress("5127.0.0.1") == false + + test "ipv6 is valid": + check isIpAddress("2001:cdba:0000:0000:0000:0000:3257:9652") == true + + test "invalid ipv6": + check isIpAddress("gggg:cdba:0000:0000:0000:0000:3257:9652") == false + + +suite "parseIpAddress tests": + test "127.0.0.1 is valid": + discard parseIpAddress("127.0.0.1") + + test "ipv6 localhost is valid": + discard parseIpAddress("::1") + + test "fqdn is not an ip address": + expect(ValueError): + discard parseIpAddress("example.com") + + test "random string is not an ipaddress": + expect(ValueError): + discard parseIpAddress("foo bar") + + test "ipv6 is valid": + discard parseIpAddress("2001:cdba:0000:0000:0000:0000:3257:9652") + + test "invalid ipv6": + expect(ValueError): + discard parseIpAddress("gggg:cdba:0000:0000:0000:0000:3257:9652") From 6a548a5c920533fabb9f71327e814752faef29a5 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sat, 7 Mar 2015 19:52:11 +1100 Subject: [PATCH 3/8] Add some documentation to the server-side callback functions. --- lib/wrappers/openssl.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/wrappers/openssl.nim b/lib/wrappers/openssl.nim index dfc08a2bd..791c8d351 100644 --- a/lib/wrappers/openssl.nim +++ b/lib/wrappers/openssl.nim @@ -319,9 +319,12 @@ proc SSL_set_tlsext_host_name*(ssl: SslPtr, name: cstring): int = proc SSL_get_servername*(ssl: SslPtr, typ: cInt = TLSEXT_NAMETYPE_host_name): cstring {.cdecl, dynlib: DLLSSLName, importc.} proc SSL_CTX_set_tlsext_servername_callback*(ctx: SslCtx, cb: PFunction): int = + ## Set the callback to be used on listening SSL connections when the client hello is received. + ## Callback proc ``cb`` should be of the form `proc (ssl: SslPtr, cb_id: int, arg: pointer): int` result = SSL_CTX_callback_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, cb) proc SSL_CTX_set_tlsext_servername_arg*(ctx: SslCtx, arg: pointer): int = + ## Set the pointer to be used in the callback registered to ``SSL_CTX_set_tlsext_servername_callback``. result = SSL_CTX_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG, 0, arg) From 82803d28717c21b3cd6a1e74b2f429d093b9d487 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sun, 8 Mar 2015 12:36:33 +1100 Subject: [PATCH 4/8] Don't support old versions of OpenSSL. --- lib/wrappers/openssl.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wrappers/openssl.nim b/lib/wrappers/openssl.nim index 791c8d351..1305d1f31 100644 --- a/lib/wrappers/openssl.nim +++ b/lib/wrappers/openssl.nim @@ -50,7 +50,7 @@ when useWinVersion: from winlean import SocketHandle else: const - versions = "(.10|.1.0.1|.1.0.0|.0.9.9|.0.9.8|.0.9.7|.0.9.6|.0.9.5|.0.9.4)" + versions = "(.10|.1.0.1|.1.0.0|.0.9.9|.0.9.8)" when defined(macosx): const DLLSSLName = "libssl" & versions & ".dylib" From c179d10897e89fc1bfd0ab42578f79722989cd3a Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sun, 8 Mar 2015 13:37:32 +1100 Subject: [PATCH 5/8] isIpAddress doesn't have any effects, so fix it up to clean up connect. --- lib/pure/net.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pure/net.nim b/lib/pure/net.nim index 5361994f8..0c5b594bd 100644 --- a/lib/pure/net.nim +++ b/lib/pure/net.nim @@ -95,7 +95,7 @@ type address_v4*: array[0..3, uint8] ## Contains the IP address in bytes in ## case of IPv4 -proc isIpAddress*(address_str: string): bool +proc isIpAddress*(address_str: string): bool {.tags: [].} proc parseIpAddress*(address_str: string): TIpAddress proc isDisconnectionError*(flags: set[SocketFlag], @@ -504,7 +504,7 @@ proc setSockOpt*(socket: Socket, opt: SOBool, value: bool, level = SOL_SOCKET) { setSockOptInt(socket.fd, cint(level), toCInt(opt), valuei) proc connect*(socket: Socket, address: string, port = Port(0), - af: Domain = AF_INET) {.tags: [ReadIOEffect, RootEffect].} = + af: Domain = AF_INET) {.tags: [ReadIOEffect].} = ## Connects socket to ``address``:``port``. ``Address`` can be an IP address or a ## host name. If ``address`` is a host name, this function will try each IP ## of that host name. ``htons`` is already performed on ``port`` so you must From e214308cb19c5085979dc470c4804d0173eccb1e Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sun, 8 Mar 2015 13:38:44 +1100 Subject: [PATCH 6/8] Remvoe the visibility on these, given their forward declarations already specify it. --- lib/pure/net.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pure/net.nim b/lib/pure/net.nim index 0c5b594bd..ffbc6e320 100644 --- a/lib/pure/net.nim +++ b/lib/pure/net.nim @@ -1250,7 +1250,7 @@ proc parseIPv6Address(address_str: string): TIpAddress = raise newException(ValueError, "Invalid IP Address. The address consists of too many groups") -proc parseIpAddress*(address_str: string): TIpAddress = +proc parseIpAddress(address_str: string): TIpAddress = ## Parses an IP address ## Raises EInvalidValue on error if address_str == nil: @@ -1261,7 +1261,7 @@ proc parseIpAddress*(address_str: string): TIpAddress = return parseIPv4Address(address_str) -proc isIpAddress*(address_str: string): bool = +proc isIpAddress(address_str: string): bool = ## Checks if a string is an IP address ## Returns true if it is, false otherwise try: From 19ddae38da79b592b66452810a9415a075617e71 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sun, 8 Mar 2015 13:39:37 +1100 Subject: [PATCH 7/8] Some more documentation for the SNI related procs. --- lib/wrappers/openssl.nim | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/wrappers/openssl.nim b/lib/wrappers/openssl.nim index 1305d1f31..34e2d6ff5 100644 --- a/lib/wrappers/openssl.nim +++ b/lib/wrappers/openssl.nim @@ -315,12 +315,24 @@ proc SSL_ctrl*(ssl: SslPtr, cmd: cInt, larg: int, parg: pointer): int{. proc SSL_set_tlsext_host_name*(ssl: SslPtr, name: cstring): int = result = SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, name) + ## Set the SNI server name extension to be used in a client hello. + ## Returns 1 if SNI was set, 0 if current SSL configuration doesn't support SNI. + proc SSL_get_servername*(ssl: SslPtr, typ: cInt = TLSEXT_NAMETYPE_host_name): cstring {.cdecl, dynlib: DLLSSLName, importc.} + ## Retrieve the server name requested in the client hello. This can be used + ## in the callback set in `SSL_CTX_set_tlsext_servername_callback` to + ## implement virtual hosting. May return `nil`. proc SSL_CTX_set_tlsext_servername_callback*(ctx: SslCtx, cb: PFunction): int = ## Set the callback to be used on listening SSL connections when the client hello is received. ## Callback proc ``cb`` should be of the form `proc (ssl: SslPtr, cb_id: int, arg: pointer): int` + ## + ## The callback should return one of: + ## * SSL_TLSEXT_ERR_OK + ## * SSL_TLSEXT_ERR_ALERT_WARNING + ## * SSL_TLSEXT_ERR_ALERT_FATAL + ## * SSL_TLSEXT_ERR_NOACK result = SSL_CTX_callback_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, cb) proc SSL_CTX_set_tlsext_servername_arg*(ctx: SslCtx, arg: pointer): int = From 7cffd290bf5d20c7b9f191f222ae3f4ea7952523 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Sun, 8 Mar 2015 21:54:34 +1100 Subject: [PATCH 8/8] Refine the definition of the server name callbcak. --- lib/wrappers/openssl.nim | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/wrappers/openssl.nim b/lib/wrappers/openssl.nim index 34e2d6ff5..bca7b3a40 100644 --- a/lib/wrappers/openssl.nim +++ b/lib/wrappers/openssl.nim @@ -324,16 +324,15 @@ proc SSL_get_servername*(ssl: SslPtr, typ: cInt = TLSEXT_NAMETYPE_host_name): cs ## in the callback set in `SSL_CTX_set_tlsext_servername_callback` to ## implement virtual hosting. May return `nil`. -proc SSL_CTX_set_tlsext_servername_callback*(ctx: SslCtx, cb: PFunction): int = +proc SSL_CTX_set_tlsext_servername_callback*(ctx: SslCtx, cb: proc(ssl: SslPtr, cb_id: int, arg: pointer): int {.cdecl.}): int = ## Set the callback to be used on listening SSL connections when the client hello is received. - ## Callback proc ``cb`` should be of the form `proc (ssl: SslPtr, cb_id: int, arg: pointer): int` ## ## The callback should return one of: ## * SSL_TLSEXT_ERR_OK ## * SSL_TLSEXT_ERR_ALERT_WARNING ## * SSL_TLSEXT_ERR_ALERT_FATAL ## * SSL_TLSEXT_ERR_NOACK - result = SSL_CTX_callback_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, cb) + result = SSL_CTX_callback_ctrl(ctx, SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, cast[PFunction](cb)) proc SSL_CTX_set_tlsext_servername_arg*(ctx: SslCtx, arg: pointer): int = ## Set the pointer to be used in the callback registered to ``SSL_CTX_set_tlsext_servername_callback``.