Skip to content
This repository was archived by the owner on Apr 1, 2025. It is now read-only.

Entrega #34

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
471a69c
add gitignore
CfM47 Feb 7, 2025
23f8596
client added
CfM47 Feb 7, 2025
4423302
added main
CfM47 Feb 7, 2025
ffaebc6
add entrypoint
CfM47 Feb 7, 2025
362eeff
fixed client
CfM47 Feb 8, 2025
4a29530
changed default port to 80
CfM47 Feb 8, 2025
847ed8f
refactored folder structure
CfM47 Feb 8, 2025
bc6098a
basic error handling for http client
Sekai02 Feb 8, 2025
542957f
Merge pull request #1 from CfM47/feat/error_handling
CfM47 Feb 9, 2025
c249b27
server skeleton added
CfM47 Feb 8, 2025
0182e56
added some mvp routes to the server
CfM47 Feb 8, 2025
5203777
connection close handler
CfM47 Feb 8, 2025
606f474
Merge pull request #2 from CfM47/feat/server
CfM47 Feb 9, 2025
478087c
support for receiving chunked bodies
Sekai02 Feb 9, 2025
82ebda3
Merge pull request #3 from CfM47/feat/chunks
Sekai02 Feb 9, 2025
95969f1
support for redirecting status
Sekai02 Feb 9, 2025
902c254
Merge pull request #4 from CfM47/feat/redirect-status
Sekai02 Feb 9, 2025
653aae4
status codes added
Sekai02 Feb 9, 2025
ad3b6e7
Merge pull request #5 from CfM47/feat/status-codes
Sekai02 Feb 9, 2025
d3747b0
server made persistent
CfM47 Feb 9, 2025
64c6977
Merge pull request #6 from CfM47/feat/persistent
CfM47 Feb 9, 2025
3edacf6
refactor: replace status codes with HTTPStatus constants
CfM47 Feb 9, 2025
3eebfbf
Merge pull request #7 from CfM47/refactor/server
CfM47 Feb 9, 2025
7304926
http methods better handling for client and some minor changes
Sekai02 Feb 9, 2025
abf2cdd
Merge pull request #8 from CfM47/feat/http-methods-client-support
Sekai02 Feb 9, 2025
b9bc6f1
better http methods handling for server
Sekai02 Feb 9, 2025
c325d26
Merge pull request #9 from CfM47/feat/http-methods-server-support
Sekai02 Feb 9, 2025
415a8cf
fixed some stuff
CfM47 Feb 10, 2025
18f9ef2
Merge pull request #10 from CfM47/fix/enums
CfM47 Feb 10, 2025
ed7392c
implementing https on client
CfM47 Feb 9, 2025
999f837
implementing https on server
CfM47 Feb 9, 2025
a7ffdd9
Merge pull request #11 from CfM47/feat/implement-https
CfM47 Feb 10, 2025
3478cc0
fixed redirection
CfM47 Feb 10, 2025
061d730
added main try catch
CfM47 Feb 10, 2025
f7a84a1
Merge pull request #12 from CfM47/fix/redirection
CfM47 Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
venv
.idea
.vscode
__pycache__
cert.pem
key.pem
28 changes: 26 additions & 2 deletions run.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
#!/bin/bash

# Replace the next shell command with the entrypoint of your solution
method=""
url=""
headers=""
data=""

echo $@
while getopts ":m:u:h:d:" opt; do
case $opt in
m) method="$OPTARG" ;;
u) url="$OPTARG" ;;
h) headers="$OPTARG" ;;
d) data="$OPTARG" ;;
\?) echo "Opción inválida: -$OPTARG" >&2
exit 1
;;
:) echo "La opción -$OPTARG requiere un argumento." >&2
exit 1
;;
esac
done

if [ -z "$method" ] || [ -z "$url" ]; then
echo "Uso: $0 -m <method> -u <url> -h <headers> -d <data>"
exit 1
fi

export PYTHONPATH=$PWD
python3 src/client/main.py -m "$method" -u "$url" -H "$headers" -d "$data"
135 changes: 135 additions & 0 deletions src/client/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import socket
import ssl
from src.grammar import httpMessage, basic_rules, httpRequest, httpResponse
from src.error.error import (
InvalidURLError, ConnectionError, RequestBuildError,
RequestSendError, ResponseReceiveError, ResponseParseError, ResponseBodyError
)
from src.status import HTTPStatus

class httpClient:
def __init__(self, url):
try:
secure, host, port, path = httpMessage.get_url_info(url)
except Exception as e:
raise InvalidURLError(f"Invalid URL: {url}", url) from e
self.secure = secure
self.host = host
self.port = port
self.url = url
self.path = path

def send_request(self, method: str, header: str, data: str, max_redirects=5):
redirect_count = 0
while redirect_count < max_redirects:
try:
req_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.secure:
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
req_socket = context.wrap_socket(req_socket, server_hostname=self.host)
req_socket.connect((self.host, self.port))
except socket.error as e:
raise ConnectionError(f"Failed to connect to {self.host}:{self.port}", self.host, self.port) from e
try:
if method == "CONNECT":
uri = f"{self.host}:{self.port}"
else:
uri = self.path
request = httpRequest.build_req(method=method, uri=uri, headers=header, body=data)
except Exception as e:
raise RequestBuildError("Failed to build request", method, uri, header, data) from e

