Skip to content
Merged

Dev #18

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: 1 addition & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,4 @@ jobs:
name: rohanbatrain/second_brain_database
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io

registry: ghcr.io
14 changes: 7 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use the official Python image
FROM python:3.9-slim
# Use a lightweight base image for production
FROM python:3.9-slim as base

# Set the working directory inside the container
WORKDIR /app
Expand All @@ -10,6 +10,9 @@ COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

# Create a non-root user and group named sbd_user with home directory
RUN groupadd -r sbd_user && useradd -r -g sbd_user -d /sbd_user -m sbd_user

Expand All @@ -25,8 +28,6 @@ RUN chown -R sbd_user:sbd_user /app
# Install your package (assuming setup.py or pyproject.toml exists)
RUN pip install .

RUN pip install second_brain_database

# Switch to the non-root user
USER sbd_user

Expand All @@ -36,6 +37,5 @@ ENV HOME=/sbd_user
# Expose the port
EXPOSE 5000

# Run the app
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "second_brain_database.main"]
# CMD ["python", "-m", "Second_Brain_Database.main"]
# Use Gunicorn for production with optimized settings
CMD ["gunicorn", "--workers=3", "--threads=2", "--bind", "0.0.0.0:5000", "second_brain_database.main:app"]
6 changes: 6 additions & 0 deletions dev-environment/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ services:
networks:
- proxy
- default
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

nginx_proxy_manager:
image: jc21/nginx-proxy-manager:latest
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ services:
networks:
- proxy
- default
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

nginx_proxy_manager:
image: jc21/nginx-proxy-manager:latest
Expand Down
Binary file removed src/second_brain_database/assets/banner.png
Binary file not shown.
61 changes: 44 additions & 17 deletions src/second_brain_database/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,42 @@
swallow_errors=True # Prevent Flask from logging too many limit errors
)

# Global whitelist for IPs
WHITELISTED_IPS = ["127.0.0.1", "localhost"] # Replace with your list of whitelisted IPs

@app.before_request
def block_abusive_ips():
"""
Block requests from IPs that are currently blocked in Redis.
Prevent blocking whitelisted IPs.
Raises:
403: If the IP is blocked.
"""
ip = request.remote_addr
ip = request.headers.get("X-Forwarded-For", request.remote_addr).split(",")[0].strip()

if ip in WHITELISTED_IPS:
return # Skip blocking and logging for whitelisted IPs

blocked = r.get(f"blocked:{ip}")
if blocked:
print(f"Blocked request from IP: {ip}")
abort(403, description="You are temporarily blocked.")

@app.before_request
def slow_down_attackers():
"""
Introduce a delay for IPs with multiple failed attempts (tar-pitting).
"""
ip = request.remote_addr

if ip in WHITELISTED_IPS:
return # Skip delay for whitelisted IPs

failed_attempts = int(r.get(f"failed:{ip}") or 0)
if failed_attempts > 5: # Delay only if user has failed 5+ times
print(f"Delaying request from IP {ip} due to {failed_attempts} failed attempts.")
time.sleep(2) # 2-second delay before processing request

@app.after_request
def track_failed_attempts(response):
"""
Expand All @@ -68,26 +91,19 @@ def track_failed_attempts(response):
Returns:
flask.Response: The response object.
"""
ip = request.remote_addr

if ip in WHITELISTED_IPS:
return response # Skip tracking for whitelisted IPs

if response.status_code == 429: # Too Many Requests
ip = request.remote_addr
r.incr(f"failed:{ip}") # Increment failure count
attempts = int(r.get(f"failed:{ip}") or 0)
if attempts > 10: # Block IP after 10 failures
r.setex(f"blocked:{ip}", 3600, 1) # Block for 1 hour
print(f"IP {ip} blocked for 1 hour after {attempts} failures.")
return response

@app.before_request
def slow_down_attackers():
"""
Introduce a delay for IPs with multiple failed attempts (tar-pitting).
"""
ip = request.remote_addr
failed_attempts = int(r.get(f"failed:{ip}") or 0)
if failed_attempts > 5: # Delay only if user has failed 5+ times
print(f"Delaying request from IP {ip} due to {failed_attempts} failed attempts.")
time.sleep(2) # 2-second delay before processing request

@app.route("/")
def landing_page():
"""
Expand All @@ -104,7 +120,7 @@ def landing_page():
<title>Welcome to Second Brain Database</title>

<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">

<style>
Expand Down Expand Up @@ -160,7 +176,6 @@ def landing_page():

</body>
</html>

"""

@app.route("/login")
Expand All @@ -182,6 +197,16 @@ def register_page():
return """<html><head><title>Register</title></head>
<body><h1>Register Page</h1></body></html>"""

@app.route("/health")
@limiter.limit("1 per 10 seconds")
def health_check():
"""
Health check route to verify the application is running.
Returns:
str: JSON response indicating the application status.
"""
return {"status": "ok", "message": "Application is healthy."}, 200

@app.errorhandler(404)
def not_found_error(error): # pylint: disable=unused-argument
"""
Expand Down Expand Up @@ -222,8 +247,10 @@ def forbidden_error(error): # pylint: disable=unused-argument
tuple: HTML content and status code.
"""
return (
"""<html><head><title>403 Forbidden</title></head><body><h1>403 - Forbidden</h1>"
"</body></html>""",
"""
<html><head><title>403 Forbidden</title></head><body><h1>403 - Forbidden</h1>"
"</body></html>
""",
403,
)

Expand Down