SwiftNIO: Send and receive UDP broadcast - swift-nio

I'm trying to build a TCP server with SwiftNIO. The server starts in the net, but the clients don't know the ip address. Therefore I want to start an UDP server as well and if the clients comes up, he sends a broadcast message to the net. The server will receive and answer, so that the client now knows the IP address.
Is it possible to build something like this with SwiftNIO?

Yes, that's possible also there's not much support in SwiftNIO to make this easy.
See below for a commented example which will send HELLO WORLD once a second to en0's broadcast address and port 37020.
import NIO
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
let matchingInterfaces = try System.enumerateInterfaces().filter {
// find an IPv4 interface named en0 that has a broadcast address.
$0.name == "en0" && $0.broadcastAddress != nil
}
guard let en0Interface = matchingInterfaces.first, let broadcastAddress = en0Interface.broadcastAddress else {
print("ERROR: No suitable interface found. en0 matches \(matchingInterfaces)")
exit(1)
}
// let's bind the server socket
let server = try! DatagramBootstrap(group: group)
// enable broadast
.channelOption(ChannelOptions.socket(SOL_SOCKET, SO_BROADCAST), value: 1)
.bind(to: en0Interface.address)
.wait()
print("bound to \(server.localAddress!)")
var buffer = server.allocator.buffer(capacity: 32)
buffer.writeString("HELLO WORLD!")
var destAddr = broadcastAddress
destAddr.port = 37020 // we're sending to port 37020
// now let's just send the buffer once a second.
group.next().scheduleRepeatedTask(initialDelay: .seconds(1),
delay: .seconds(1),
notifying: nil) { task in
server.writeAndFlush(AddressedEnvelope(remoteAddress: destAddr,data: buffer)).map {
print("message sent to \(destAddr)")
}.whenFailure { error in
print("ERROR: \(error)")
// and stop if there's an error.
task.cancel()
server.close(promise: nil)
}
}
try server.closeFuture.wait()
In case you want to bind to 0.0.0.0 and send to 255.255.255.255 you can use this
import NIO
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
// let's bind the server socket
let server = try! DatagramBootstrap(group: group)
// enable broadast
.channelOption(ChannelOptions.socket(SOL_SOCKET, SO_BROADCAST), value: 1)
.bind(to: .init(ipAddress: "0.0.0.0", port: 0))
.wait()
print("bound to \(server.localAddress!)")
var buffer = server.allocator.buffer(capacity: 32)
buffer.writeString("HELLO WORLD!")
// we're sending to port 37020
let destPort = 37020
let destAddress = try SocketAddress(ipAddress: "255.255.255.255", port: destPort)
// now let's just send the buffer once a second.
group.next().scheduleRepeatedTask(initialDelay: .seconds(1),
delay: .seconds(1),
notifying: nil) { task in
server.writeAndFlush(AddressedEnvelope(remoteAddress: destAddress, data: buffer)).map {
print("message sent to \(destAddress)")
}.whenFailure { error in
print("ERROR: \(error)")
// and stop if there's an error.
task.cancel()
server.close(promise: nil)
}
}
try server.closeFuture.wait()

Related

Is is possible to open SwiftNIO based server socket within XCTest test app?

I have an XCTest which works with UI components. I tried to open a server socket within the xctext function using SwiftNIO.
I took the echo server example from here. and I simplified, removed the args with hardcoded values for the sake of a dirty test.
import XCTest
import NIOCore
import NIOPosix
private final class EchoHandler: ChannelInboundHandler {
public typealias InboundIn = ByteBuffer
public typealias OutboundOut = ByteBuffer
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.write(data, promise: nil)
}
// Flush it out. This can make use of gathering writes if multiple buffers are pending
public func channelReadComplete(context: ChannelHandlerContext) {
context.flush()
}
public func errorCaught(context: ChannelHandlerContext, error: Error) {
print("error: ", error)
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.close(promise: nil)
}
}
class MyXCTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
// Catch system alerts such as "allow connecting to Wi-fi network"
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
alert.buttons["Join"].tap()
return true
}
}
override func tearDownWithError() throws {
}
func testXYZ() throws {
app.launch()
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let bootstrap = ServerBootstrap(group: group)
// Specify backlog and enable SO_REUSEADDR for the server itself
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
// Set the handlers that are appled to the accepted Channels
.childChannelInitializer { channel in
// Ensure we don't read faster than we can write by adding the BackPressureHandler into the pipeline.
channel.pipeline.addHandler(BackPressureHandler()).flatMap { v in
channel.pipeline.addHandler(EchoHandler())
}
}
// Enable SO_REUSEADDR for the accepted Channels
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
.childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
defer {
try! group.syncShutdownGracefully()
}
let channel = try { () -> Channel in
return try bootstrap.bind(host: "0.0.0.0", port: 1234).wait()
}()
print("============= Server started and listening on \(channel.localAddress!)")
// then some XCTest code which works here was cut from this snippet
}
The test runs correctly, it also prints
============= Server started and listening on [IPv4]0.0.0.0/0.0.0.0:1234
But in reality EchoClient from here doesn't work
swift run NIOEchoClient localhost 1234    1785
[0/0] Build complete!
Please enter line to send to the server
dfsdfd
Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOPosix.NIOConnectionError(host: "localhost", port: 1234, dnsAError: nil, dnsAAAAError: nil, connectionErrors: [NIOPosix.SingleConnectionFailure(target: [IPv6]localhost/::1:1234, error: connection reset (error set): Connection refused (errno: 61)), NIOPosix.SingleConnectionFailure(target: [IPv4]localhost/127.0.0.1:1234, error: connection reset (error set): Connection refused (errno: 61))])
[1] 28213 trace trap swift run NIOEchoClient localhost 1234
The listening socket also unavailable with
sudo lsof -PiTCP -sTCP:LISTEN
I was also trying UITestEntitlements to set com.apple.security.app-sandbox to false.
Is there a way to allow server sockets from XCTest?
Originally I was trying to embed a Swift-GRPC endpoint, to allow more finer grained control from a HW in the loop controller. The intent is to start an XCTest using command line xcodebuild, which in turn is starting a long running test, but instead of the test code written in Swift, I would expose the events when to tap some buttons, right outside of the test process through a grpc endpoint.
Since the grpc endpoint didn't worked, I reduced the problem to the one above.
Anybody have a hint, how to pass through this issue, or have a hint why it will never be possible to open server socket within an XCTest app, don't hesitate to reply here.
Yes, that is possible, you can find many examples of this in the AsyncHTTPClient and SwiftNIO test suites.
The reason that yours doesn't work is because you shut down the MultiThreadedEventLoopGroup right after binding the socket. So essentially you're starting everything up and then you shut it down again.
Also, for unit tests, I'd recommend binding to 127.0.0.1 only because you probably don't want connections from elsewhere. Another good idea is to use an ephemeral port, ie. have the system pick a free, random port automatically. You can achieve this by specifying port 0. After you bind the server Channel you can then interrogate the server channel by using serverChannel.localAddress?.port! about the port it picked.
Here's a full example with a client and a server in a test case.
import XCTest
import NIO
final class ExampleTests: XCTestCase {
// We keep the `EventLoopGroup` where all the I/O runs alive during the test
private var group: EventLoopGroup!
// Same for the server channel.
private var serverChannel: Channel!
private var serverAddress: SocketAddress {
return self.serverChannel.localAddress!
}
// We set up the server in `setUp`...
override func setUp() {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
XCTAssertNoThrow(self.serverChannel = try ServerBootstrap(group: self.group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(UppercasingHandler())
}
.bind(host: "127.0.0.1", port: 0) // port 0 means: pick a free port.
.wait())
}
// ... and tear it down in `tearDown`.
override func tearDown() {
XCTAssertNoThrow(try self.serverChannel?.close().wait())
XCTAssertNoThrow(try self.group?.syncShutdownGracefully())
}
func testExample() throws {
// Here we just bootstrap a little client that sends "Hello world!\n"
let clientChannel = try ClientBootstrap(group: self.group)
.channelInitializer { channel in
channel.pipeline.addHandler(PrintEverythingHandler())
}
.connect(to: self.serverAddress)
.wait()
XCTAssertNoThrow(try clientChannel
.writeAndFlush(ByteBuffer(string: "Hello world!\n"))
.wait())
XCTAssertNoThrow(try clientChannel.closeFuture.wait())
}
}
// Just a handler that uses the C `toupper` function which uppercases characters.
final class UppercasingHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let inBuffer = self.unwrapInboundIn(data)
var outBuffer = context.channel.allocator.buffer(capacity: inBuffer.readableBytes)
// Here we just upper case each byte using the C stdlib's `toupper` function.
outBuffer.writeBytes(inBuffer.readableBytesView.map { UInt8(toupper(CInt($0))) })
context.writeAndFlush(self.wrapOutboundOut(outBuffer),
promise: nil)
}
// We want to close the connection on any error really.
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("server: unexpected error \(error), closing")
context.close(promise: nil)
}
}
// This handler just prints everything using the `write` system call. And closes the connection on a newline.
final class PrintEverythingHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let inBuffer = self.unwrapInboundIn(data)
guard inBuffer.readableBytes > 0 else {
return
}
// We're using Unsafe* stuff here because we're using the `write` system call, which is a C function.
_ = inBuffer.withUnsafeReadableBytes { ptr in
write(STDOUT_FILENO, ptr.baseAddress!, ptr.count)
}
// If we see a newline, then let's actually close the connection...
if inBuffer.readableBytesView.contains(UInt8(ascii: "\n")) {
print("found newline, closing...")
context.close(promise: nil)
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("client: unexpected error \(error), closing")
context.close(promise: nil)
}
}

GRPC-Swift Send HTTPProtocolVersion from Client

I'm trying GRPC-Swift for Client-Server application.
I'm using GRPC-Swift for both Client and Server
Client is an iPhone application, which I tried with iPhone Simulator.
I followed this link for Client-side streaming RPC.
When I send message to Server from Client, I got the following error message in the console from Server,
error io.grpc.server_channel_call : unable to determine http version
From the Server in the
HTTPProtocolSwitcher.swift
inside the function func channelRead(context: ChannelHandlerContext, data: NIOAny), it is checking for HTTPProtocolVersion, and it is missing.
How to send the HTTPVersion from the Client code?
Update:
Client Code
import GRPC
import NIO
class HTTPClient {
private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
private var channel: ClientConnection?
private var client: ChatGuide_ChatGuideClient?
private var clientCall: ClientStreamingCall<ChatGuide_TextMessage, ChatGuide_TextMessage>?
func connect(host: String, port: Int) throws {
let channel = ClientConnection.secure(group: self.group)
.connect(host: host, port: port)
self.channel = channel
self.client = ChatGuide_ChatGuideClient(channel: channel)
}
func disconnect() {
do {
self.clientCall?.sendEnd(promise: nil)
_ = try self.clientCall?.status.wait()
try self.group.syncShutdownGracefully()
} catch let error {
print("\(type(of: self)): Could not shutdown gracefully -", error.localizedDescription)
}
}
func initiateClient() {
let timeAmount = TimeAmount.minutes(1)
let timeLimit = TimeLimit.timeout(timeAmount)
let options = CallOptions(timeLimit: timeLimit)
let call = self.client?.chat(callOptions: options)
call?.response.whenSuccess { (message) in
print("\(type(of: self)): Message from server -", message.text)
}
call?.response.whenFailure { (error) in
print("\(type(of: self)): Response error -", error.localizedDescription)
}
self.clientCall = call
}
func send(text: String) {
if self.clientCall == nil {
self.initiateClient()
}
let message = ChatGuide_TextMessage.with {
$0.text = text
}
self.clientCall?.sendMessage(message, promise: nil)
}
}
Hey Vignesh,
I am currently learning gRPC-Swift myself, so I hope I will be of service and not muck things further.
However, it looks to me that you are not configuring the HTTP/1.x layer in order to transfer Protobuf packets, if you take a look at the HTTP1ToGRPCServerCodec.swift file Here
I think you will have a much clearer idea of how to adjust your code, I am sorry I can't provide more details, however not being too sure myself without further testing and reviewing the codebase.
Best regards and keep me posted if indeed i was helpful,
cheers
From the Server I have initiated insecure Server as,
let server = Server.insecure(group: self.group)
From the Client I have initiated secure ClientConnection as,
let channel = ClientConnection.secure(group: self.group)
And I got this clarification from here
So I made the ClientConnection also insecure as,
let channel = ClientConnection.insecure(group: self.group)
And after this it is working now.

What is proper way to get ip address from NWConnection

I'm using NWListener newConnectionHandler handler to get newConnection like this:
listener.newConnectionHandler = { [weak self] newConnection in
guard let self = self else { return }
newConnection.stateUpdateHandler = { [weak self] newState in
guard let self = self else { return }
...
...
print("Connection endpoint: \(newConnection.endpoint)")
}
newConnection.start(queue: self.queue)
}
This is prints:
10.0.1.2:62610
I need to get just ip address, to save it for later, but I wasn't able to find any property inside NWEndpoint to get it. I can do something like this:
var ipAddressWithPort = newConnection.endpoint.debugDescription
if let portRange = ipAddressWithPort.range(of: ":") {
ipAddressWithPort.removeSubrange(portRange.lowerBound..<ipAddressWithPort.endIndex)
}
But I don't like it at all. What is proper way to get ip address?
Thank you.
NWEndpoint is an enumeration (with associated values) for the different kinds of endpoints. The remote host of an accepted TCP connection will be an endpoint defined by host and port, and you can use a switch statement to extract those values.
If you just want a string representation of the host part, without the port, it would be
switch(connection.endpoint) {
case .hostPort(let host, _):
let remoteHost = "\(host)"
print(remoteHost) // 10.0.1.2
default:
break
}

unable to download Tello SDK for IOS

Anyone knows how to download the Tello sdk for iOS. I have visited the link https://www.ryzerobotics.com/tello, but it is not useful
There isn't an official SDK to download for the Tello. You can find the instructions at this link which provides details of sending UDP packets over a network connection to the drone. You can use CocoaAsyncSocket found on GitHub to help you create the connection to the socket and then pass the commands as ASCII data to the Tello.
Here is some sample code to get you going (which I wrote earlier today on my own website):
var socket = GCDAsyncUdpSocket()
let sendHost = "192.168.10.1"
let sendPort: UInt16 = 8889
let statePort: UInt16 = 8890
First, create the socket, a String with the Tello IP, and a couple of constants holding the port numbers (8889 is used to send commands to, and also receives responses back such as "OK" if the command was successful. 8890 is used for receiving telemetry from the drone multiple times per second.
This will send the initial "command" command to the Tello:
// Set the delegate and dispatch queue
socket.setDelegate(self)
socket.setDelegateQueue(DispatchQueue.main)
// Send the "command" command to the socket.
do {
try socket.bind(toPort: sendPort)
try socket.enableBroadcast(true)
try socket.beginReceiving()
socket.send("command".data(using: String.Encoding.utf8)!,
toHost: sendHost,
port: sendPort,
withTimeout: 0,
tag: 0)
} catch {
print("Command command sent.")
}
You need to make sure that the class conforms to GCDAsyncUdpSocketDelegate.
Implement the delegate method which receives data from the Tello.
func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress address: Data, withFilterContext filterContext: Any?) {
let dataString = String(data: data, encoding: String.Encoding.utf8)
if (sock.localPort() == sendPort) {
print(dataString)
}
if (sock.localPort() == statePort) {
var telloStateDictionary = [String:Double]()
let stateArray = dataString?.components(separatedBy: ";")
for itemState in stateArray! {
let keyValueArray = itemState.components(separatedBy: ":")
if (keyValueArray.count == 2) {
telloStateDictionary[keyValueArray[0]] = Double(keyValueArray[1])
}
}
print(telloStateDictionary)
}
}
Set up port 8890 for listening to the telemetry:
let receiveSocket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
do {
try receiveSocket.bind(toPort: statePort)
} catch {
print("Bind Problem")
}
do {
try receiveSocket.beginReceiving()
} catch {
print("Receiving Problem")
}
Run the app. Assuming you have manually connected to the Tello (like you would if you were controlling it with the Tello app), you should begin getting information back from the Tello.
To send more commands to it, such as "takeoff" or "cw 360", you can do something such as:
func sendCommand(command: String) {
let message = command.data(using: String.Encoding.utf8)
socket.send(message!, toHost: sendHost, port: sendPort, withTimeout: 2, tag: 0)
}
Pass in the command as a string, and this will send it to the Tello.

