Skip to content

Commit 6ee4722

Browse files
committed
Added very experimental web server
1 parent 1a9627c commit 6ee4722

File tree

8 files changed

+252
-0
lines changed

8 files changed

+252
-0
lines changed

capsul/server/__init__.py

Whitespace-only changes.

capsul/server/__main__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
import socket
3+
import uvicorn
4+
5+
def find_free_port():
6+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
7+
s.bind(('', 0)) # Port 0 = demander un port libre
8+
return s.getsockname()[1]
9+
10+
port = find_free_port()
11+
os.environ["BASE_URL"] = f"http://127.0.0.1:{port}"
12+
13+
uvicorn.run(
14+
"capsul.server.asgi:application",
15+
host="127.0.0.1",
16+
port=port,
17+
)

capsul/server/asgi.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import asyncio
3+
import logging
4+
import os
5+
6+
from django.core.asgi import get_asgi_application
7+
8+
from . import settings
9+
10+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'capsul.server.settings')
11+
12+
logger = logging.getLogger(__name__)
13+
14+
_started = False
15+
16+
class PostStartupMiddleware:
17+
def __init__(self, app):
18+
self.app = app
19+
20+
async def __call__(self, scope, receive, send):
21+
global _started
22+
if not _started:
23+
_started = True
24+
asyncio.create_task(self.run_after_startup(f"{os.environ['BASE_URL']}?capsul_token={settings.SECRET_KEY}"))
25+
await self.app(scope, receive, send)
26+
27+
async def run_after_startup(self, url):
28+
from . import settings
29+
await asyncio.sleep(0.1) # Laisse Uvicorn terminer sa sortie console
30+
print(f"✅ Serveur prêt: {url}")
31+
32+
application = PostStartupMiddleware(get_asgi_application())
33+

capsul/server/settings.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from pathlib import Path
2+
import secrets
3+
4+
from django.templatetags.static import static
5+
from django.urls import reverse
6+
from jinja2 import Environment
7+
8+
9+
def template_environment(**options):
10+
env = Environment(**options)
11+
env.globals.update(
12+
{
13+
"static": static,
14+
"url": reverse,
15+
}
16+
)
17+
return env
18+
19+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
20+
BASE_DIR = Path(__file__).resolve().parent
21+
22+
23+
# Quick-start development settings - unsuitable for production
24+
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
25+
26+
# SECURITY WARNING: keep the secret key used in production secret!
27+
SECRET_KEY = secrets.token_urlsafe()
28+
29+
# SECURITY WARNING: don't run with debug turned on in production!
30+
DEBUG = True
31+
32+
ALLOWED_HOSTS = []
33+
34+
35+
# Application definition
36+
37+
INSTALLED_APPS = [
38+
# 'django.contrib.admin',
39+
# 'django.contrib.auth',
40+
# 'django.contrib.contenttypes',
41+
# 'django.contrib.sessions',
42+
# 'django.contrib.messages',
43+
# 'django.contrib.staticfiles',
44+
]
45+
46+
MIDDLEWARE = [
47+
'capsul.server.urls.SecurityMiddleware',
48+
# 'django.middleware.security.SecurityMiddleware',
49+
# 'django.contrib.sessions.middleware.SessionMiddleware',
50+
# 'django.middleware.common.CommonMiddleware',
51+
# 'django.middleware.csrf.CsrfViewMiddleware',
52+
# 'django.contrib.auth.middleware.AuthenticationMiddleware',
53+
# 'django.contrib.messages.middleware.MessageMiddleware',
54+
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
55+
]
56+
57+
ROOT_URLCONF = 'capsul.server.urls'
58+
59+
TEMPLATES = [
60+
{
61+
'BACKEND': 'django.template.backends.jinja2.Jinja2',
62+
'DIRS': [BASE_DIR / "templates"],
63+
'APP_DIRS': True,
64+
'OPTIONS': {
65+
'environment': 'capsul.server.settings.template_environment',
66+
},
67+
},
68+
]
69+
70+
# WSGI_APPLICATION = 'capsul.server.wsgi.application'
71+
72+
73+
# Database
74+
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
75+
76+
# DATABASES = {
77+
# 'default': {
78+
# 'ENGINE': 'django.db.backends.sqlite3',
79+
# 'NAME': BASE_DIR / 'db.sqlite3',
80+
# }
81+
# }
82+
83+
84+
# Password validation
85+
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
86+
87+
AUTH_PASSWORD_VALIDATORS = [
88+
{
89+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
90+
},
91+
{
92+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
93+
},
94+
{
95+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
96+
},
97+
{
98+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
99+
},
100+
]
101+
102+
103+
# Internationalization
104+
# https://docs.djangoproject.com/en/5.2/topics/i18n/
105+
106+
LANGUAGE_CODE = 'en-us'
107+
108+
TIME_ZONE = 'UTC'
109+
110+
USE_I18N = True
111+
112+
USE_TZ = True
113+
114+
115+
# Static files (CSS, JavaScript, Images)
116+
# https://docs.djangoproject.com/en/5.2/howto/static-files/
117+
118+
STATIC_URL = 'static/'
119+
120+
# Default primary key field type
121+
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
122+
123+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

