MacOS HTTP Listener for a Swift App

I have a wireless stateless-switch within my private network (LAN) which transmits HTTP posts to a configured URL (e.g. an always-on MacMini) about the switch's state. Upon receipt of a post, my MacOS app will then take appropriate action. No response to the sender is required. The frequency of posts will be minimal ( a few per day), but require immediate attention when received. The LAN is protected from external misuse by a secure gateway.

I'd appreciate any suggestions for a lightweight solution (i.e. not a full-blown web-server), either as an overview of methods or sample source (e.g. on GitHub).

Regards, Michaela

Replies

This is a difficult question to answer. On one hand, there are too many answers. On the other hand, you haven't provided enough information. Most of the available solutions assume that your listener always runs in the background and doesn't need a logged-in GUI session.

If your listener can run in the background and doesn't need to be constantly logged in, then any solution you can find on the internet will work. That's just a basic network listener. Your only complication is that you will have to support HTTP.

Your Mac already has a full-blown web-server built in. The easiest solution would be to just enable it and use it. From there, it is merely a question of how much extra cleverness and complexity you want to add.

If you don't want to use the built-in web server, you will have to add your own code to respond to HTTP requests. Apple's APIs do support that.

You will need to use launchd to start your server automatically. You can just start your server at boot up or you can use launchd cleverness to automatically launch your app in response to activity on the listening port.

If you require a logged in user GUI session, then you will need a launchd agent to launch your app. I don't know if the other launchd cleverness will work in this case.

  • Thank you for your answer. I'm already using the Web Server built into MacOS for publicly facing web-sites, through the DMZ of my secure gateway to a different Mac. As I said, I don't need a full-blown web server and the Listener App's actions need to use Swift APIs, which are difficult to get to via Apache. I know the local IP addresses of the switch and listener.

    I've developed other Mac apps that constantly (well, every few seconds - but still always running) monitor blue-tooth devices and this is similar, except that the external device (switch) triggers the app's processing via an HTTP Post rather than Bluetooth connectivity with device update notifications. There will be a simple GUI (SwiftUI), for showing switch events, and events will be stored in CoreData and/or CloudKit (of which I've done many apps). The app will also send out alerts, the mode depending on the switch state reported.

    So, after further thought and reading, plus your answer, I'm inclined to build my own solution using Network Framework and URLSession - once I've gotten my ageing brain around the complexities. Hopefully I'll find some examples or tutorials to get an overview and options.

    Regards, Michaela

Add a Comment

Annoyingly, macOS does not have a simple HTTP server API. There are a bunch of HTTP server libraries out there. If I were in your situation I’d do this using SwiftNIO. You can run it on top of Network framework, so it’s a first class network product on Apple platforms.

The SwiftNIO folks are very active on Swift Forums > Related Projects > SwiftNIO.

I'm already using the Web Server built into MacOS for publicly facing web-sites, through the DMZ of my secure gateway to a different Mac.

So does that result in a port conflict, with both programs listener on port 80? Or can you configure one or the other to use a different port?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Thanks Quinn. I'll have a look at SwiftNIO, although I'm not yet 100% sure what protocol the Switch uses. All I know is that for each Switch state I can provide a URL for it to contact (so assume it's HTTP): today I'll log it's IP address traffic in my firewall and see what's happening. There shouldn't be a port 80 conflict because the public WebServer is on a separate physical LAN, with all external (internet) incoming port 80 packets being directed to it by the Gateway. The switch and intended Listener are on the internal (private) LAN, which I can configure for port 80 between only their 2 IP addresses.

  • Yep, the switch sends an HTTP GET to the specified UR: tested by sending to one of my public websites with a dummy file name e.g. http://mywebsite.com/switchstate1.html - can see the request in the Apache access_log

Add a Comment

OMG, the solution is very easy - at least in a prototype state. Now let's see how it performs in reality, with code for handling state changes.

import Foundation
import Network

class HTTPServer {
    static let shared = HTTPServer()
    var listener : NWListener
    var queue : DispatchQueue
    var connected : Bool = false
    let nwParms = NWParameters.tcp
    
    init() {
        queue = DispatchQueue(label: "HTTP Server Queue")
        listener = try! NWListener(using: nwParms, on: 80)
        listener.newConnectionHandler = { [weak self] (newConnection ) in
            print("**** New Connection added")
            if let strongSelf = self {
                newConnection.start(queue: strongSelf.queue)
                strongSelf.receive(on: newConnection)
            }
        }

        listener.stateUpdateHandler = { (newState) in
            print("**** Listener changed state to \(newState)")
        }

        listener.start(queue: queue)
    }

    func receive(on connection: NWConnection) {
        connection.receive(minimumIncompleteLength: 0, maximumLength: 4096) { (content, context, isComplete, error) in
            guard let receivedData = content else {
                print("**** content is nil")
                return
            }
            let dataString = String(decoding: receivedData, as: UTF8.self)
            print("**** received data = \(dataString)")
            connection.cancel()
        }
    }
}

This solution is a modification of a Bonjour Listener demo app in a WWDC presentation.

As I said in my original question, this is for listening for infrequent HTTP packets on an internal, secure network. I wouldn't dare use it as a fully open web-server. For my purposes, the information in the incoming HTTP command is all I need: it contains the new state of the WiFi switch that is on the local network.

Regards, Michaela