Add SocketOptionChannel for wider socket options. (#589)
Motivation: A small number of socket options have values that do not fit into a C int type. Our current ChannelOption based approach for setting these simply does not work, and cannot be extended to support the truly arbitrary types that the setsockopt/getsockopt functions allow here. This makes it impossible to use some socket options, which is hardly a good place to be. There were a number of ways we could have addressed this: we could have special-cased all socket options with non-integer types in ChannelOption, but I believe that would be too manual, and risk limiting users that need to set other socket options. We could also have added a ChannelOption that simply allows users to pass a buffer to write into or read from, but that is a very un-Swift-like API that feels pretty gross to hold. Ultimately, the nicest seemed to be a new protocol users could check for, and that would provide APIs that let users hold the correct concrete type. As with setsockopt/getsockopt, while this API is typed it is not type-safe: ultimately, the struct we have here is treated just as a buffer by setsockopt/getsockopt. We do not attempt to prevent users from shooting themselves in the foot here. This PR does not include an example use case in any server, as I will provide such an example in a subsequent multicast PR. Modifications: - Added a SocketOptionChannel protocol. - Conformed BaseSocketChannel to SocketOptionChannel. - Wrote some tests for this. Result: Users can set and get sockopts with data that is larger than a C int.
This commit is contained in:
parent
c9e3c4d48c
commit
be4ea8ac74
|
@ -484,7 +484,7 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
|
||||||
switch option {
|
switch option {
|
||||||
case _ as SocketOption:
|
case _ as SocketOption:
|
||||||
let (level, name) = option.value as! (SocketOptionLevel, SocketOptionName)
|
let (level, name) = option.value as! (SocketOptionLevel, SocketOptionName)
|
||||||
try socket.setOption(level: Int32(level), name: name, value: value)
|
try self.setSocketOption0(level: level, name: name, value: value)
|
||||||
case _ as AllocatorOption:
|
case _ as AllocatorOption:
|
||||||
bufferAllocator = value as! ByteBufferAllocator
|
bufferAllocator = value as! ByteBufferAllocator
|
||||||
case _ as RecvAllocatorOption:
|
case _ as RecvAllocatorOption:
|
||||||
|
@ -532,7 +532,7 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
|
||||||
switch option {
|
switch option {
|
||||||
case _ as SocketOption:
|
case _ as SocketOption:
|
||||||
let (level, name) = option.value as! (SocketOptionLevel, SocketOptionName)
|
let (level, name) = option.value as! (SocketOptionLevel, SocketOptionName)
|
||||||
return try socket.getOption(level: Int32(level), name: name)
|
return try self.getSocketOption0(level: level, name: name)
|
||||||
case _ as AllocatorOption:
|
case _ as AllocatorOption:
|
||||||
return bufferAllocator as! T.OptionType
|
return bufferAllocator as! T.OptionType
|
||||||
case _ as RecvAllocatorOption:
|
case _ as RecvAllocatorOption:
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
//===----------------------------------------------------------------------===//
|
||||||
|
//
|
||||||
|
// This source file is part of the SwiftNIO open source project
|
||||||
|
//
|
||||||
|
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
||||||
|
// Licensed under Apache License v2.0
|
||||||
|
//
|
||||||
|
// See LICENSE.txt for license information
|
||||||
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
//===----------------------------------------------------------------------===//
|
||||||
|
|
||||||
|
/// This protocol defines an object, most commonly a `Channel`, that supports
|
||||||
|
/// setting and getting socket options (via `setsockopt`/`getsockopt` or similar).
|
||||||
|
/// It provides a strongly typed API that makes working with larger, less-common
|
||||||
|
/// socket options easier than the `ChannelOption` API allows.
|
||||||
|
///
|
||||||
|
/// The API is divided into two portions. For socket options that NIO has prior
|
||||||
|
/// knowledge about, the API has strongly and safely typed APIs that only allow
|
||||||
|
/// users to use the exact correct type for the socket option. This will ensure
|
||||||
|
/// that the API is safe to use, and these are encouraged where possible.
|
||||||
|
///
|
||||||
|
/// These safe APIs are built on top of an "unsafe" API that is also exposed to
|
||||||
|
/// users as part of this protocol. The "unsafe" API is unsafe in the same way
|
||||||
|
/// that `UnsafePointer` is: incorrect use of the API allows all kinds of
|
||||||
|
/// memory-unsafe behaviour. This API is necessary for socket options that NIO
|
||||||
|
/// does not have prior knowledge of, but wherever possible users are discouraged
|
||||||
|
/// from using it.
|
||||||
|
///
|
||||||
|
/// ### Relationship to SocketOption
|
||||||
|
///
|
||||||
|
/// All `Channel` objects that implement this protocol should also support the
|
||||||
|
/// `SocketOption` `ChannelOption` for simple socket options (those with C `int`
|
||||||
|
/// values). These are the most common socket option types, and so this `ChannelOption`
|
||||||
|
/// represents a convenient shorthand for using this protocol where the type allows,
|
||||||
|
/// as well as avoiding the need to cast to this protocol.
|
||||||
|
///
|
||||||
|
/// - note: Like the `Channel` protocol, all methods in this protocol are
|
||||||
|
/// thread-safe.
|
||||||
|
public protocol SocketOptionProvider {
|
||||||
|
/// The `EventLoop` which is used by this `SocketOptionProvider` for execution.
|
||||||
|
var eventLoop: EventLoop { get }
|
||||||
|
|
||||||
|
/// Set a socket option for a given level and name to the specified value.
|
||||||
|
///
|
||||||
|
/// This function is not memory-safe: if you set the generic type parameter incorrectly,
|
||||||
|
/// this function will still execute, and this can cause you to incorrectly interpret memory
|
||||||
|
/// and thereby read uninitialized or invalid memory. If at all possible, please use one of
|
||||||
|
/// the safe functions defined by this protocol.
|
||||||
|
///
|
||||||
|
/// - parameters:
|
||||||
|
/// - level: The socket option level, e.g. `SOL_SOCKET` or `IPPROTO_IP`.
|
||||||
|
/// - name: The name of the socket option, e.g. `SO_REUSEADDR`.
|
||||||
|
/// - value: The value to set the socket option to.
|
||||||
|
/// - returns: An `EventLoopFuture` that fires when the option has been set,
|
||||||
|
/// or if an error has occurred.
|
||||||
|
func unsafeSetSocketOption<Value>(level: SocketOptionLevel, name: SocketOptionName, value: Value) -> EventLoopFuture<Void>
|
||||||
|
|
||||||
|
/// Obtain the value of the socket option for the given level and name.
|
||||||
|
///
|
||||||
|
/// This function is not memory-safe: if you set the generic type parameter incorrectly,
|
||||||
|
/// this function will still execute, and this can cause you to incorrectly interpret memory
|
||||||
|
/// and thereby read uninitialized or invalid memory. If at all possible, please use one of
|
||||||
|
/// the safe functions defined by this protocol.
|
||||||
|
///
|
||||||
|
/// - parameters:
|
||||||
|
/// - level: The socket option level, e.g. `SOL_SOCKET` or `IPPROTO_IP`.
|
||||||
|
/// - name: The name of the socket option, e.g. `SO_REUSEADDR`.
|
||||||
|
/// - returns: An `EventLoopFuture` containing the value of the socket option, or
|
||||||
|
/// any error that occurred while retrieving the socket option.
|
||||||
|
func unsafeGetSocketOption<Value>(level: SocketOptionLevel, name: SocketOptionName) -> EventLoopFuture<Value>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK:- Safe helper methods.
|
||||||
|
// Hello code reader! All the methods in this extension are "safe" wrapper methods that define the correct
|
||||||
|
// types for `setSocketOption` and `getSocketOption` and call those methods on behalf of the user. These
|
||||||
|
// wrapper methods are memory safe. All of these methods are basically identical, and have been copy-pasted
|
||||||
|
// around. As a result, if you change one, you should probably change them all.
|
||||||
|
//
|
||||||
|
// You are welcome to add more helper methods here, but each helper method you add must be tested.
|
||||||
|
public extension SocketOptionProvider {
|
||||||
|
/// Sets the socket option SO_LINGER to `value`.
|
||||||
|
///
|
||||||
|
/// - parameters:
|
||||||
|
/// - value: The value to set SO_LINGER to.
|
||||||
|
/// - returns: An `EventLoopFuture` that fires when the option has been set,
|
||||||
|
/// or if an error has occurred.
|
||||||
|
public func setSoLinger(_ value: linger) -> EventLoopFuture<Void> {
|
||||||
|
return self.unsafeSetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_LINGER, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the value of the socket option SO_LINGER.
|
||||||
|
///
|
||||||
|
/// - returns: An `EventLoopFuture` containing the value of the socket option, or
|
||||||
|
/// any error that occurred while retrieving the socket option.
|
||||||
|
public func getSoLinger() -> EventLoopFuture<linger> {
|
||||||
|
return self.unsafeGetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_LINGER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension BaseSocketChannel: SocketOptionProvider {
|
||||||
|
public func unsafeSetSocketOption<Value>(level: SocketOptionLevel, name: SocketOptionName, value: Value) -> EventLoopFuture<Void> {
|
||||||
|
if eventLoop.inEventLoop {
|
||||||
|
let promise: EventLoopPromise<Void> = eventLoop.newPromise()
|
||||||
|
executeAndComplete(promise) {
|
||||||
|
try setSocketOption0(level: level, name: name, value: value)
|
||||||
|
}
|
||||||
|
return promise.futureResult
|
||||||
|
} else {
|
||||||
|
return eventLoop.submit {
|
||||||
|
try self.setSocketOption0(level: level, name: name, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func unsafeGetSocketOption<Value>(level: SocketOptionLevel, name: SocketOptionName) -> EventLoopFuture<Value> {
|
||||||
|
if eventLoop.inEventLoop {
|
||||||
|
let promise: EventLoopPromise<Value> = eventLoop.newPromise()
|
||||||
|
executeAndComplete(promise) {
|
||||||
|
try getSocketOption0(level: level, name: name)
|
||||||
|
}
|
||||||
|
return promise.futureResult
|
||||||
|
} else {
|
||||||
|
return eventLoop.submit {
|
||||||
|
try self.getSocketOption0(level: level, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSocketOption0<Value>(level: SocketOptionLevel, name: SocketOptionName, value: Value) throws {
|
||||||
|
try self.socket.setOption(level: Int32(level), name: name, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSocketOption0<Value>(level: SocketOptionLevel, name: SocketOptionName) throws -> Value {
|
||||||
|
return try self.socket.getOption(level: Int32(level), name: name)
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,6 +81,7 @@ import XCTest
|
||||||
testCase(SniHandlerTest.allTests),
|
testCase(SniHandlerTest.allTests),
|
||||||
testCase(SocketAddressTest.allTests),
|
testCase(SocketAddressTest.allTests),
|
||||||
testCase(SocketChannelTest.allTests),
|
testCase(SocketChannelTest.allTests),
|
||||||
|
testCase(SocketOptionChannelTest.allTests),
|
||||||
testCase(SystemTest.allTests),
|
testCase(SystemTest.allTests),
|
||||||
testCase(ThreadTest.allTests),
|
testCase(ThreadTest.allTests),
|
||||||
testCase(TypeAssistedChannelHandlerTest.allTests),
|
testCase(TypeAssistedChannelHandlerTest.allTests),
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
//===----------------------------------------------------------------------===//
|
||||||
|
//
|
||||||
|
// This source file is part of the SwiftNIO open source project
|
||||||
|
//
|
||||||
|
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
||||||
|
// Licensed under Apache License v2.0
|
||||||
|
//
|
||||||
|
// See LICENSE.txt for license information
|
||||||
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
//===----------------------------------------------------------------------===//
|
||||||
|
//
|
||||||
|
// SocketOptionChannelTest+XCTest.swift
|
||||||
|
//
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
///
|
||||||
|
/// NOTE: This file was generated by generate_linux_tests.rb
|
||||||
|
///
|
||||||
|
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
|
||||||
|
///
|
||||||
|
|
||||||
|
extension SocketOptionChannelTest {
|
||||||
|
|
||||||
|
static var allTests : [(String, (SocketOptionChannelTest) -> () throws -> Void)] {
|
||||||
|
return [
|
||||||
|
("testSettingAndGettingComplexSocketOption", testSettingAndGettingComplexSocketOption),
|
||||||
|
("testObtainingDefaultValueOfComplexSocketOption", testObtainingDefaultValueOfComplexSocketOption),
|
||||||
|
("testSettingAndGettingSimpleSocketOption", testSettingAndGettingSimpleSocketOption),
|
||||||
|
("testObtainingDefaultValueOfSimpleSocketOption", testObtainingDefaultValueOfSimpleSocketOption),
|
||||||
|
("testPassingInvalidSizeToSetComplexSocketOptionFails", testPassingInvalidSizeToSetComplexSocketOptionFails),
|
||||||
|
("testLinger", testLinger),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
//===----------------------------------------------------------------------===//
|
||||||
|
//
|
||||||
|
// This source file is part of the SwiftNIO open source project
|
||||||
|
//
|
||||||
|
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
||||||
|
// Licensed under Apache License v2.0
|
||||||
|
//
|
||||||
|
// See LICENSE.txt for license information
|
||||||
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
//
|
||||||
|
//===----------------------------------------------------------------------===//
|
||||||
|
|
||||||
|
import NIO
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class SocketOptionChannelTest: XCTestCase {
|
||||||
|
var group: MultiThreadedEventLoopGroup!
|
||||||
|
var serverChannel: Channel!
|
||||||
|
var clientChannel: Channel!
|
||||||
|
|
||||||
|
struct CastError: Error { }
|
||||||
|
|
||||||
|
private func convertedChannel(file: StaticString = #file, line: UInt = #line) throws -> SocketOptionProvider {
|
||||||
|
guard let provider = self.clientChannel as? SocketOptionProvider else {
|
||||||
|
XCTFail("Unable to cast \(String(describing: self.clientChannel)) to SocketOptionProvider", file: file, line: line)
|
||||||
|
throw CastError()
|
||||||
|
}
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||||
|
self.serverChannel = try? assertNoThrowWithValue(ServerBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait())
|
||||||
|
self.clientChannel = try? assertNoThrowWithValue(ClientBootstrap(group: group).connect(to: serverChannel.localAddress!).wait())
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
XCTAssertNoThrow(try clientChannel.close().wait())
|
||||||
|
XCTAssertNoThrow(try serverChannel.close().wait())
|
||||||
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSettingAndGettingComplexSocketOption() throws {
|
||||||
|
let provider = try assertNoThrowWithValue(self.convertedChannel())
|
||||||
|
|
||||||
|
let newTimeout = timeval(tv_sec: 5, tv_usec: 0)
|
||||||
|
let retrievedTimeout = try assertNoThrowWithValue(provider.unsafeSetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_RCVTIMEO, value: newTimeout).then {
|
||||||
|
provider.unsafeGetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_RCVTIMEO) as EventLoopFuture<timeval>
|
||||||
|
}.wait())
|
||||||
|
|
||||||
|
XCTAssertEqual(retrievedTimeout.tv_sec, newTimeout.tv_sec)
|
||||||
|
XCTAssertEqual(retrievedTimeout.tv_usec, newTimeout.tv_usec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObtainingDefaultValueOfComplexSocketOption() throws {
|
||||||
|
let provider = try assertNoThrowWithValue(self.convertedChannel())
|
||||||
|
|
||||||
|
let retrievedTimeout: timeval = try assertNoThrowWithValue(provider.unsafeGetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_RCVTIMEO).wait())
|
||||||
|
XCTAssertEqual(retrievedTimeout.tv_sec, 0)
|
||||||
|
XCTAssertEqual(retrievedTimeout.tv_usec, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSettingAndGettingSimpleSocketOption() throws {
|
||||||
|
let provider = try assertNoThrowWithValue(self.convertedChannel())
|
||||||
|
|
||||||
|
let newReuseAddr = 1 as CInt
|
||||||
|
let retrievedReuseAddr = try assertNoThrowWithValue(provider.unsafeSetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_REUSEADDR, value: newReuseAddr).then {
|
||||||
|
provider.unsafeGetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_REUSEADDR) as EventLoopFuture<CInt>
|
||||||
|
}.wait())
|
||||||
|
|
||||||
|
XCTAssertNotEqual(retrievedReuseAddr, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObtainingDefaultValueOfSimpleSocketOption() throws {
|
||||||
|
let provider = try assertNoThrowWithValue(self.convertedChannel())
|
||||||
|
|
||||||
|
let reuseAddr: CInt = try assertNoThrowWithValue(provider.unsafeGetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_REUSEADDR).wait())
|
||||||
|
XCTAssertEqual(reuseAddr, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPassingInvalidSizeToSetComplexSocketOptionFails() throws {
|
||||||
|
// You'll notice that there are no other size mismatch tests in this file. The reason for that is that
|
||||||
|
// setsockopt is pretty dumb, and getsockopt is dumber. Specifically, setsockopt checks only that the length
|
||||||
|
// of the option value is *at least as large* as the expected struct (which is why this test will actually
|
||||||
|
// work), and getsockopt will happily return without error even in the buffer is too small. Either way,
|
||||||
|
// we just abandon the other tests: this is sufficient to prove that the error path works.
|
||||||
|
let provider = try assertNoThrowWithValue(self.convertedChannel())
|
||||||
|
|
||||||
|
do {
|
||||||
|
try provider.unsafeSetSocketOption(level: SocketOptionLevel(SOL_SOCKET), name: SO_RCVTIMEO, value: CInt(1)).wait()
|
||||||
|
XCTFail("Did not throw")
|
||||||
|
} catch let err as IOError where err.errnoCode == EINVAL {
|
||||||
|
// Acceptable error
|
||||||
|
} catch {
|
||||||
|
XCTFail("Invalid error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Tests for the safe helper functions.
|
||||||
|
func testLinger() throws {
|
||||||
|
let newLingerValue = linger(l_onoff: 1, l_linger: 64)
|
||||||
|
|
||||||
|
let provider = try self.convertedChannel()
|
||||||
|
XCTAssertNoThrow(try provider.setSoLinger(newLingerValue).then {
|
||||||
|
provider.getSoLinger()
|
||||||
|
}.map {
|
||||||
|
XCTAssertEqual($0.l_linger, newLingerValue.l_linger)
|
||||||
|
XCTAssertEqual($0.l_onoff, newLingerValue.l_onoff)
|
||||||
|
}.wait())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue