Skip to content

Commit

Permalink
Implements SimpleHTTPNode
Browse files Browse the repository at this point in the history
- Implements the project skeleton and closes #2
- Implements the TCPListener and closes #3
- Implmenets the SimpleHTTPConnectionHandler and closes #4
- Implements the SimpleHTTPApplication and closes #5
- Implements the SimpleHTTPNode and closes #6
- Implements a CI action to run automated tests and closes #8
  • Loading branch information
idjaw committed Feb 9, 2024
1 parent ac57306 commit 1edbf52
Show file tree
Hide file tree
Showing 43 changed files with 2,924 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 89
35 changes: 35 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application

on:
push:
branches:
- '*'
pull_request:
branches:
- main

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.11"
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
- name: Install and run poetry
run: |
pip install poetry
poetry install
- name: Install and run tox
run: |
pip install tox
SKIP_LOAD_TEST=true tox
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
coverage/

# Translations
*.mo
Expand Down Expand Up @@ -157,4 +158,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,67 @@
# edunet
Having fun with networks!
# EduNet
This application allows you to simulate different network operations in a simple plug-in manner.

The motivation with this project was to simply learn more about sockets, protocols, and making them all talk to one another.

The end result of this project is to be able to craft together any network entities and have them communicate as intended.

## Simple Supported Use Case

There is a built-in simple supported use case called SimpleHTTPNode.

Bringing this up will start a TCP listener on localhost on port 9999 accepting valid HTTP requests.
What will be sent back are valid HTTP responses.

## Complex Use Cases

As it will be supported soon, the intent is by using the core architecture,
we will be able to implement a router with basic features like:

- Routing table
- DHCP assignment
- Packet forwarding
- Network Access Translation (NAT)

## Simple Example to put it all together

### Installing
```shell
pip install edunet
```

### Usage
Going back to the Simple use case, as is you can simply do something like:

#### Starting service

```python
from edunet.core.applications.simple_http_application import SimpleHTTPApplication
from edunet.core.networking.handlers.simple_http_connection_handler import SimpleHTTPConnectionHandler
from edunet.core.networking.listeners.tcp_listener import TCPListener

# create an HTTP application
app = SimpleHTTPApplication()

# create a Connection Handler that takes an application
handler = SimpleHTTPConnectionHandler(app)

# A listener is needed to allow connections to come through
listener = TCPListener("127.0.0.1", 9999, handler)

listener.start()
```

#### Calling service
You can now communicate with it over HTTP by any means

### curl
```shell
curl -X GET http://127.0.0.1:9999
```