NEHotspotHelper: Not Able to Send Web Request in Authenticating State of Authentication State Machine

I have implemented the NEHotspotHelper so that I can perform authentication in the background for networks with a captive portal.
I need to perform a web request in the 'authenticating' state so that I can retrieve the Wispr and also access an API.
However, when I try to use URLSession to send a web request, the request fails. This is the error:
[594:88737] dnssd_clientstub read_all(7) DEFUNCT
[594:88783] TIC TCP Conn Failed [4:0x1c0177a00]: 12:8 Err(-65554)
[594:88783] Task <FFD0DAE6-4864-437D-94F2-C9ED5D5748E2>.<1> HTTP load failed (error code: -1003 [12:8])
[594:88783] Task <FFD0DAE6-4864-437D-94F2-C9ED5D5748E2>.<1> finished with error - code: -1003
See a snippet of my code:
let registered = NEHotspotHelper.register(options: options, queue: queue) { (cmd: NEHotspotHelperCommand) in
print("Received command: \(cmd.commandType.rawValue)")
if cmd.commandType == NEHotspotHelperCommandType.filterScanList {
//Get all available hotspots
print("filter scan list")
var list: [NEHotspotNetwork] = cmd.networkList!
var response: NEHotspotHelperResponse
for l in list {
if (l.ssid=="my-ssid") {
response = cmd.createResponse(NEHotspotHelperResult.success)
} else {
response = cmd.createResponse(NEHotspotHelperResult.failure)
}
response.setNetworkList([chosenNetwork])
response.deliver()
}
} else if cmd.commandType == NEHotspotHelperCommandType.evaluate {
if let network = cmd.network {
if (network.ssid=="my-ssid") {
network.setConfidence(NEHotspotHelperConfidence.high)
let response = cmd.createResponse(NEHotspotHelperResult.success)
response.setNetwork(network)
response.deliver() //Respond back
} else {
let response = cmd.createResponse(NEHotspotHelperResult.failure)
response.deliver()
}
}
} else if cmd.commandType == NEHotspotHelperCommandType.authenticate {
print("authenticate")
var response = cmd.createResponse(NEHotspotHelperResult.unsupportedNetwork)
if let network = cmd.network{
if network.ssid == "my-ssid"{
self.queryUrl()
response = cmd.createResponse(NEHotspotHelperResult.success)
}
}
response.deliver() //Respond back
}
}
func queryUrl(){
let config = URLSessionConfiguration.default
config.allowsCellularAccess = false;
let session = URLSession.init(configuration: config)
let url = URL(string: "https://172.217.20.35")
let semaphore = DispatchSemaphore(value: 0)
let task = session.dataTask(with: url!){(data, response, error) in
if data==nil {
print(data as Any)
}
else{
print(NSString(data: data!, encoding: String.Encoding.utf8.rawValue) as Any)
}
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
}
I was also facing the similar issue. However, I found that developer need to bind the request with the received command before making web request to the connected network. All you need to do is to make NSMutableURLRequest and then call hitTestURLRequest.bind(to: command) because bind function is defined in the category of NSMutableURLRequest.
Others have reported problem with name server resolution from NEHotspotHelper callback. Try using an IP address to make the call.
Also don't forget URLSession works asynchronously. You will need to call response.deliver() only after the web service calls finishes.
This is the just of it:
if cmd.commandType
filterScanList
- SetConfidence to each of your networks in cmd.networkList - High and add to a local list variable.
- create Response.Success
- set Response.NetworkList to your list of confident networks
----
- deliver Response
Evaluate or PresentUI
//If it's your network
- SetConfidence to cmd.network - High
- create Response.Success
- set Response.Network to cmd.network
//If it's not your network
- create Response.UnsupportedNetwork
----
- deliver Response
Authenticate or Maintain
//If it's your network
- SetConfidence to cmd.network - High
- create Response.Success
- set Response.Network to cmd.network
- TRY AUTHENTICATE HERE SYNCHRONOUSLY
//If it's not your network
- create Response.UnsupportedNetwork
----
- deliver Response
Logoff or None
- create Response.Success
----
- deliver Response

Resources