capsul/server/templates/base.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>{{title or "Capsul"}}</title>
5+
</head>
6+
<body>
7+
{% block content %}{% endblock %}
8+
</body>
9+
</html>

capsul/server/templates/home.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% extends "base.html" %}
2+
{% block content %}
3+
<h1>Toolboxes</h1>
4+
{% for toolbox in toolboxes %}
5+
<div class="toolbox">{{toolbox.launcher_html | safe}}</div>
6+
{% endfor %}
7+
{% endblock %}

capsul/server/urls.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import datetime
2+
import importlib
3+
4+
from django.urls import path
5+
from django.shortcuts import render
6+
from django.core.exceptions import PermissionDenied
7+
8+
from . import settings
9+
10+
11+
class SecurityMiddleware:
12+
def __init__(self, get_response):
13+
self.get_response = get_response
14+
15+
def __call__(self, request):
16+
capsul_token = request.GET.get("capsul_token", None)
17+
if capsul_token:
18+
get = request.GET.copy()
19+
del get["capsul_token"]
20+
request.GET = get
21+
cookies = request.COOKIES.copy()
22+
cookies["capsul_token"] = capsul_token
23+
request.COOKIES = cookies
24+
response = self.get_response(request)
25+
if capsul_token:
26+
response.set_cookie(
27+
"capsul_token", capsul_token, max_age=datetime.timedelta(days=1)
28+
)
29+
return response
30+
31+
32+
class TestToolbox:
33+
launcher_html = '<h2><a href="test">Test {{url("home")}}</a>'
34+
35+
36+
def render_string(text, context):
37+
"""Render a string with Jinja2 using the same context as for a request"""
38+
39+
env_path = settings.TEMPLATES[0]["OPTIONS"]["environment"]
40+
module_path, func_name = env_path.rsplit(".", 1)
41+
env_module = importlib.import_module(module_path)
42+
env = getattr(env_module, func_name)()
43+
template = env.from_string(text)
44+
return template.render(context)
45+
46+
47+
def home(request):
48+
capsul_token = request.COOKIES.get("capsul_token")
49+
print(repr(capsul_token), "==?", repr(settings.SECRET_KEY))
50+
if capsul_token != settings.SECRET_KEY:
51+
raise PermissionDenied()
52+
53+
toolboxes = [
54+
{"launcher_html": render_string(t.launcher_html, {}) for t in [TestToolbox]}
55+
]
56+
return render(request, "home.html", {"title": "Capsul", "toolboxes": toolboxes})
57+
58+
59+
urlpatterns = [
60+
path("", home, name="home"),
61+
]

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,5 @@ python="*"
7070
pip = "*"
7171
pytest = "*"
7272
twine = "*"
73+
django = "*"
74+
uvicorn = "*"

0 commit comments

Comments
 (0)