Custom implementation of Curl - Build Your Own curl

Overview

Build Your Own curl

We are going to build curl from scratch by accepting the coding challenge posted on Coding Challenges FYI.

Before moving ahead, you must need to know how tcp client server connections works.

client server socket connection

You can read more about GeeksforGeeks.

On Server Side : -

  1. Socket:- Socket object to expose our endpoints.
  2. setsockopt:- This functions set the extra options for the sockets if needed.
  3. Bind:- It binds the socket with the ip and port.
  4. Listen:- Socket is now in listening state, and listen at the specified port for the incoming connection request. Here it queue incoming client request to connect. The second argument specifies the maximum number of request it can queue.
  5. Accept:- In this phase, server calls accept function, it initiates the 3-way handshaking. The client sends SYN packet to the server, server responds back with the SYN-ACK packet and blocks(wait) the connection, until it finally get the ACK packet.
  6. Send/Recv:- Once ACK received from client, communication can proceed to and fro.

On Client Side : -

  1. Socket initialization:- In this setup, socket is defined with all the configuration needed to connect.
  2. Connect:- In this phase, client call the connect function, which sends the SYN packet to the server with the intent to connect.
  3. Send/Recv: Once connection is established, when client can send and receive the data.

We are doing to acheive in few steps: -

  1. Getting the cli arguments
  2. Creation of socket connection
  3. Sending the request
  4. Parsing the response

1. Getting the cli arguments.

We will be using library for Clap - A simple-to-use, efficient, and full-featured library for parsing command line arguments and subcommands.

Tha clap library provides two different ways to build parse object. First is the Builder pattern(creational design pattern to create complex things by step by step process) and second Derive pattern in which library automatically generate code based on the macros.

We are using Builder pattern for our cli tool.

But you can implement Derive pattern at doc.rs/clap/_derive

'cli.rs'

use clap::{Arg, ArgMatches, Command};

pub fn get_arguments()-> ArgMatches{
    Command::new("Ccurl - custom curl")
        .about("It helps to make http methods")
        .version("1.0")
        .author("Praveen Chaudhary <[email protected]>")
        .arg(Arg::new("url").index(1).required(true))
        .arg(
            Arg::new("x-method")
                .help("Http method which you want to use")
                .long("x-method")
                .short('X'),
        )
        .arg(
            Arg::new("data")
                .help("Payload you want to send with the request")
                .long("data")
                .short('d'),
        )
        .arg(
            Arg::new("headers")
                .help("Request header")
                .long("header")
                .short('H')
                .action(ArgAction::Append),
        )
        .arg(
            Arg::new("verbose")
                .help("verbose mode")
                .long("verbose")
                .short('v')
                .action(clap::ArgAction::SetTrue),
        )
        .get_matches()
}

Firstly, we have define the basic info like about, author and version.

We have defined all the arguments need for our own curl. We have made one positional required argument url.

Argument matching or parsing

Clap makes it easier to match arguments.
For verbose, we have used action method .action(clap::ArgAction::SetTrue) because it will not contain any subsequent value. For headers, similarly we have used action method .action(ArgAction::Append), Append will append new values to the previous value if any value have already encountered. For others, we have simply used get_one method to get the value.

let verbose_enabled = matches.contains_id("verbose") && matches.get_flag("verbose");
    let url = matches.get_one::<String>("url").unwrap();
    let data = matches.get_one::<String>("data");
    let method = matches.get_one::<String>("x-method");
    let headers: Vec<&str> = matches
        .get_many::<String>("headers")
        .unwrap_or_default()
        .map(|s| s.as_str())
        .collect();

2. Drafting request with input information [ HTTP 1.1 - RFC9110.]

We will be using the RFC9110 for HTTP 1.1 client.

we will start with empty string, and append all the information needed for the Request according to RFC.

fn populate_get_request(
    protocol: &str,
    host: &str,
    path: &str,
    data: Option<&String>,
    method: Option<&String>,
    headers: Vec<&str>,
) -> String {
    let default_method = String::from("GET");
    let method = method.unwrap_or(&default_method);
    let mut res = String::new();
    res += &format!("{} /{} {}\r\n", method, path, protocol);
    res += &format!("Host: {}\r\n", host);
    res += "Accept: */*\r\n";
    res += "Connection: close\r\n";
    ....
    ....
    ....
    res += "\r\n";
    res
}

