patx/micropie
add multipart parser
Commit 76df4ad · patx · 2025-02-01T09:59:32-05:00
Comments
No comments yet.
Diff
diff --git a/MicroPie.py b/MicroPie.py
index 2609c50..d505062 100644
--- a/MicroPie.py
+++ b/MicroPie.py
@@ -30,15 +30,19 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
+import asyncio
import inspect
import mimetypes
import os
+import re
import time
from typing import Optional, Dict, Any, Union, Tuple, List
from urllib.parse import parse_qs
import uuid
import contextvars
+from multipart import PushMultipartParser, MultipartSegment
+
try:
from jinja2 import Environment, FileSystemLoader
JINJA_INSTALLED = True
@@ -127,7 +131,23 @@ class Server:
break
content_type = headers_dict.get("content-type", "")
if "multipart/form-data" in content_type:
- self._parse_multipart(bytes(body_data), content_type, request)
+ # Extract the boundary from the Content-Type header
+ match = re.search(r'boundary=([^;]+)', content_type)
+ if not match:
+ await self._send_response(
+ send, status_code=400,
+ body="400 Bad Request: Boundary not found in Content-Type header"
+ )
+ return
+ boundary = match.group(1).encode("utf-8") # Convert boundary to bytes
+
+ # Create a StreamReader and feed it the body data
+ reader = asyncio.StreamReader()
+ reader.feed_data(body_data)
+ reader.feed_eof()
+
+ # Now call _parse_multipart with the reader and the boundary
+ await self._parse_multipart(reader, boundary)
else:
body_str = body_data.decode("utf-8", "ignore")
request.body_params = parse_qs(body_str)
@@ -214,67 +234,86 @@ class Server:
cookies[k] = v
return cookies
- def _parse_multipart(self, body: bytes, content_type: str, request: Request) -> None:
- boundary = None
- parts = content_type.split(";")
- for part in parts:
- part = part.strip()
- if part.startswith("boundary="):
- boundary = part.split("=", 1)[1]
- break
-
- if not boundary:
- raise ValueError("Boundary not found in Content-Type header.")
-
- boundary_bytes = boundary.encode("utf-8")
- delimiter = b"--" + boundary_bytes
- sections = body.split(delimiter)
- for section in sections:
- if not section or section in (b"--", b"--\r\n"):
- continue
- if section.startswith(b"\r\n"):
- section = section[2:]
- if section.endswith(b"\r\n"):
- section = section[:-2]
- if section == b"--":
- continue
-
- try:
- headers, content = section.split(b"\r\n\r\n", 1)
- except ValueError:
- continue
-
- headers_list = headers.decode("utf-8", "ignore").split("\r\n")
- header_dict = {}
- for header_line in headers_list:
- if ":" in header_line:
- key, value = header_line.split(":", 1)
- header_dict[key.strip().lower()] = value.strip()
-
- disposition = header_dict.get("content-disposition", "")
- disposition_parts = disposition.split(";")
- disposition_dict = {}
- for disp_part in disposition_parts:
- if "=" in disp_part:
- k, v = disp_part.strip().split("=", 1)
- disposition_dict[k] = v.strip('"')
-
- name = disposition_dict.get("name")
- filename = disposition_dict.get("filename")
-
- if filename:
- file_content_type = header_dict.get("content-type", "application/octet-stream")
- request.files[name] = {
- "filename": filename,
- "content_type": file_content_type,
- "data": content
- }
- elif name:
- value = content.decode("utf-8", "ignore")
- if name in request.body_params:
- request.body_params[name].append(value)
- else:
- request.body_params[name] = [value]
+ async def _parse_multipart(self, reader: asyncio.StreamReader, boundary: bytes):
+ """
+ Demonstrates handling multipart/form-data in a more streaming-friendly manner.
+ For large files, data is written to disk instead of stored in memory.
+ """
+ with PushMultipartParser(boundary) as parser:
+ current_field_name = None
+ current_filename = None
+ current_content_type = None
+ current_file = None # File handle for streaming writes
+ form_value = ""
+
+ # Directory for storing uploaded files:
+ # Adjust if you want a different path or a dynamic approach inside your app.
+ upload_directory = "uploads"
+
+ # Ensure the directory exists
+ os.makedirs(upload_directory, exist_ok=True)
+
+ while not parser.closed:
+ # Read data in chunks from the request stream
+ chunk = await reader.read(65536) # 64KB per read
+ for result in parser.parse(chunk):
+ if isinstance(result, MultipartSegment):
+ # We have a new part: form field or file
+ current_field_name = result.name
+ current_filename = result.filename
+ current_content_type = None
+ form_value = ""
+
+ # Parse content-type if present
+ for header, value in result.headerlist:
+ if header.lower() == "content-type":
+ current_content_type = value
+
+ # If it's a file, open a file handle right away
+ if current_filename:
+ safe_filename = f"{uuid.uuid4()}_{current_filename}"
+ file_path = os.path.join(upload_directory, safe_filename)
+ current_file = open(file_path, "wb")
+
+ # Otherwise, treat it as a field (string value).
+ else:
+ if current_field_name not in self.request.body_params:
+ self.request.body_params[current_field_name] = []
+
+ elif result:
+ # This chunk is body data for the current part
+ if current_file:
+ # If it's a file, write directly to disk
+ current_file.write(result)
+ else:
+ # It's a form field chunk
+ form_value += result.decode("utf-8", "ignore")
+ else:
+ # End of this part
+ if current_file:
+ # Close out the file if we're done writing it
+ current_file.close()
+ current_file = None
+
+ # Store reference in self.request.files so the upload handler can use it
+ # Example structure includes just filename and content type;
+ # no in-memory data, since we wrote it to disk.
+ if current_field_name:
+ self.request.files[current_field_name] = {
+ "filename": current_filename,
+ "content_type": current_content_type or "application/octet-stream",
+ "saved_path": os.path.join(upload_directory, safe_filename),
+ }
+ else:
+ # If it was a form field, add the form value to body_params
+ if current_field_name:
+ self.request.body_params[current_field_name].append(form_value)
+
+ # Reset for the next part
+ current_field_name = None
+ current_filename = None
+ current_content_type = None
+ form_value = ""
async def _send_response(
self,
diff --git a/examples/file_uploads_developmental/app.py b/examples/file_uploads/app.py
similarity index 57%
rename from examples/file_uploads_developmental/app.py
rename to examples/file_uploads/app.py
index 180b3dd..e4c9569 100644
--- a/examples/file_uploads_developmental/app.py
+++ b/examples/file_uploads/app.py
@@ -21,18 +21,20 @@ class FileUploadApp(Server):
</body>
</html>"""
- async def upload(self, file: Any):
- """Handles file uploads."""
- if isinstance(file, dict) and "filename" in file and "data" in file:
+ async def upload(self, file):
+ # Check for streaming-based attributes:
+ if (isinstance(file, dict)
+ and "filename" in file
+ and "saved_path" in file):
filename = file["filename"]
- file_data = file["data"]
+ saved_path = file["saved_path"]
- file_path = os.path.join(UPLOAD_DIR, filename)
- with open(file_path, "wb") as f:
- f.write(file_data)
+ # Optionally, rename the file or do further checks.
+ # For instance, you might want to store an original name in your DB or process the file.
+ return f"File '{filename}' uploaded successfully, saved to: {saved_path}!"
- return f"File '{filename}' uploaded successfully!"
- return "No file uploaded.", 400
+ # If file data is missing or doesn't match expected structure, return an error.
+ return 400, "No file uploaded."
# Run the ASGI app
app = FileUploadApp()
diff --git a/examples/pastebin/app.py b/examples/pastebin/app.py
index b7f51e3..bf133d9 100644
--- a/examples/pastebin/app.py
+++ b/examples/pastebin/app.py
@@ -14,21 +14,20 @@ db = PickleDB('pastes.db')
class Root(Server):
async def index(self):
- # Check the HTTP method from the ASGI scope
if self.request.method == "POST":
paste_content = self.request.body_params.get('paste_content', [''])[0]
pid = str(uuid4())
db.set(pid, escape(paste_content))
db.save()
- return self.redirect(f'/paste/{pid}')
- return await self.render_template('index.html')
+ return self._redirect(f'/paste/{pid}')
+ return await self._render_template('index.html')
async def paste(self, paste_id, delete=None):
if delete == 'delete':
db.remove(paste_id)
db.save()
- return self.redirect('/')
- return await self.render_template(
+ return self._redirect('/')
+ return await self._render_template(
'paste.html',
paste_id=paste_id,
paste_content=db.get(paste_id)
diff --git a/examples/socketio/chatroom.py b/examples/socketio/chatroom.py
index eca009c..c660578 100644
--- a/examples/socketio/chatroom.py
+++ b/examples/socketio/chatroom.py
@@ -6,37 +6,8 @@ sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") # Allow
# Create the MicroPie server
class MyApp(Server):
- def index(self):
- return """
- <html>
- <head>
- <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
- <script>
- var socket = io("http://localhost:8000");
- socket.on("connect", function() {
- console.log("Connected to Socket.IO server");
- });
- socket.on("message", function(data) {
- document.getElementById("output").innerHTML += data + "<br>";
- });
- function sendMessage() {
- var message = document.getElementById("message").value;
- socket.send(message);
- document.getElementById("message").value = ""; // Clear input after sending
- }
- window.onbeforeunload = function() {
- socket.disconnect();
- };
- </script>
- </head>
- <body>
- <h1>Socket.IO Chat</h1>
- <input type="text" id="message" placeholder="Type a message">
- <button onclick="sendMessage()">Send</button>
- <div id="output"></div>
- </body>
- </html>
- """
+ async def index(self):
+ return await self._render_template("chat.html")
# Socket.IO event handlers
@sio.event
@@ -57,4 +28,4 @@ async def message(sid, data):
# Attach Socket.IO to the ASGI app
asgi_app = MyApp()
-app = socketio.ASGIApp(sio, app)
+app = socketio.ASGIApp(sio, asgi_app)
diff --git a/examples/socketio/templates/chat.html b/examples/socketio/templates/chat.html
new file mode 100644
index 0000000..ecb11f6
--- /dev/null
+++ b/examples/socketio/templates/chat.html
@@ -0,0 +1,28 @@
+<html>
+<head>
+<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
+<script>
+var socket = io("http://localhost:8000");
+socket.on("connect", function() {
+ console.log("Connected to Socket.IO server");
+});
+socket.on("message", function(data) {
+ document.getElementById("output").innerHTML += data + "<br>";
+});
+function sendMessage() {
+ var message = document.getElementById("message").value;
+ socket.send(message);
+ document.getElementById("message").value = ""; // Clear input after sending
+}
+window.onbeforeunload = function() {
+ socket.disconnect();
+};
+</script>
+</head>
+ <body>
+ <h1>Socket.IO Chat</h1>
+ <input type="text" id="message" placeholder="Type a message">
+ <button onclick="sendMessage()">Send</button>
+ <div id="output"></div>
+ </body>
+</html>
diff --git a/examples/socketio/templates/index_stream.html b/examples/socketio/templates/index.html
similarity index 100%
rename from examples/socketio/templates/index_stream.html
rename to examples/socketio/templates/index.html
diff --git a/examples/file_uploads_developmental/MicroPie.py b/examples/socketio/webtrc/MicroPie.py
similarity index 67%
rename from examples/file_uploads_developmental/MicroPie.py
rename to examples/socketio/webtrc/MicroPie.py
index 0bea053..d505062 100644
--- a/examples/file_uploads_developmental/MicroPie.py
+++ b/examples/socketio/webtrc/MicroPie.py
@@ -30,20 +30,18 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
+import asyncio
import inspect
import mimetypes
import os
+import re
import time
from typing import Optional, Dict, Any, Union, Tuple, List
from urllib.parse import parse_qs
import uuid
import contextvars
-try:
- from multipart import PushMultipartParser, MultipartSegment
- MULTIP_INSTALLED = True
-except ImportError:
- MULTIP_INSTALLED = False
+from multipart import PushMultipartParser, MultipartSegment
try:
from jinja2 import Environment, FileSystemLoader
@@ -79,9 +77,9 @@ class Server:
return current_request.get()
async def __call__(self, scope, receive, send):
- await self.asgi_app(scope, receive, send)
+ await self._asgi_app(scope, receive, send)
- async def asgi_app(self, scope: Dict[str, Any], receive: Any, send: Any) -> None:
+ async def _asgi_app(self, scope: Dict[str, Any], receive: Any, send: Any) -> None:
"""ASGI application entrypoint for both HTTP and WebSockets."""
if scope["type"] == "http":
request = Request(scope)
@@ -133,10 +131,23 @@ class Server:
break
content_type = headers_dict.get("content-type", "")
if "multipart/form-data" in content_type:
- if MULTIP_INSTALLED:
- await self._parse_multipart(receive, content_type, request)
- else:
- print('Library multipart required in order to parse multipart form data/file uploads')
+ # Extract the boundary from the Content-Type header
+ match = re.search(r'boundary=([^;]+)', content_type)
+ if not match:
+ await self._send_response(
+ send, status_code=400,
+ body="400 Bad Request: Boundary not found in Content-Type header"
+ )
+ return
+ boundary = match.group(1).encode("utf-8") # Convert boundary to bytes
+
+ # Create a StreamReader and feed it the body data
+ reader = asyncio.StreamReader()
+ reader.feed_data(body_data)
+ reader.feed_eof()
+
+ # Now call _parse_multipart with the reader and the boundary
+ await self._parse_multipart(reader, boundary)
else:
body_str = body_data.decode("utf-8", "ignore")
request.body_params = parse_qs(body_str)
@@ -223,80 +234,86 @@ class Server:
cookies[k] = v
return cookies
- def _parse_multipart(self, body: bytes, content_type: str, request: Request) -> None:
- boundary = None
- parts = content_type.split(";")
- for part in parts:
- part = part.strip()
- if part.startswith("boundary="):
- boundary = part.split("=", 1)[1]
- break
-
- if not boundary:
- raise ValueError("Boundary not found in Content-Type header.")
-
- async def _parse_multipart(self, receive, content_type: str, request: Request):
- """Parses multipart form data using PushMultipartParser."""
-
- # Extract boundary from content_type
- boundary = None
- parts = content_type.split(";")
- for part in parts:
- part = part.strip()
- if part.startswith("boundary="):
- boundary = part.split("=", 1)[1]
- break
-
- if not boundary:
- raise ValueError("Boundary not found in Content-Type header.")
-
- # Initialize parser
- parser = PushMultipartParser(boundary)
- field_name = None
- file_data = None
-
- while not parser.closed:
- # Read next chunk from ASGI receive function
- msg = await receive()
- if msg["type"] == "http.request":
- chunk = msg.get("body", b"")
-
+ async def _parse_multipart(self, reader: asyncio.StreamReader, boundary: bytes):
+ """
+ Demonstrates handling multipart/form-data in a more streaming-friendly manner.
+ For large files, data is written to disk instead of stored in memory.
+ """
+ with PushMultipartParser(boundary) as parser:
+ current_field_name = None
+ current_filename = None
+ current_content_type = None
+ current_file = None # File handle for streaming writes
+ form_value = ""
+
+ # Directory for storing uploaded files:
+ # Adjust if you want a different path or a dynamic approach inside your app.
+ upload_directory = "uploads"
+
+ # Ensure the directory exists
+ os.makedirs(upload_directory, exist_ok=True)
+
+ while not parser.closed:
+ # Read data in chunks from the request stream
+ chunk = await reader.read(65536) # 64KB per read
for result in parser.parse(chunk):
if isinstance(result, MultipartSegment):
- # Start of a new multipart segment
- field_name = result.name
- filename = result.filename
-
- if filename:
- # It's a file upload
- content_type = result.content_type
- file_data = bytearray()
- request.files[field_name] = {
- "filename": filename,
- "content_type": content_type,
- "data": file_data
- }
+ # We have a new part: form field or file
+ current_field_name = result.name
+ current_filename = result.filename
+ current_content_type = None
+ form_value = ""
+
+ # Parse content-type if present
+ for header, value in result.headerlist:
+ if header.lower() == "content-type":
+ current_content_type = value
+
+ # If it's a file, open a file handle right away
+ if current_filename:
+ safe_filename = f"{uuid.uuid4()}_{current_filename}"
+ file_path = os.path.join(upload_directory, safe_filename)
+ current_file = open(file_path, "wb")
+
+ # Otherwise, treat it as a field (string value).
else:
- # It's a normal form field
- request.body_params[field_name] = ""
-
- elif isinstance(result, bytearray):
- # This is part of the file or form field data
- if field_name in request.files:
- request.files[field_name]["data"].extend(result)
+ if current_field_name not in self.request.body_params:
+ self.request.body_params[current_field_name] = []
+
+ elif result:
+ # This chunk is body data for the current part
+ if current_file:
+ # If it's a file, write directly to disk
+ current_file.write(result)
else:
- request.body_params[field_name] += result.decode("utf-8", "ignore")
-
- elif result is None:
- # End of a segment, finalize file content if present
- if field_name in request.files:
- request.files[field_name]["data"] = bytes(request.files[field_name]["data"])
-
- # Stop if there's no more body content
- if not msg.get("more_body", False):
- break
-
+ # It's a form field chunk
+ form_value += result.decode("utf-8", "ignore")
+ else:
+ # End of this part
+ if current_file:
+ # Close out the file if we're done writing it
+ current_file.close()
+ current_file = None
+
+ # Store reference in self.request.files so the upload handler can use it
+ # Example structure includes just filename and content type;
+ # no in-memory data, since we wrote it to disk.
+ if current_field_name:
+ self.request.files[current_field_name] = {
+ "filename": current_filename,
+ "content_type": current_content_type or "application/octet-stream",
+ "saved_path": os.path.join(upload_directory, safe_filename),
+ }
+ else:
+ # If it was a form field, add the form value to body_params
+ if current_field_name:
+ self.request.body_params[current_field_name].append(form_value)
+ # Reset for the next part
+ current_field_name = None
+ current_filename = None
+ current_content_type = None
+ form_value = ""
async def _send_response(
self,
@@ -317,20 +334,27 @@ class Server:
404: "404 Not Found",
500: "500 Internal Server Error",
}
- # Fallback if not in map
status_text = status_map.get(status_code, f"{status_code} OK")
- # Ensure there's a Content-Type unless already provided
- has_content_type = any(h[0].lower() == "content-type" for h in extra_headers)
+ # Ensure extra headers are safe
+ sanitized_headers = []
+ for k, v in extra_headers:
+ if "\n" in k or "\r" in k or "\n" in v or "\r" in v:
+ print(f"Header injection attempt detected: {k}: {v}")
+ continue # Skip invalid headers
+ sanitized_headers.append((k, v))
+
+ # Ensure Content-Type is set unless explicitly provided
+ has_content_type = any(h[0].lower() == "content-type" for h in sanitized_headers)
if not has_content_type:
- extra_headers.append(("Content-Type", "text/html; charset=utf-8"))
+ sanitized_headers.append(("Content-Type", "text/html; charset=utf-8"))
- # Send the initial response start
+ # Send response start
await send({
"type": "http.response.start",
"status": status_code,
"headers": [
- (k.encode("latin-1"), v.encode("latin-1")) for k, v in extra_headers
+ (k.encode("latin-1"), v.encode("latin-1")) for k, v in sanitized_headers
],
})
@@ -379,13 +403,15 @@ class Server:
# Convert anything else to string then to bytes
response_body = str(body).encode("utf-8")
+ # Ensure body is properly encoded
+ response_body = body.encode("utf-8") if isinstance(body, str) else body
await send({
"type": "http.response.body",
"body": response_body,
"more_body": False
})
- def cleanup_sessions(self) -> None:
+ def _cleanup_sessions(self) -> None:
now = time.time()
self.sessions = {
sid: data
@@ -393,7 +419,7 @@ class Server:
if data.get("last_access", now) + self.SESSION_TIMEOUT > now
}
- def redirect(self, location: str) -> Tuple[int, str]:
+ def _redirect(self, location: str) -> Tuple[int, str]:
return (
302,
(
@@ -403,7 +429,7 @@ class Server:
),
)
- async def render_template(self, name: str, **kwargs: Any) -> str:
+ async def _render_template(self, name: str, **kwargs: Any) -> str:
"""
Async-compatible template rendering using Jinja2.
"""
diff --git a/examples/socketio/webtrc/app.py b/examples/socketio/webtrc/app.py
index 0cac768..4456f09 100644
--- a/examples/socketio/webtrc/app.py
+++ b/examples/socketio/webtrc/app.py
@@ -16,11 +16,11 @@ class MyApp(Server):
async def stream(self, username: str):
# Mark the username active, render the streamer template
active_users.add(username)
- return await self.render_template("stream.html", username=username)
+ return await self._render_template("stream.html", username=username)
async def watch(self, username: str):
# Mark the username active, render the watcher template
- return await self.render_template("watch.html", username=username)
+ return await self._render_template("watch.html", username=username)
#
# ------------------- Socket.IO Events for Signaling --------------------
diff --git a/examples/twutr/twutr.py b/examples/twutr/twutr.py
index 4955df1..7e60afd 100644
--- a/examples/twutr/twutr.py
+++ b/examples/twutr/twutr.py
@@ -151,20 +151,20 @@ class Twutr(Server):
async def index(self):
"""Shows the user's timeline (the messages of people they follow, including their own)."""
if not self.request.session.get('logged_in'):
- return self.redirect('/public')
+ return self._redirect('/public')
user_id = self.request.session.get('user_id')
all_messages = get_all_messages_for_user_and_following(user_id)
all_messages = sort_messages_by_timestamp(all_messages, timestamp_index=2)
- return await self.render_template('timeline.html', messages=all_messages, session=self.request.session)
+ return await self._render_template('timeline.html', messages=all_messages, session=self.request.session)
async def public(self):
"""Displays the latest messages of all users with usernames."""
all_messages = get_all_messages_from_all_users()
all_messages = sort_messages_by_timestamp(all_messages, timestamp_index=2)
- return await self.render_template('public.html', messages=all_messages, session=self.request.session)
+ return await self._render_template('public.html', messages=all_messages, session=self.request.session)
async def user(self, username):
"""Displays a specific user's messages."""
@@ -189,7 +189,7 @@ class Twutr(Server):
followers = user_data.get('followers', [])
following_count = len(user_data.get('following', []))
- return await self.render_template(
+ return await self._render_template(
'user.html',
messages=messages,
username=username,
@@ -204,7 +204,7 @@ class Twutr(Server):
def follow(self, username):
"""Follow another user."""
if not self.request.session.get('logged_in'):
- return self.redirect('/login')
+ return self._redirect('/login')
username = escape(username)
current_user = self.request.session.get('user_id')
@@ -213,17 +213,17 @@ class Twutr(Server):
return "You cannot follow yourself"
update_follow_relationship(current_user, username, follow=True)
- return self.redirect(f'/user/{username}')
+ return self._redirect(f'/user/{username}')
def unfollow(self, username):
"""Unfollow another user."""
if not self.request.session.get('logged_in'):
- return self.redirect('/login')
+ return self._redirect('/login')
current_user = self.request.session.get('user_id')
update_follow_relationship(current_user, escape(username), follow=False)
- return self.redirect(f'/user/{username}')
+ return self._redirect(f'/user/{username}')
async def list_followers(self, username):
"""Displays the list of followers for a given user."""
@@ -233,7 +233,7 @@ class Twutr(Server):
return "User not found", 404
followers = user_data.get('followers', [])
- return await self.render_template(
+ return await self._render_template(
'list_followers.html',
username=username,
followers=followers,
@@ -248,7 +248,7 @@ class Twutr(Server):
return "User not found", 404
following = user_data.get('following', [])
- return await self.render_template(
+ return await self._render_template(
'list_following.html',
username=username,
following=following,
@@ -258,7 +258,7 @@ class Twutr(Server):
def add_message(self):
"""Registers a new message for the logged-in user with custom link and mention handling."""
if not self.request.session.get('logged_in'):
- return self.redirect('/login')
+ return self._redirect('/login')
if self.request.method == 'POST':
message = self.request.body_params.get('message', [''])[0]
@@ -268,7 +268,7 @@ class Twutr(Server):
# Prevent empty message submissions
if not sanitized_message.strip():
- return self.render_template('timeline.html', error="Message cannot be empty", session=self.request.session)
+ return self._render_template('timeline.html', error="Message cannot be empty", session=self.request.session)
time_stamp = str(datetime.utcnow().strftime('%m/%d/%Y %I:%M %p'))
message_tuple = (sanitized_message, time_stamp)
@@ -277,43 +277,43 @@ class Twutr(Server):
user_data['messages'].append(message_tuple)
save_user_data(self.request.session.get('user_id'), user_data)
- return self.redirect('/')
+ return self._redirect('/')
async def login(self):
"""Logs the user in."""
if self.request.session.get('logged_in'):
- return self.redirect('/')
+ return self._redirect('/')
if self.request.method == 'POST':
username = escape(self.request.body_params.get('username', [''])[0].strip())
password = escape(self.request.body_params.get('password', [''])[0].strip())
if not username or not password:
- return await self.render_template('login.html', error="Fields cannot be empty", session=self.request.session)
+ return await self._render_template('login.html', error="Fields cannot be empty", session=self.request.session)
user = get_user_data(username)
if not user or user['password'] != password:
- return await self.render_template('login.html', error="Invalid credentials", session=self.request.session)
+ return await self._render_template('login.html', error="Invalid credentials", session=self.request.session)
self.request.session['user_id'] = username
self.request.session['logged_in'] = True
- return self.redirect('/')
+ return self._redirect('/')
- return await self.render_template('login.html', session=self.request.session)
+ return await self._render_template('login.html', session=self.request.session)
async def register(self):
"""Registers a new user."""
if self.request.session.get('logged_in'):
- return self.redirect('/')
+ return self._redirect('/')
if self.request.method == 'POST':
username = escape(self.request.body_params.get('username', [''])[0].strip())
password = escape(self.request.body_params.get('password', [''])[0].strip())
if not username or not password:
- return await self.render_template('login.html', error="Fields cannot be empty", session=self.request.session)
+ return await self._render_template('login.html', error="Fields cannot be empty", session=self.request.session)
if db.get(username):
- return await self.render_template('register.html', session=self.request.session, error="Username already taken.")
+ return await self._render_template('register.html', session=self.request.session, error="Username already taken.")
db.set(str(username), {
'username': username,
@@ -323,15 +323,15 @@ class Twutr(Server):
'following': []
})
db.save()
- return self.redirect('/login')
+ return self._redirect('/login')
- return await self.render_template('register.html', session=self.request.session)
+ return await self._render_template('register.html', session=self.request.session)
def logout(self):
"""Logs the user out."""
if self.request.session.get('logged_in'):
self.request.session.clear()
- return self.redirect('/public')
+ return self._redirect('/public')