try:
req_socket.send(request.encode())
except Exception as e:
req_socket.close()
raise RequestSendError("Failed to send request", request) from e

try:
response = self.receive_response(req_socket)
except Exception as e:
raise ResponseReceiveError("Failed to receive response") from e

if response["status"] in (HTTPStatus.MOVED_PERMANENTLY.value, HTTPStatus.FOUND.value, HTTPStatus.SEE_OTHER.value, HTTPStatus.TEMPORARY_REDIRECT.value):
redirect_count += 1
new_url = response["headers"].get("Location")
if not new_url:
raise ResponseParseError("Redirection response missing 'Location' header", response["headers"])
try:
_, self.host, self.port, self.path = httpMessage.get_url_info(new_url)
except Exception as e:
raise InvalidURLError(f"Invalid URL in 'Location' header: {new_url}", new_url) from e
if response["status"] == HTTPStatus.SEE_OTHER.value:
method = "GET"
data = ""
else:
return response
req_socket.close()

raise ResponseReceiveError("Too many redirects")

def receive_response(self, req_socket: socket.socket):
head = ""
try:
while True:
data = req_socket.recv(1)
if not data:
break
head += data.decode()
if head.endswith(basic_rules.crlf * 2):
break
except socket.error as e:
raise ResponseReceiveError("Error receiving response header") from e

try:
head_info = httpResponse.extract_head_info(head)
except Exception as e:
raise ResponseParseError("Error parsing response header", head) from e

body = ""
if "Transfer-Encoding" in head_info["headers"] and head_info["headers"]["Transfer-Encoding"] == "chunked":
try:
body = self.receive_chunked_body(req_socket)
except ResponseBodyError as e:
raise ResponseBodyError("Error receiving chunked response body", e.details) from e
elif "Content-Length" in head_info["headers"]:
try:
body = req_socket.recv(int(head_info["headers"]["Content-Length"])).decode()
except socket.error as e:
raise ResponseBodyError("Error receiving response body", head_info["headers"]["Content-Length"]) from e

return {
"status": head_info["status_code"],
"headers": head_info["headers"],
"body": body
}

def receive_chunked_body(self, req_socket: socket.socket):
body = ""
while True:
chunk_size_str = ""
while True:
try:
data = req_socket.recv(1)
if not data:
raise ResponseBodyError("Error receiving chunk size", "No data received")
chunk_size_str += data.decode()
if chunk_size_str.endswith(basic_rules.crlf):
break
except socket.error as e:
raise ResponseBodyError("Error receiving chunk size", str(e)) from e
try:
chunk_size = int(chunk_size_str.strip(), 16)
except ValueError as e:
raise ResponseBodyError("Invalid chunk size", chunk_size_str.strip()) from e
if chunk_size == 0:
break
try:
chunk_data = req_socket.recv(chunk_size).decode()
body += chunk_data
req_socket.recv(2)
except socket.error as e:
raise ResponseBodyError("Error receiving chunk data", str(e)) from e
return body
30 changes: 30 additions & 0 deletions src/client/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import argparse
from client import httpClient
import json

def parse_arguments():
parser = argparse.ArgumentParser(description='Make an http request')
parser.add_argument("-m", "--method", type=str, required=True, help="http method of the request")
parser.add_argument("-u", "--url", type=str, required=True, help="Resource url")
parser.add_argument("-H", "--headers", type=str, default="{}", help='headers of the request')
parser.add_argument("-d", "--data", type=str, default="", help="Body of the request")

args = parser.parse_args()
return {
"method": args.method.upper(),
"url": args.url,
"headers": args.headers,
"data": args.data,
}

def main():
try:
args = parse_arguments()
client = httpClient(args["url"])
response = client.send_request(method=args["method"], header=args["headers"], data=args["data"])
print(json.dumps(response, indent=4))
except Exception as e:
print(f"Fatal Error: {e}")

if __name__=="__main__":
main()
50 changes: 50 additions & 0 deletions src/error/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class HTTPClientError(Exception):
"""Base class for HTTP client errors."""
def __init__(self, message, details=None):
super().__init__(message)
self.details = details

class ConnectionError(HTTPClientError):
"""Raised when a connection error occurs."""
def __init__(self, message, host, port):
super().__init__(message)
self.host = host
self.port = port

class InvalidURLError(HTTPClientError):
"""Raised when the URL is invalid."""
def __init__(self, message, url):
super().__init__(message)
self.url = url

class RequestBuildError(HTTPClientError):
"""Raised when there is an error building the request."""
def __init__(self, message, method, uri, headers, body):
super().__init__(message)
self.method = method
self.uri = uri
self.headers = headers
self.body = body

class RequestSendError(HTTPClientError):
"""Raised when there is an error sending the request."""
def __init__(self, message, request):
super().__init__(message)
self.request = request

class ResponseReceiveError(HTTPClientError):
"""Raised when there is an error receiving the response."""
def __init__(self, message):
super().__init__(message)

class ResponseParseError(HTTPClientError):
"""Raised when there is an error parsing the response."""
def __init__(self, message, response_header):
super().__init__(message)
self.response_header = response_header

class ResponseBodyError(HTTPClientError):
"""Raised when there is an error with the response body."""
def __init__(self, message, content_length):
super().__init__(message)
self.content_length = content_length
Loading