For PUT and POST, we need to add headers and data.

fn populate_get_request(
    protocol: &str,
    host: &str,
    path: &str,
    data: Option<&String>,
    method: Option<&String>,
    headers: Vec<&str>,
) -> String {
    ....
    ....

    if method == "POST" || method == "PUT" {
        if headers.len() > 0 {
            for head in headers {
                res += head;
            }
            res += "\r\n"
        } else {
            res += "Content-Type: application/json\r\n";
        }
        if let Some(data_str) = data {
            let data_bytes = data_str.as_bytes();
            res += &format!("Content-Length: {}\r\n\r\n", data_bytes.len());
            res += data_str;
            res += "\r\n";
        }
    }

    ....
    res
}

According to RFC, for post or post we need to provide Content-Length and Content-Type header.

So now we have complete request string. Let's move to socket connection, sending this request string to the server.

fn populate_get_request(
    protocol: &str,
    host: &str,
    path: &str,
    data: Option<&String>,
    method: Option<&String>,
    headers: Vec<&str>,
) -> String {
    let default_method = String::from("GET");
    let method = method.unwrap_or(&default_method);
    let mut res = String::new();
    res += &format!("{} /{} {}\r\n", method, path, protocol);
    res += &format!("Host: {}\r\n", host);
    res += "Accept: */*\r\n";
    res += "Connection: close\r\n";

    if method == "POST" || method == "PUT" {
        if headers.len() > 0 {
            for head in headers {
                res += head;
            }
            res += "\r\n"
        } else {
            res += "Content-Type: application/json\r\n";
        }
        if let Some(data_str) = data {
            let data_bytes = data_str.as_bytes();
            res += &format!("Content-Length: {}\r\n\r\n", data_bytes.len());
            res += data_str;
            res += "\r\n";
        }
    }

    res += "\r\n";
    res
}

3. Creation of socket connection

We will using the standard rust network library for socket connection with the host server.

fn main() {
    ....
    ....

    let tcp_socket = TcpStream::connect(socket_addr);

    match tcp_socket {
        Ok(mut stream) => {
            ....
            ....
        }
        Err(e) => {
            eprintln!("Failed to establish connection: {}", e);
        }
    }
    ....
    ....
}

Once we are successfully connected, we can listen and send your own request to server.

4. Sending the request

  1. First we have check if verbose mode is enabled, then we print out the request.
  2. We have used the write_all to ensure our that our whole buffer is added to the stream.
  3. Create a new empty buffer, and provide this buffer to the stream, to read the response data from the host.
  4. Converts that bytes string to UTF-8 string using the from_utf8_lossy.
  5. Print the response header and body.
fn main() {
    ....
    ....

    match tcp_socket {
        Ok(mut stream) => {
            if verbose_enabled {
                let lines = buffer_str.lines();
                for line in lines {
                    println!("> {}", line)
                }
            }
            stream
                .write_all(buffer_str.as_bytes())
                .expect("Failed to write data to stream");

            // initialising the buffer, reads data from the stream and stores it in the buffer.
            let mut buffer = [0; 1024];
            stream
                .read(&mut buffer)
                .expect("Failed to read from response from host!");

            // converts buffer data into a UTF-8 enccoded string (lossy ensures invalid data can be truncated).
            let response = String::from_utf8_lossy(&buffer[..]);

            // dividing the response headers and body
            let (response_header, response_data) = parse_resp(&response);
            if verbose_enabled {
                let lines = response_header.split("\r\n");
                for line in lines {
                    println!("< {}", line)
                }
            }
            println!("{}", response_data);
        }
        Err(e) => {
            eprintln!("Failed to establish connection: {}", e);
        }
    }
    ....
    ....
}

5. Time for Testing

cli - cargo run -- http://eu.httpbin.org:80/get response -

{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "eu.httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-65fec214-25771a3e732101c433ce67a7"
  }, 
  "origin": "49.36.177.79", 
  "url": "http://eu.httpbin.org/get"
}

Similarly, you can test others.

Hurray!! We have able to make our own curl.

You might also like...
Rust implementation of custom numeric base conversion.

base_custom Use any characters as your own numeric base and convert to and from decimal. This can be taken advantage of in various ways: Mathematics:

Reference implementation of a decentralized exchange for custom instruments, risk, and fees

