Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ curl -i --path-as-is http://127.0.0.1:8080/../etc/passwd
python3 -m unittest discover -s tests -v
```

The streaming test creates a deterministic multi-chunk payload in a temporary
directory. Large generated binaries are not stored in the repository.

## Project structure

```text
Expand Down
5 changes: 2 additions & 3 deletions resources/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<a href="/logo.png" download>logo.png (PNG)</a>
<a href="/photo.jpg" download>photo.jpg (JPEG)</a>
<a href="/photo2.jpg" download>photo2.jpg (JPEG)</a>
<a href="/large.png" download>large.png (PNG ~27MB)</a>
<a href="/sample.txt" download>sample.txt</a>
<a href="/sample2.txt" download>sample2.txt</a>
</div>
Expand All @@ -35,7 +34,7 @@ <h2 style="font-size: 20px; margin: 20px 0 10px; color: #333;">Key Features</h2>
<li><strong>Socket Programming:</strong> Built on raw TCP sockets (bind, listen, accept) for client connections</li>
<li><strong>Multi-threading:</strong> Custom ThreadPool with bounded queue (max 100 tasks) handles concurrent requests</li>
<li><strong>HTTP Methods:</strong> Supports GET for static files and POST for JSON uploads to /upload endpoint</li>
<li><strong>Binary Transfer:</strong> Serves images (PNG, JPEG) and large files (>1MB) with proper Content-Type headers</li>
<li><strong>Binary Transfer:</strong> Streams PNG and JPEG fixtures with explicit download headers</li>
<li><strong>Keep-Alive:</strong> Connection reuse with timeout=30s and max=100 requests per connection</li>
<li><strong>Security:</strong> Path traversal protection (blocks .., ./, ~, absolute paths) and Host header validation</li>
<li><strong>Error Handling:</strong> Returns 503 Service Unavailable when thread pool is saturated</li>
Expand Down Expand Up @@ -63,4 +62,4 @@ <h2 style="font-size: 20px; margin: 20px 0 10px; color: #333;">Project Specifica
</main>
<footer class="footer">Server: Multi-threaded HTTP Server</footer>
</body>
</html>
</html>
7 changes: 2 additions & 5 deletions resources/contact.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<a href="/logo.png" download>logo.png (PNG)</a>
<a href="/photo.jpg" download>photo.jpg (JPEG)</a>
<a href="/photo2.jpg" download>photo2.jpg (JPEG)</a>
<a href="/large.png" download>large.png (PNG ~27MB)</a>
<a href="/sample.txt" download>sample.txt</a>
<a href="/sample2.txt" download>sample2.txt</a>
</div>
Expand All @@ -40,8 +39,7 @@ <h2 style="font-size: 20px; margin: 20px 0 10px; color: #333;">Testing Examples<
<h3 style="font-size: 16px; margin: 15px 0 8px; color: #555;">GET Requests (Static Files)</h3>
<pre class="code">curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/sample.txt
curl -i http://127.0.0.1:8080/photo.jpg --output photo.jpg
curl -i http://127.0.0.1:8080/large.png --output large.png</pre>
curl -i http://127.0.0.1:8080/photo.jpg --output photo.jpg</pre>

<h3 style="font-size: 16px; margin: 15px 0 8px; color: #555;">POST Request (Upload JSON)</h3>
<pre class="code">curl -i -X POST http://127.0.0.1:8080/upload \
Expand All @@ -59,7 +57,6 @@ <h2 style="font-size: 20px; margin: 20px 0 10px; color: #333;">Available Downloa
<ul style="line-height: 1.8;">
<li><strong>logo.png</strong> - Small PNG image</li>
<li><strong>photo.jpg, photo2.jpg</strong> - JPEG images</li>
<li><strong>large.png</strong> - ~27MB PNG for large file transfer testing</li>
<li><strong>sample.txt, sample2.txt</strong> - Plain text files</li>
</ul>

