bjtj/swift-http-server

UPnP NOTIFY Event request header is not properly recognised

Closed this issue · 2 comments

The HTTP server does not properly recognise the header in the request from the device. The method readHeaderString looks for the "\r\n\r\n" as a suffix, while in fact, this delimiter could be found in the middle of the received data.

func readHeaderString(remoteSocket: Socket?) throws -> String {
    var data = Data(capacity: 1)
    var headerString = ""
    while self.finishing == false {
        if try remoteSocket?.isReadableOrWritable(timeout: 1_000).0 == false {
            continue
        }

        let bytesRead = try remoteSocket?.read(into: &data)
        if bytesRead! <= 0 {
            throw HttpHeaderError.insufficentHeaderString
        }
        headerString += String(data: data, encoding: .utf8)!
        if headerString.hasSuffix("\r\n\r\n") {
            break
        }
    }
    return headerString
}

Example of headerString after the first pass of the "while loop", where the header delimiter is in the middle of the bytes read.

"NOTIFY /Event/5C8A87D188AA4E6A97BA459FE6190AD2 HTTP/1.1\r\nNt: upnp:event\r\nNts: upnp:propchange\r\nSid: uuid:b3abedf7-ec1f-4c3a-8410-36b062f51772\r\nContent-type: text/xml\r\nSeq: 0\r\nContent-Length: 274\r\nHost: 192.168.30.101:52808\r\nConnection: Keep-Alive\r\nUser-Agent: Linux/3.10.105 UPnP/1.0 BubbleUPnPServer/0.9-update41**\r\n\r\n**<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><e:property>290</e:property><e:property>290</e:property><e:property>0</e:property></e:propertyset>"

I turns out, that the function to read the header actually reads the complete buffer. Therefore, when the request is handled, and the request handler tries to read data, the request handler ends up in and endless loop, because there are not byte left to be read.

Therefore I created a new function, that reads data from the socket, until the complete request has been read and splits it into header and data. For this I added a new variable data to the HttpRequest class

import Foundation
import Socket

public class HttpRequest {
public var remoteSocket: Socket?
public var header: HttpHeader = HttpHeader()

public var path: String {
    return header.firstLine.second
}

public var data = Data(capacity: 1)

public init(remoteSocket: Socket?, header: HttpHeader?) {
    self.remoteSocket = remoteSocket
    self.header = header!
    self.header.specVersion = HttpSpecVersion(rawValue: self.header.firstLine.third)
}

}

func communicate(remoteSocket: Socket) {

    do {
        guard let request = try readRequest(remoteSocket: remoteSocket) else { return }

// let headerString = try readHeaderString(remoteSocket: remoteSocket)
// let header = HttpHeader.read(text: headerString)
// let request = HttpRequest(remoteSocket: remoteSocket, header: header)
guard let response = handleRequest(request: request) else {
let response = errorResponse(code: 500)
try sendResponse(socket: remoteSocket, response: response)
return
}
try sendResponse(socket: remoteSocket, response: response)
} catch let error {
print("error: (error)")
}
}

func sendResponse(socket: Socket, response: HttpResponse) throws {
    try socket.write(from: response.header.description.data(using: .utf8)!)
    if let data = response.data {
        if response.header.transferEncoding == .chunked {
            try socket.write(from: "\(data.count)\r\n".data(using: .utf8)!)
        }
        try socket.write(from: data)
    }
    if let stream = response.stream {
        let bufferSize = 4096
        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
        while stream.hasBytesAvailable {
            let read = stream.read(buffer, maxLength: bufferSize)
            if response.header.transferEncoding == .chunked {
                try socket.write(from: "\(read)\r\n".data(using: .utf8)!)
            }
            try socket.write(from: buffer, bufSize: read)
        }
        buffer.deallocate()
    }
}

func errorResponse(code: Int) -> HttpResponse {
    let reason = HttpStatusCode.shared[code]
    let response = HttpResponse(code: code, reason: reason)
    response.header.contentType = "text/plain"
    response.data = "\(code) \(reason)".data(using: .utf8)
    return response
}

func readRequest(remoteSocket: Socket?) throws -> HttpRequest? {
    var request: HttpRequest?
    var data = Data(capacity: 1)
    var headerString = ""
    while self.finishing == false {
        if try remoteSocket?.isReadableOrWritable(timeout: 1_000).0 == false {
            continue
        }

        let bytesRead = try remoteSocket?.read(into: &data)
        if bytesRead! <= 0 {
            throw HttpHeaderError.insufficentHeaderString
        }
        headerString += String(data: data, encoding: .utf8)!
        if headerString.contains("\r\n\r\n") {
            if let index = headerString.range(of: "\r\n\r\n") {
                headerString = String(headerString.prefix(upTo: index.upperBound))
                let header = HttpHeader.read(text: headerString)
                guard let contentSize = header.contentLength else { throw HttpHeaderError.insufficentHeaderString }
                if headerString.count + contentSize + 11 == data.count {
                    request = HttpRequest(remoteSocket: remoteSocket, header: header)
                    let dataCopy = data[(data.endIndex - contentSize)...data.endIndex - 1]
                    request?.data.append(dataCopy)
                    break
                }
            } else { throw HttpHeaderError.insufficentHeaderString }
        }
    }
    return request
}

func readHeaderString(remoteSocket: Socket?) throws -> String {
    var data = Data(capacity: 1)
    var headerString = ""
    while self.finishing == false {
        if try remoteSocket?.isReadableOrWritable(timeout: 1_000).0 == false {
            continue
        }

        let bytesRead = try remoteSocket?.read(into: &data)
        if bytesRead! <= 0 {
            throw HttpHeaderError.insufficentHeaderString
        }
        headerString += String(data: data, encoding: .utf8)!
        if headerString.contains("\r\n\r\n") {
            if let index = headerString.range(of: "\r\n\r\n") {
                headerString = String(headerString.prefix(upTo: index.upperBound))
                break
            } else { throw HttpHeaderError.insufficentHeaderString }
        }
    }
    return headerString
}

Currently I don't really understand, where the offset of 11 between the data from the socket and the header size and content size is coming from. But at least for the reading of UPnP Events, this offset seems to be consistent.

after further tests and investigations, I found a solution for the problem.

Source files with the proposed solution are attached
HttpServer.swift.zip
HttpRequest.swift.zip