Dexterity What is Dexterity At a high level, Dexterity is a smart contract (or collection of smart contracts) that allow for the creation of a decentr

Use Thunk to build your Rust program that runs on old Windows platforms, support Windows XP and more!

Use Thunk to build your Rust program that runs on old platforms. Thunk uses VC-LTL5 and YY-Thunks to build programs that support old platforms. So, ho

A very simple third-party cargo subcommand to execute a custom command

cargo-x A very simple third-party cargo subcommand to execute a custom command Usage install cargo-x cargo install cargo-x or upgrade cargo install -

git-cliff can generate changelog files from the Git history by utilizing conventional commits as well as regex-powered custom parsers.⛰️
git-cliff can generate changelog files from the Git history by utilizing conventional commits as well as regex-powered custom parsers.⛰️

git-cliff can generate changelog files from the Git history by utilizing conventional commits as well as regex-powered custom parsers. The changelog template can be customized with a configuration file to match the desired format.

Easy access of struct fields in strings using different/custom pre/postfix:
Easy access of struct fields in strings using different/custom pre/postfix: "Hello, {field}" in rust

Easy access to struct fields in strings 🐠 add strung to the dependencies in the Cargo.toml: [dependencies] strung = "0.1.3" 🦀 use/import everything

An efficient pictures manager based on custom tags and file system organization.

PicturesManager An efficient pictures manager based on custom tags and file system organization. Developed with Tauri (web app) with a Rust backend an

Custom module for showing the weather in Waybar, using the great wttr.io
Custom module for showing the weather in Waybar, using the great wttr.io

wttrbar a simple but detailed weather indicator for Waybar using wttr.in. Installation Compile yourself using cargo build --release, or download the p

Owner
Praveen Chaudhary
SDE at India Today| Rust🦀 and Python🐍 enthusiast | Full stack developer | API & App developer| Performance & Acceptance Tester
Praveen Chaudhary
Catch Tailwindcss Errors at Compile-Time Before They Catch You, without making any change to your code! Supports overriding, extending, custom classes, custom modifiers, Plugins and many more 🚀🔥🦀

twust Twust is a powerful static checker in rust for TailwindCSS class names at compile-time. Table of Contents Overview Installation Usage Statement

null 15 Nov 8, 2023
A curl(libcurl) mod for rust.

curl Master Dev A lightweight Curl-wrapper for using (mostly) HTTP from Rust. While there are a couple of Rust HTTP libraries like rust-http and its s

Valerii Hiora 2 Sep 14, 2016
Build Java applications without fighting your build tool. Drink some espresso.

Espresso Build Java applications without fighting your build tool. Drink some espresso. Features Modern Look & Feel Command line interface inspired by

Hunter LaFaille 5 Apr 2, 2024
A toolkit for building your own interactive command-line tools in Rust

promkit A toolkit for building your own interactive command-line tools in Rust, utilizing crossterm. Getting Started Put the package in your Cargo.tom

null 70 Dec 18, 2022
A bring-your-own-mutex version of once_cell.

generic_once_cell generic_once_cell is a generic no_std version of once_cell. Internal synchronization for initialization is provided as type paramete

Martin Kröning 3 Nov 28, 2022
Write your own exploit for $CASH

Cashio Exploit Workshop The Cashio hack was one of the biggest hacks occurred in Solana ecosystem which allowed the attacker to print infinite amount

Narya.ai 21 Mar 22, 2023
Create `.gitignore` files using one or more templates from TopTal, GitHub or your own collection

gitnr A cross-platform CLI utility to create .gitignore files using templates. Use templates from the GitHub & TopTal collections Use local files and

reemus.dev 25 Sep 2, 2023
A mansplainer for man commands, cheeky and rude, use at your own risk

mansplain mansplain is a Command-Line Interface (CLI) tool built in Rust that mansplains a command to you Prerequisites Rust and Cargo installed An Op

Pratik Kanthi 3 Oct 24, 2023
Gives custom drugs to your terminal 💊

Linux on drugs ?? Gives custom drugs to your terminal ?? Output random colors really fast Installation ?? Arch Linux Linux on drugs is in the AUR yay

Skwal 3 Sep 10, 2022
Coppers is a custom test harnass for Rust that measures the energy usage of your test suite.

Coppers Coppers is a test harness for Rust that can measure the evolution of power consumptions of a Rust program between different versions with the

Thijs Raymakers 175 Dec 4, 2022