Expand All @@ -82,7 +79,7 @@ <h2 style="font-size: 20px; margin: 20px 0 10px; color: #333;">Assignment Requir
<li>✅ Multi-threaded server using socket programming</li>
<li>✅ ThreadPool with bounded queue (503 on saturation)</li>
<li>✅ GET and POST method support</li>
<li>✅ Binary file transfers (images, large files)</li>
<li>✅ Streamed binary file transfers</li>
<li>✅ Static file serving (HTML, CSS, JS, text)</li>
<li>✅ Path traversal attack prevention</li>
<li>✅ Host header validation</li>
Expand Down
3 changes: 1 addition & 2 deletions resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
<a href="/logo.png" download>logo.png (PNG)</a>
<a href="/photo.jpg" download>photo.jpg (JPEG)</a>
<a href="/photo2.jpg" download>photo2.jpg (JPEG)</a>
<a href="/large.png" download>large.png (PNG ~27MB)</a>
<a href="/sample.txt" download>sample.txt</a>
<a href="/sample2.txt" download>sample2.txt</a>
</div>
Expand Down Expand Up @@ -59,4 +58,4 @@ <h2 class="h1" style="font-size:24px;">Try POST /upload (JSON)</h2>
<footer class="footer">Server: Multi-threaded HTTP Server</footer>
<script src="/app.js"></script>
</body>
</html>
</html>
Binary file removed resources/large.png
Binary file not shown.
11 changes: 3 additions & 8 deletions server.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
"""
Multi-threaded HTTP/1.1 server built from sockets.

HELLO SIR , I am Nipun Patel Thumu . Roll no - 10207 . I wrote the starting basic coding on my own and later generated bit of it . Tried to go through all the code and understand most of it.

"""
"""Educational HTTP/1.1 server built directly on Python sockets."""
import socket, sys, threading, os, json, random, string, queue
from datetime import datetime, timezone
from typing import Optional, Tuple, Dict
Expand Down Expand Up @@ -111,11 +106,11 @@ def main():

class ThreadPool:
"""Very small fixed-size thread pool with a bounded task queue."""
def __init__(self, max_workers: int, on_dequeued=None):
def __init__(self, max_workers: int, on_dequeued=None, queue_size: int = MAX_QUEUE_SIZE):
# I'm prestarting worker threads so incoming connections get handled immediately.
self.max_workers = max_workers
self.on_dequeued = on_dequeued # callback(thread_name, client_address)
self.tasks = queue.Queue(maxsize=MAX_QUEUE_SIZE)
self.tasks = queue.Queue(maxsize=queue_size)
self.threads = []
self._active = 0
self._lock = threading.Lock()
Expand Down
81 changes: 81 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import socket
import tempfile
import threading
import unittest
from unittest.mock import patch

import server

Expand Down Expand Up @@ -99,5 +102,83 @@ def test_response_headers_include_length_and_status(self):
self.assertIn("Connection: close", response)


class StreamingAndConcurrencyTests(unittest.TestCase):
def test_streams_a_deterministic_multi_chunk_fixture(self):
payload = b"0123456789abcdef" * 700
sender, receiver = socket.socketpair()
result = {}

try:
with tempfile.NamedTemporaryFile(suffix=".txt") as fixture:
fixture.write(payload)
fixture.flush()
headers, error = server.get_content_headers_for_path(fixture.name)

self.assertIsNone(error)

def stream_fixture():
try:
result["bytes_sent"] = server.send_file(sender, fixture.name, headers)
sender.shutdown(socket.SHUT_WR)
except Exception as exc: # Propagate worker failures below.
result["error"] = exc

stream_thread = threading.Thread(target=stream_fixture)
stream_thread.start()

response = bytearray()
while chunk := receiver.recv(4096):
response.extend(chunk)

stream_thread.join(timeout=2)
self.assertFalse(stream_thread.is_alive())
finally:
sender.close()
receiver.close()

if "error" in result:
raise result["error"]

_, response_body = bytes(response).split(b"\r\n\r\n", 1)
self.assertEqual(result["bytes_sent"], len(payload))
self.assertEqual(response_body, payload)

def test_bounded_pool_rejects_work_when_its_queue_is_full(self):
started = threading.Event()
release = threading.Event()
server_sockets = []
peer_sockets = []

def blocking_handler(client_socket, _client_address):
started.set()
release.wait(timeout=2)
client_socket.close()

try:
with patch.object(server, "handle_client", side_effect=blocking_handler):
pool = server.ThreadPool(max_workers=1, queue_size=1)

for _ in range(3):
server_socket, peer_socket = socket.socketpair()
server_sockets.append(server_socket)
peer_sockets.append(peer_socket)

self.assertTrue(pool.submit(server_sockets[0], ("local", 1)))
self.assertTrue(started.wait(timeout=1))
self.assertTrue(pool.submit(server_sockets[1], ("local", 2)))
self.assertFalse(pool.submit(server_sockets[2], ("local", 3)))

server_sockets[2].close()
release.set()
pool.tasks.join()
finally:
release.set()
for sock in server_sockets + peer_sockets:
try:
sock.close()
except OSError:
pass


if __name__ == "__main__":
unittest.main()
Loading