Learning A Language By Learning To Speak To Machines
Sometimes it is curiosity, and sometimes it is just because the buddy next to you said “hey have you heard of [insert programming language here], it is the future and you should learn it.” You decide “why not, let's learn a new programming language” or maybe you have a list of languages that you just want to learn. There are many reasons to learn a language but I find that the best way is by building a few projects to talk to other applications.
In my case, I decided it was finally time to check another language off the list and learn Rust. Since I will primarily use the language for security testing, I decided that the best project to start with was to build an API server and a client application to interact with it. It will provide some reusable stub code to talk to various applications and services or building proof of concept code in future.
If you wish to follow along, the rest of this blog will just require both Rust and Rustup installed. Documentation on how to do that can be found here: https://www.rust-lang.org/tools/install I will also include the references I used while building out this code.
Learning to say hello
Before starting to make these applications, the first step will be to set up a build environment by creating a few template files for Rust, namely a Cargo.toml and a Makefile. More information about what goes into the Cargo.toml file can be found here: https://doc.rust-lang.org/edition-guide/editions/creating-a-new-project.html.
The only non-standard addition to the Cargo.toml file I added is including the package Clap (https://docs.rs/clap/latest/clap/) as a dependency. Similar to argparse in Python, this will give us the ability to create help menus. The fastest way to get the initial setup is to run the command “cargo new hello” which will give you a project directory that looks like this:
hello ├── Cargo.toml └── src └── main.rs |
This will create a good starting point but will require one modification to the Cargo.toml so that the Clap dependency can be implemented. That will give you a Cargo.toml file that looks similar to the values below:
Cargo.toml
[package] name = "hello" version = "0.1.0" edition = "2021"
[dependencies] clap = { version = "4.5.4", features = ["derive"] } |
Once that change is made, create a Makefile including the following lines. This is not really necessary for small applications but can be useful if you want to set up builds in a specific way, clean up your project directory or create an easier means of cross compiling code for different operating systems in the future. I created a basic Makefile including the following lines to build and clean up the project. The command “cargo clean” will do the same but I find that sometimes it leaves a few files behind like Cargo.lock.
Makefile
build: cargo build --release
clean: rm Cargo.lock
all: build |
Since I plan to use Clap for help menus, I chose to modify the main.rs file within the src folder to the code below to create a starting template to modify and build into other applications. For references on Clap:
https://docs.rs/clap/latest/clap/
After modifying the base “hello world” example that was created in main.rs for the initialized project I ended up with this code which allows for command line arguments and building simple functions:
main.rs
use clap::Parser;
#[derive(Parser, Debug)] #[command(arg_required_else_help(true))] struct Args { #[clap(short, long)] name: String, }
fn greet(name: String) { println!("hello {}", name) }
fn main() { let args = Args::parse(); greet(args.name); } |
Running the command “make” within that project folder will create a binary to run the application with command line arguments and verify that Rust is working as intended before moving onto creating servers and client applications.
$ make cargo build --release Updating crates.io index Compiling proc-macro2 v1.0.86 Compiling unicode-ident v1.0.12 Compiling utf8parse v0.2.2 Compiling is_terminal_polyfill v1.70.0 Compiling anstyle v1.0.7 Compiling anstyle-query v1.1.0 Compiling colorchoice v1.0.1 Compiling clap_lex v0.7.1 Compiling anstyle-parse v0.2.4 Compiling strsim v0.11.1 Compiling heck v0.5.0 Compiling anstream v0.6.14 Compiling clap_builder v4.5.7 Compiling quote v1.0.36 Compiling syn v2.0.68 Compiling clap_derive v4.5.5 Compiling clap v4.5.7 Compiling hello v0.1.0 Finished `release` profile [optimized] target(s) in 9.16s |
After executing the binary, we now see the help menu is working and it returns the argument names we provided it.
$ ./target/release/hello Usage: hello --name <NAME>
Options: -n, --name <NAME> -h, --help Print help hello rust |
Building an API server
To start building the API server, I determined that it needed to have the ability to support logging, a POST and GET request, and returning JSON objects. I decided to build the server using Actix since the code looked easier to read at a glance. I ended up using the following references to build out the “hello world” API server:
https://actix.rs/docs/server
https://actix.rs/docs/request
https://actix.rs/docs/response
https://actix.rs/docs/middleware
https://docs.rs/actix-web/latest/actix_web/middleware/struct.Logger.html
I created the following code for my API server utilizing Clap to allow for arbitrarily setting the port:
server.rs
use clap::Parser; use actix_web::{get, post, APP, HTTPServer, web,middleware::Logger, Responder, Result}; use serde::Serialize; use serde::Deserialize; use env_logger::Env;
// argument declaration #[derive(Parser, Debug)] #[command(arg_required_else_help(true))] struct Args { #[clap(short, long)] port: u16, }
// JSON object Serialization #[derive(Serialize)] struct MyObj { name: String, }
// JSON object Deserialization #[derive(Deserialize)] struct Info { name: String, }
// GET request taking URL and using values #[get("/{name}")] async fn index(name: web::Path<String>) -> Result<impl Responder> { let obj = MyObj { name: name.to_string(), }; Ok(web::Json(obj)) }
// POST request ingesting and returning JSON objects #[post("/hello")] async fn echo(info: web::Json<Info>) -> Result<String> { Ok(format!("Welcome {}!", info.name)) }
// main function #[actix_web::main] async fn main() -> std::io::Result<()> { // imports arguments let args = Args::parse();
// enables logging to the console env_logger::init_from_env(Env::default() .default_filter_or("info"));
// sets up the web server, hosted API calls and logging HttpServer::new(|| App::new() .service(index) .service(echo) .wrap(Logger::default())) .bind(("127.0.0.1", args.port))? .run() .await } |
This also required me to modify the Cargo.toml since I now had to include the new dependencies. Also after changing main.rs to server.rs I decided that I would make both the client and the server within the same project, so I declared two separate binaries within the Cargo.toml file to be generated:
Cargo.toml
[package] name = "hello" version = "0.1.0" edition = "2021"
[dependencies] clap = { version = "4.5.4", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[[bin]] name = "server" path = "src/server.rs"
[[bin]] name = "client" path = "src/client.rs" |
This code runs on port 8080 and also logs when requests are made to the server. After doing a quick curl request to the server, we see it is successfully logging requests:
Curl command executed:
$ curl -v http://127.0.0.1:8080/rust * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 > GET /rust HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/8.8.0 > Accept: */* > * Request completely sent off < HTTP/1.1 200 OK < content-length: 15 < content-type: application/json < date: Wed, 26 Jun 2024 15:55:40 GMT < * Connection #0 to host 127.0.0.1 left intact {"name":"rust"}% |
Response on the web server:
$ ./target/release/server -p 8080 [2024-06-26T15:53:11Z INFO actix_server::builder] starting 8 workers [2024-06-26T15:53:11Z INFO actix_server::server] Actix runtime found; starting in Actix runtime |
Talking to the API Server
Now that the API server is created, I moved onto building out the client application to talk to the API. For that I decided to use reqwest. Here are some links to documentation including a blog which did a great breakdown of each of the API methods:
https://docs.rs/reqwest/latest/reqwest/#modules
https://medium.com/@carlosmarcano2704/building-a-http-client-with-reqwest-rust-c049cbe4bc2b
After doing some modifications to the “hello world” Rust file, I ended up with this for the client code:
client.rs
use clap::Parser; use reqwest::Error;
// argument declaration #[derive(Parser, Debug)] #[command(arg_required_else_help(true))] struct Args { #[clap(short, long)] method: String,
#[clap(short, long)] url: String }
// GET request function async fn get_request(url: String) -> Result<(), Error> { let response = reqwest::get(url+"/rust").await?; println!("Status: {}", response.status());
let body = response.text().await?; println!("Body:\n{}", body);
Ok(()) }
// POST request function async fn post_request(url: String) -> Result<(), Error> { let client = reqwest::Client::new();
let response = client .post(url+"/hello") .header("Content-Type","application/json") .body(r#"{"name":"test-post"}"#) .send() .await?;
println!("Status: {}", response.status());
let body = response.text().await?; println!("Body:\n{}", body);
Ok(()) }
// main function #[tokio::main] async fn main() -> Result<(), Error> { // imports arguments let args = Args::parse();
// sends a GET or POST request based on method flag if args.method == "get" { get_request(args.url).await?; } else if args.method == "post" { post_request(args.url).await?; }
Ok(()) } |
Compiling the code and verifying that it worked, I now have a working client to talk to the API server I created.
Client binary being executed:
$ ./target/release/client -m get -u "http://127.0.0.1:8080" Status: 200 OK Body: {"name":"rust"} Status: 200 OK Body: Welcome test-post! |
Server receiving the request:
$ ./target/release/server -p 8080 [2024-06-26T15:56:34Z INFO actix_server::builder] starting 8 workers [2024-06-26T15:56:34Z INFO actix_server::server] Actix runtime found; starting in Actix runtime [2024-06-26T15:56:52Z INFO actix_web::middleware::logger] 127.0.0.1 "GET /rust HTTP/1.1" 200 15 "-" "-" 0.000071 [2024-06-26T15:56:55Z INFO actix_web::middleware::logger] 127.0.0.1 "POST /hello HTTP/1.1" 200 18 "-" "-" 0.000055 |
1.
Final Thoughts
I find that building small projects, even if they are simple, help build fundamental understanding in a language and create a springboard into building more complex applications moving forward. For example, the above code could be leveraged to simulate an authentication flow to an application or even be used to interact with another API service to facilitate other tasks. Overall, I find it is always fun to try something new while also creating a personal library of reusable code. I hope this is helpful for others, especially for those trying to answer the question of what to do when starting a new language or even asking how to start building new tools, because you might find a new tool is easier to create when you understand the pieces that go into making them. Have fun coding out there!
MORE FROM OUR TECHNICAL BLOG
Cyber Advisors specializes in providing fully customizable cyber security solutions & services. Our knowledgeable, highly skilled, talented security experts are here to help design, deliver, implement, manage, monitor, put your defenses to the test, & strengthen your systems - so you don’t have to.