### python
```python
import urllib.request
response = urllib.request.urlopen("http://localhost:9999")
print(response.read().decode("utf-8"))
```
639 changes: 639 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[tool.poetry]
name = "edunet"
version = "0.1.0"
description = "Simple Network Simulation For The Funs!"
authors = ["Wajdi Al-Hawari <[email protected]>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "~3.11"
scapy = "^2.5.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.4"
pytest-asyncio = "^0.23.4"
coverage = "^7.4.1"
pytest-cov = "^4.1.0"
mypy = "^1.8.0"
black = "^24.1.1"
isort = "^5.13.2"
flake8 = "^7.0.0"
pylint = "^3.0.3"
tox = "^4.12.1"
hypothesis = "^6.92.9"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
pythonpath = src/edunet
35 changes: 35 additions & 0 deletions simple_http_server_multi_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import asyncio
import urllib.request
import uuid


async def send_request_and_collect_response(identifier):
url = 'http://localhost:9999'
headers = {'X-UUID': str(identifier)} # Include the UUID in the request headers

with urllib.request.urlopen(urllib.request.Request(url, headers=headers)) as response:
# Collect and return the response along with the identifier and response UUID
response_data = response.read().decode('utf-8')
print(f"Received response for identifier {identifier}: {response_data}")
return identifier, response_data


async def main():
identifiers = [uuid.uuid4() for _ in range(5000)]
expected_responses = {} # Dictionary to store expected responses with identifiers

# Send concurrent requests using asyncio.gather
tasks = [send_request_and_collect_response(identifier) for identifier in identifiers]
responses = await asyncio.gather(*tasks)

# Update expected_responses dictionary with the collected responses
print(responses)
for identifier, response in responses:
expected_responses[identifier] = response

# Perform assertions to ensure each client received the correct response
for identifier, expected_response in expected_responses.items():
# Assert that the received response contains the UUID
assert str(identifier) in expected_response, f"UUID mismatch for identifier {identifier}"

asyncio.run(main())
22 changes: 22 additions & 0 deletions simple_http_tester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import logging

from core.applications.simple_http_application import SimpleHTTPApplication
from core.networking.handlers.simple_http_connection_handler import (
SimpleHTTPConnectionHandler,
)
from core.networking.listeners.tcp_listener import TCPListener
from core.nodes.simple_http_node import SimpleHTTPNode

if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
# Instantiate an HTTP application
http_application = SimpleHTTPApplication()
# Wrap the HTTP application with a Connection Handler
http_connection_handler = SimpleHTTPConnectionHandler(http_application)
# Wrap the Connection handler with a TCP Listener
tcp_server = TCPListener("127.0.0.1", 9999, http_connection_handler)

http_node = SimpleHTTPNode(tcp_server)

# Start the listener to start allowing communication to your application
http_node.start()
45 changes: 45 additions & 0 deletions simple_sync_test_for_http_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import socket
import threading
from time import sleep

error_count = 0


def send_requests(call_number):
global error_count
# Connect to the server
server_address = ('127.0.0.1', 9999)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(server_address)

try:
# Send a simple HTTP GET request
request = b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: " + str(call_number).encode() + b"\r\n\r\n"
client_socket.sendall(request)

# Receive and print the server's response
response = client_socket.recv(4096)
print("Received response:", response.decode())
except Exception as e:
print(f"fail {e}")
error_count += 1
finally:
# Close the client socket
client_socket.close()

# Number of concurrent connections to create
num_connections = 500

# Create threads to send requests concurrently
threads = []
for i in range(num_connections):
#sleep(0.08)
thread = threading.Thread(target=send_requests, args=(i,))
thread.start()
threads.append(thread)

# Wait for all threads to finish
for thread in threads:
thread.join()

print(error_count)
27 changes: 27 additions & 0 deletions src/edunet/core/applications/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from abc import abstractmethod, ABC

from models.base_types import Request, Response


class Application(ABC):
"""
Base Application implementation for any type of application that simply handles
request and responses
class MyApplication(Application):
def handle_request(self, request):
# implementation using request object that might be of type HTTP, TCP, UDP
return some_response
"""

@abstractmethod
def handle_request(self, request_data: Request) -> Response:
"""
Handling the request intended coming from a ConnectionHandler implementation
e.g.
class HTTPApplication(Application):
...
def handle_request(self, foo, bar):
self.custom_logic(foo, bar)
"""
33 changes: 33 additions & 0 deletions src/edunet/core/applications/simple_http_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import logging

from core.applications.application import Application
from models.http import HTTPRequest, HTTPResponse

logger = logging.getLogger(__name__)


class SimpleHTTPApplication(Application):

def handle_request(self, request_data: HTTPRequest) -> HTTPResponse:
"""
Provide a HTTPRequest object to receive a HTTPResponse object.
"""

logger.info(f"Request data: {request_data}")

try:
return HTTPResponse(
status_code=200,
status_text="OK",
body=f"Message received: {request_data}",
)
except (SyntaxError, TypeError, ValueError, AttributeError) as e:
logger.error(f"Could not construct data: {e}")
return HTTPResponse(
status_code=500, status_text="Internal Server Error", body=""
)
except Exception as e:
logger.exception(f"Unexpected server error: {e}")
return HTTPResponse(
status_code=500, status_text="Internal Server Error", body=""
)
21 changes: 21 additions & 0 deletions src/edunet/core/networking/handlers/connection_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from abc import ABC, abstractmethod


class ConnectionHandler(ABC):
"""
Interface for implementing a connection handler used by the TCPListener
"""

@abstractmethod
def handle_connection(self, *args, **kwargs):
"""
This provides the interface that has to be implemented that will be used by the
TCPListener. The main idea is that a connection handler is provided to the
TCPListener to handle the socket data as at comes in.
As an example, you can implement a wsgi handler to handle connections.
class WSGIHandler(ConnectionHandler):
def handle_connection(foo, bar):
pass
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import logging
import socket

from core.applications.application import Application
from core.networking.handlers.connection_handler import ConnectionHandler
from models.http import HTTPRequest

logger = logging.getLogger(__name__)


class SimpleHTTPConnectionHandler(ConnectionHandler):
def __init__(self, application: Application):
self.application = application

def handle_connection(self, data: bytes, client_socket: socket.socket) -> bytes:
return self.application.handle_request(HTTPRequest.from_bytes(data)).to_bytes()
8 changes: 8 additions & 0 deletions src/edunet/core/networking/handlers/wsgi_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import socket

from core.networking.handlers.connection_handler import ConnectionHandler


class WSGIHandler(ConnectionHandler):
def handle_connection(self, client_socket: socket.socket):
pass
Loading

0 comments on commit 1edbf52

Please sign in to comment.