diff --git a/README.md b/README.md index 217d7dd..e614f28 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/resources/about.html b/resources/about.html index 65a7174..1f3dd9d 100644 --- a/resources/about.html +++ b/resources/about.html @@ -18,7 +18,6 @@ logo.png (PNG) photo.jpg (JPEG) photo2.jpg (JPEG) - large.png (PNG ~27MB) sample.txt sample2.txt @@ -35,7 +34,7 @@

Key Features

  • Socket Programming: Built on raw TCP sockets (bind, listen, accept) for client connections
  • Multi-threading: Custom ThreadPool with bounded queue (max 100 tasks) handles concurrent requests
  • HTTP Methods: Supports GET for static files and POST for JSON uploads to /upload endpoint
  • -
  • Binary Transfer: Serves images (PNG, JPEG) and large files (>1MB) with proper Content-Type headers
  • +
  • Binary Transfer: Streams PNG and JPEG fixtures with explicit download headers
  • Keep-Alive: Connection reuse with timeout=30s and max=100 requests per connection
  • Security: Path traversal protection (blocks .., ./, ~, absolute paths) and Host header validation
  • Error Handling: Returns 503 Service Unavailable when thread pool is saturated
  • @@ -63,4 +62,4 @@

    Project Specifica - \ No newline at end of file + diff --git a/resources/contact.html b/resources/contact.html index 6bc833b..2b02446 100644 --- a/resources/contact.html +++ b/resources/contact.html @@ -18,7 +18,6 @@ logo.png (PNG) photo.jpg (JPEG) photo2.jpg (JPEG) - large.png (PNG ~27MB) sample.txt sample2.txt @@ -40,8 +39,7 @@

    Testing Examples<

    GET Requests (Static Files)

    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
    +curl -i http://127.0.0.1:8080/photo.jpg --output photo.jpg

    POST Request (Upload JSON)

    curl -i -X POST http://127.0.0.1:8080/upload \
    @@ -59,7 +57,6 @@ 

    Available Downloa @@ -82,7 +79,7 @@

    Assignment Requir
  • ✅ Multi-threaded server using socket programming
  • ✅ ThreadPool with bounded queue (503 on saturation)
  • ✅ GET and POST method support
  • -
  • ✅ Binary file transfers (images, large files)
  • +
  • ✅ Streamed binary file transfers
  • ✅ Static file serving (HTML, CSS, JS, text)
  • ✅ Path traversal attack prevention
  • ✅ Host header validation
  • diff --git a/resources/index.html b/resources/index.html index 91ef8fe..a1d16ac 100644 --- a/resources/index.html +++ b/resources/index.html @@ -18,7 +18,6 @@ logo.png (PNG) photo.jpg (JPEG) photo2.jpg (JPEG) - large.png (PNG ~27MB) sample.txt sample2.txt @@ -59,4 +58,4 @@

    Try POST /upload (JSON)

    - \ No newline at end of file + diff --git a/resources/large.png b/resources/large.png deleted file mode 100644 index 4ac8e5a..0000000 Binary files a/resources/large.png and /dev/null differ diff --git a/server.py b/server.py index b2fe807..c62c0a5 100644 --- a/server.py +++ b/server.py @@ -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 @@ -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() diff --git a/tests/test_server.py b/tests/test_server.py index ba8eb7f..257b0a3 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,6 +1,9 @@ import os import socket +import tempfile +import threading import unittest +from unittest.mock import patch import server @@ -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()