Skip to content

Commit

Permalink
http client
Browse files Browse the repository at this point in the history
  • Loading branch information
Kibet-mutai committed Mar 25, 2024
1 parent cb68a45 commit 4d1baa2
Show file tree
Hide file tree
Showing 104 changed files with 374 additions and 0 deletions.
7 changes: 7 additions & 0 deletions http/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions http/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "http"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
93 changes: 93 additions & 0 deletions http/request.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
## Request

GET /page1.htm HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept-Language: en-US
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate
Host: example.com
Connection: Keep-Alive

##Response

HTTP/1.1 200 OK
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Fri, 14 Dec 2018 16:46:09 GMT
Etag: "1541025663+gzip"
Expires: Fri, 21 Dec 2018 16:46:09 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (ord/5730)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1270

<!doctype html>
<html>
<head>
<title>Example Domain</title>
...


"HTTP/1.1 200 OK
Accept-Ranges: bytes
Age: 43577
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Mon, 25 Mar 2024 22:06:50 GMT
Etag: \"3147526947\"
Expires: Mon, 01 Apr 2024 22:06:50 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (laa/7B49
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
Connection: close

<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset=\"utf-8\" />
<meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<style type=\"text/css\">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, \"Segoe UI\", \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}

</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href=\"https://www.iana.org/domains/example\">More information...</a></p>
</div>
</body>
</html>

"

216 changes: 216 additions & 0 deletions http/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use std::net::TcpStream;
use std::str::from_utf8;
use std::{
collections::HashMap,
io::{Read, Write},
ops::Range,
};
use std::{fmt, io};

#[derive(Debug)]
pub enum Error {
ParseUrlError,
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "URL parsing error",)
}
}

#[derive(Debug, PartialEq)]
pub struct ParsedUrl {
pub scheme: String,
pub host: String,
pub path: String,
}

impl ParsedUrl {
pub fn from(url: &str) -> Result<ParsedUrl, Error> {
let addr = if url.starts_with("http") || url.starts_with("https") {
url.to_owned()
} else {
format!("http://{}", url)
};

let mut split = addr.split("://");

let scheme = match split.next() {
Some(v) => v.to_string(),
None => return Err(Error::ParseUrlError),
};

split = match split.next() {
Some(v) => v.split("/"),
None => return Err(Error::ParseUrlError),
};

let host = match split.next() {
Some(v) => v.to_string(),
None => return Err(Error::ParseUrlError),
};

let mut path = String::new();
loop {
match split.next() {
Some(v) => path.push_str(format!("/{}", v).as_str()),
None => {
if path.is_empty() {
path.push('/');
}
break;
}
}
}

Ok(ParsedUrl { scheme, host, path })
}
}

#[derive(Debug)]
pub struct Connection {
url: ParsedUrl,
stream: TcpStream,
}

pub enum Method {
GET,
POST,
PUT,
DELETE,
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Method::GET => write!(f, "GET"),
Method::POST => write!(f, "POST"),
Method::PUT => write!(f, "PUT"),
Method::DELETE => write!(f, "DELETE"),
}
}
}

pub struct Request {
method: Method,
headers: HashMap<String, String>,
query_strings: String,
range: Option<Range<usize>>,
body: Option<Vec<u8>>,
}

impl Request {
pub fn new() -> Request {
Request {
method: Method::GET,
headers: HashMap::new(),
query_strings: String::new(),
range: None,
body: None,
}
}

pub fn set_method(mut self, method: Method) -> Request {
self.method = method;
self
}

pub fn get_method(&self) -> &Method {
&self.method
}

pub fn set_headers(mut self, headers: HashMap<String, String>) -> Request {
self.headers = headers;
self
}

pub fn get_headers(&self) -> &HashMap<String, String> {
&self.headers
}

pub fn set_range(mut self, range: Range<usize>) -> Request {
self.range = Some(range);
self
}

pub fn get_range(&self) -> &Option<Range<usize>> {
&self.range
}

pub fn get_query_strings(&self) -> &String {
&self.query_strings
}

pub fn get_body(&self) -> &Option<Vec<u8>> {
&self.body
}

pub fn get_content_length(&self) -> usize {
if let Some(body) = &self.body {
body.len()
} else {
0
}
}
}
impl Connection {
pub fn new(parse_url: &str) -> Result<Connection, Error> {
let url = ParsedUrl::from(&parse_url).unwrap();
let stream = TcpStream::connect(format!("{}:80", url.host)).unwrap();
Ok(Connection { url, stream })
}

pub fn send_request(&mut self) -> Result<(), Error> {
let mut request = Request::new();
self.stream
.write_all(format!("GET {} HTTP/1.1\r\n", self.url.path).as_bytes())
.unwrap();
self.stream
.write_all(format!("HOST: {}\r\n", self.url.host).as_bytes())
.unwrap();
for header in request.get_headers() {
self.stream
.write_all(format!("{}: {}\r\n", header.0, header.1).as_bytes())
.unwrap();
}
self.stream
.write_all(format!("Content-Length: {}\r\n", request.get_content_length()).as_bytes())
.unwrap();
if let Some(range) = request.get_range() {
self.stream
.write_all(format!("Range: bytes={}-{}\r\n", range.start, range.end).as_bytes())
.unwrap();
}

self.stream.write_all(b"Connection: Close\r\n").unwrap();
self.stream.write_all(b"\r\n").unwrap();

if let Some(body) = request.get_body() {
self.stream.write_all(body.as_slice()).unwrap();
}

self.stream.write_all(b"\r\n\r\n").unwrap();
let mut buf = String::new();
match self.stream.read_to_string(&mut buf) {
Ok(_) => {
let mut response = buf.split("/");
let scheme = response.next();
let protocol = response.next();
let status_code = response.next();
println!("Response:\n{:?}", buf);
}
Err(e) => {
println!("Failed to receive data: {}", e);
}
}
Ok(())
}
}

fn main() -> Result<(), Error> {
let url = "https://example.com";

let mut connection = Connection::new(&url).unwrap();
//println!("Connection: {:?}", connection);
connection.send_request().unwrap();
Ok(())
}
1 change: 1 addition & 0 deletions http/target/.rustc_info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rustc_fingerprint":6537163298064755573,"outputs":{"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/kibet/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.76.0 (07dca489a 2024-02-04)\nbinary: rustc\ncommit-hash: 07dca489ac2d933c78d3c5158e3f43beefeb02ce\ncommit-date: 2024-02-04\nhost: x86_64-unknown-linux-gnu\nrelease: 1.76.0\nLLVM version: 17.0.6\n","stderr":""}},"successes":{}}
3 changes: 3 additions & 0 deletions http/target/CACHEDIR.TAG
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/
Empty file added http/target/debug/.cargo-lock
Empty file.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file has an mtime of when this was started.
Loading

0 comments on commit 4d1baa2

Please sign in to comment.