Basic HTTP client from scratch

I want to create basic HTTP client from scratch using Swift for learning purpose to unserstand how the sender and receiver of http over the internet works. It's just for raw learning purpose so, I just want to go with socket. For now I am trying in the localhost, I've posted same in SO. I've tried below code

import Foundation
import Darwin

final class Client {
    
    let host: String
    let port: UInt16
    
    init(host: String, port: UInt16) {
        
        self.host = host
        self.port = port
    }
    
    func get(_ completionHandler: @escaping() -> Void) {
        
        // Create a socket
        let sock = socket(AF_INET, SOCK_STREAM, 0)
 
        var serveraddr = sockaddr_in()
        serveraddr.sin_family = sa_family_t(AF_INET)
        serveraddr.sin_port = self.port
        serveraddr.sin_addr.s_addr = inet_addr(self.host)

        // Connect to the server
          
        print("Connecting....")
        
        withUnsafePointer(to: &serveraddr) { sockaddrInPtr in
          
            let sockaddrPtr = UnsafeRawPointer(sockaddrInPtr).assumingMemoryBound(to: sockaddr.self)
            let connection = connect(sock, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_in>.size))
             
            if connection == 0 {
                
                // Form an HTTP GET request
                let request = "GET / HTTP/1.1\r\nHost: \(self.host)\r\n\r\n"
                let bytes = [UInt8](request.data(using: .utf8)!)

                // Send the request over the socket
                send(sock, bytes, request.count, 0)

                /*
                // Receive and print the response
                var response = [UInt8](repeating: 0, count: 1024)
                let bytesRead = recv(sock, &response, response.count, 0)
                if bytesRead > 0 {
                    if let httpResponse = String(bytes: response, encoding: .utf8) {
                        print(httpResponse)
                    }
                }

                // Close the socket
                close(sock)
                 */
                
            } else {
                
                print("not connected \(connection)")
            }
        }
    }
}

Usage:

let client: Client = .init(host: "127.0.0.1", port: 8080)
client.get {
                
     print("GET request successfully executed!")
}

I used python3 -m http.server 8080 for test server on a folder with some files. It works in Browser & Postman GET Request.

But the problem is that cleint is exit with print not connected -1

How can I resolve this issue & make successful request? Thanking you!

1 Like

It might be obvious but… you did run a server on localhost that listens on port 8080?

1 Like

Yes, I did runnig successfully using python3 -m http.server 8080 and Tested the same with Postman and Web Browser.

Please note that this forum tries to stay focused on topics related to Swift programming language itself.

Please also note that using socket API is no longer recommended on apple platforms (don't remember if it's officially deprecated or not, it probably should if not already).

htons or its modern analogue is missing here (to convert from platform endian to big endian). In the current form the app should work with a port set to, say, 37008 (0x9090).

Once you pass that, try to run this code in a sandboxed environment and it doesn't work - don't forget to enable "network client" permission in the app's entitlement file. And once you passed that you may also need to fiddle with NSAppTransportSecurity permission to allow HTTP.

Edit: fixed some typos above

2 Likes

I've ran the server using the socket and it's successfully works. So the client socket should also work also no deprication warning in the xcode.

for sin_port the type is in_port_t which is public typealias in_port_t = __uint16_t. I've used 0xF01F, in_port_t(self.port). Nothing works. I am not sure what is htons.

Yes I've already set App Transport Security Settings/Allow Arbitrary Loads = YES & Removed sandboxed environment already. The same host and port address using URLSession is works perfect.

@IBAction func actionGet(_ sender: NSButton) {
         
       URLSession.shared.dataTask(with: URL(string: "http://127.0.0.1:8080")!) { data, _, error in
           
           if let data = data, let stringValue = String(data: data, encoding: .utf32) { //  UTF32 CUSTOM_ENCODING in server side
                
               print("======== URLSession ========")
               print(stringValue)
               print("==================")

               print("======== Socket ======")
               self.client.get {

                   print("GET request successfully executed!")
               }
               print("==================")

           } else {
               
               if let error = error {
                   
                   print("Error: \(error.localizedDescription)")
                   
               } else {
                   
                   print("Unknown Execption!")
               }
           }
           
       }.resume()
    }

Console Log:


======== URLSession ========
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 53

<h1>Hello, World <b style="color:green;">GET</b></h1>
==================
======== Socket ======
Connecting....
not connected -1
==================

htons et al is traditional unix way to convert from host endian (little on current apple platforms) to network endian (always big). (the ntohx family of functions is doing the reverse). You could achieve the same result by using:

    serveraddr.sin_port = port.bigEndian

37008 works even if you got endian wrong as it is 0x9090.

What is 0xF01F? 8080 is 0x1F90 and in the big endian form would be 0x901F.

1 Like

Wow, It Works. network endian (always big) I never knew this & I understood now.

What is 0xF01F? 8080 is 0x1F90 and in the big endian form would be 0x901F.

I asked chatGPT about big endian & It replies,

The big-endian representation of the UInt16 value 8080 is 0xF01F

Thank you very much for yours help!!!

:rofl:

When asked math or programming questions it gives very convincing and plausible answers which are almost always wrong.

5 Likes

Using BSD Sockets correctly from Swift is a serious challenge. I’ve gone back and forth on this issue myself a lot, and I’ve finally settled on the approach shown in Calling BSD Sockets from Swift. As explained in that top-level post, this isn’t meant for production code but rather for test projects like the one we’re discussing here.

As to whether you should be using BSD Sockets at all, that’s something I cover in TN3151 Choosing the right networking API.

Oh, and implementing a correct HTTP client is ridiculously difficult, even if you limit yourself to HTTP/1.1 and no HTTPS. So, while writing your own HTTP code for a test project like this is fine, if you ever plan to deploy this I recommend that you use an existing HTTP library.

Yes I've already set App Transport Security Settings > Allow Arbitrary
Loads [to] YES

Just to be clear, ATS only applies to URLSession and above. Low-level APIs, like Network framework and BSD Sockets, just ignore ATS.

Removed sandboxed environment already.

OTOH, App Sandbox applies to all network connections, so that was the right move (-:

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

5 Likes

Good to know.

To expand on what Quinn wrote: if you do need sandboxed app (e.g. a UI app, not sure if this could apply to a terminal app) you could still use networking: on macOS you'd need to enable "outgoing network connections" permission (com.apple.security.network.client) or "incoming network connections" (com.apple.security.network.server), or both depending upon your needs – those are in the target's entitlements file.

2 Likes

Thanks for your writeup Calling BSD Sockets from Swift | Apple Developer Forums et. al. Is your QSocket2023 anywhere downloadable or do we need to copy'n'paste from your posts?

1 Like

Is your QSocket2023 anywhere downloadable or do we need to
copy'n'paste from your posts?

The latter.

It’s tricky, from a red tape perspective, for me to make it available as, say, a Swift package. I could probably cut through that but I don’t really want to. My latest design is tightly focused on my need to create small test projects, forums posts, and so on. In Real Code™ you’d do things differently; for example, you wouldn’t want to do name-to-address translation every time you send a UDP datagram O-:

I’d love for someone to tackle this problem properly, building a standard BSD Sockets [1] wrapper, kinda like Swift System, that everyone can use [2]. However, I’m not the person to take on that task.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] Actually, you’d want it to support Linux in which case it’d be… well… just Sockets (-:

[2] Much as I think that BSD Sockets is a horrible API that should just go away, that’s not going to happen )-:

1 Like

Well, there are quite a few contenders… would one of those receive your blessing or are they doing it wrong?

I can’t offer an opinon because I haven’t looked at any of them )-:

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple