Merge pull request #759 from ae-utbm/accel-redirect

Accel redirect
This commit is contained in:
thomas girod 2024-08-05 15:15:39 +02:00 committed by GitHub
commit e37ce4172e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 203 additions and 68 deletions

View File

@ -21,19 +21,12 @@ from club.models import Club
from core.models import Group, SithFile, User from core.models import Group, SithFile, User
from core.views.site import search_user from core.views.site import search_user
from counter.models import Counter, Customer, Product from counter.models import Counter, Customer, Product
from counter.utils import is_logged_in_counter
def check_token(request):
return (
"counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter(token=request.session["counter_token"]).exists()
)
class RightManagedLookupChannel(LookupChannel): class RightManagedLookupChannel(LookupChannel):
def check_auth(self, request): def check_auth(self, request):
if not request.user.was_subscribed and not check_token(request): if not request.user.was_subscribed and not is_logged_in_counter(request):
raise PermissionDenied raise PermissionDenied

View File

@ -1137,11 +1137,9 @@ class SithFile(models.Model):
else: else:
self._check_path_consistence() self._check_path_consistence()
def __getattribute__(self, attr): @property
if attr == "is_file": def is_file(self):
return not self.is_folder return not self.is_folder
else:
return super().__getattribute__(attr)
@cached_property @cached_property
def as_picture(self): def as_picture(self):

View File

@ -12,6 +12,7 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from urllib.parse import quote, urljoin
# This file contains all the views that concern the page model # This file contains all the views that concern the page model
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
@ -21,7 +22,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.http import Http404, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.http import http_date from django.utils.http import http_date
@ -37,33 +38,42 @@ from core.views import (
CanViewMixin, CanViewMixin,
can_view, can_view,
) )
from counter.models import Counter from counter.utils import is_logged_in_counter
def send_file(request, file_id, file_class=SithFile, file_attr="file"): def send_file(
"""Send a file through Django without loading the whole file into request: HttpRequest,
memory at once. The FileWrapper will turn the file object into an file_id: int,
iterator for chunks of 8KB. file_class: type[SithFile] = SithFile,
file_attr: str = "file",
) -> HttpResponse:
"""Send a protected file, if the user can see it.
In prod, the server won't handle the download itself,
but set the appropriate headers in the response to make the reverse-proxy
deal with it.
In debug mode, the server will directly send the file.
""" """
f = get_object_or_404(file_class, id=file_id) f = get_object_or_404(file_class, id=file_id)
if not ( if not can_view(f, request.user) and not is_logged_in_counter(request):
can_view(f, request.user)
or (
"counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
)
):
raise PermissionDenied raise PermissionDenied
name = f.__getattribute__(file_attr).name name = getattr(f, file_attr).name
filepath = settings.MEDIA_ROOT / name filepath = settings.MEDIA_ROOT / name
# check if file exists on disk # check if file exists on disk
if not filepath.exists(): if not filepath.exists():
raise Http404 raise Http404
if not settings.DEBUG:
# When receiving a response with the Accel-Redirect header,
# the reverse proxy will automatically handle the file sending.
# This is really hard to test (thus isn't tested)
# so please do not mess with this.
response = HttpResponse(status=200)
response["Content-Type"] = ""
response["X-Accel-Redirect"] = quote(urljoin(settings.MEDIA_URL, name))
return response
with open(filepath, "rb") as filename: with open(filepath, "rb") as filename:
wrapper = FileWrapper(filename) wrapper = FileWrapper(filename)
response = HttpResponse(wrapper, content_type=f.mime_type) response = HttpResponse(wrapper, content_type=f.mime_type)

36
counter/utils.py Normal file
View File

@ -0,0 +1,36 @@
from urllib.parse import urlparse
from django.http import HttpRequest
from django.urls import resolve
from counter.models import Counter
def is_logged_in_counter(request: HttpRequest) -> bool:
"""Check if the request is sent from a device logged to a counter.
The request must also be sent within the frame of a counter's activity.
Trying to use this function to manage access to non-sas
related resources probably won't work.
A request is considered as coming from a logged counter if :
- Its referer comes from the counter app
(eg. fetching user pictures from the click UI)
or the request path belongs to the counter app
(eg. the barman went back to the main by missclick and go back
to the counter)
- The current session has a counter token associated with it.
- A counter with this token exists.
"""
referer = urlparse(request.META["HTTP_REFERER"]).path
path_ok = (
request.resolver_match.app_name == "counter"
or resolve(referer).app_name == "counter"
)
return (
path_ok
and "counter_token" in request.session
and request.session["counter_token"]
and Counter.objects.filter(token=request.session["counter_token"]).exists()
)

View File

@ -80,6 +80,7 @@ from counter.models import (
Selling, Selling,
StudentCard, StudentCard,
) )
from counter.utils import is_logged_in_counter
class CounterAdminMixin(View): class CounterAdminMixin(View):
@ -901,15 +902,9 @@ class RefillingDeleteView(DeleteView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
self.object = self.get_object() self.object = self.get_object()
if ( if timezone.now() - self.object.date <= timedelta(
timezone.now() - self.object.date minutes=settings.SITH_LAST_OPERATIONS_LIMIT
<= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT) ) and is_logged_in_counter(request):
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
self.success_url = reverse( self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.counter.id} "counter:details", kwargs={"counter_id": self.object.counter.id}
) )
@ -932,15 +927,9 @@ class SellingDeleteView(DeleteView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin.""" """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
self.object = self.get_object() self.object = self.get_object()
if ( if timezone.now() - self.object.date <= timedelta(
timezone.now() - self.object.date minutes=settings.SITH_LAST_OPERATIONS_LIMIT
<= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT) ) and is_logged_in_counter(request):
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
self.success_url = reverse( self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.counter.id} "counter:details", kwargs={"counter_id": self.object.counter.id}
) )
@ -1175,14 +1164,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""We have here again a very particular right handling.""" """We have here again a very particular right handling."""
self.object = self.get_object() self.object = self.get_object()
if ( if is_logged_in_counter(request) and self.object.barmen_list:
self.object.barmen_list
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
return HttpResponseRedirect( return HttpResponseRedirect(
reverse("counter:details", kwargs={"counter_id": self.object.id}) reverse("counter:details", kwargs={"counter_id": self.object.id})
@ -1215,14 +1197,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
"""We have here again a very particular right handling.""" """We have here again a very particular right handling."""
self.object = self.get_object() self.object = self.get_object()
if ( if is_logged_in_counter(request) and self.object.barmen_list:
self.object.barmen_list
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
return HttpResponseRedirect( return HttpResponseRedirect(
reverse("counter:details", kwargs={"counter_id": self.object.id}) reverse("counter:details", kwargs={"counter_id": self.object.id})

View File

@ -9,6 +9,33 @@ que votre environnement de développement soit encore plus
proche de celui en production. proche de celui en production.
Voici les étapes à suivre pour ça. Voici les étapes à suivre pour ça.
!!!tip
Configurer les dépendances du projet
peut demander beaucoup d'allers et retours entre
votre répertoire projet et divers autres emplacements.
Vous pouvez gagner du temps en déclarant un alias :
=== "bash/zsh"
```bash
alias cdp="cd /repertoire/du/projet"
```
=== "nu"
```nu
alias cdp = cd /repertoire/du/projet
```
Chaque fois qu'on vous demandera de retourner au répertoire
projet, vous aurez juste à faire :
```bash
cdp
```
## Installer les dépendances manquantes ## Installer les dépendances manquantes
Pour installer complètement le projet, il va falloir Pour installer complètement le projet, il va falloir
@ -20,19 +47,19 @@ Commencez par installer les dépendances système :
=== "Debian/Ubuntu" === "Debian/Ubuntu"
```bash ```bash
sudo apt install postgresql redis libq-dev sudo apt install postgresql redis libq-dev nginx
``` ```
=== "Arch Linux" === "Arch Linux"
```bash ```bash
sudo pacman -S postgresql redis sudo pacman -S postgresql redis nginx
``` ```
=== "macOS" === "macOS"
```bash ```bash
brew install postgresql redis nginx lipbq brew install postgresql redis lipbq nginx
export PATH="/usr/local/opt/libpq/bin:$PATH" export PATH="/usr/local/opt/libpq/bin:$PATH"
source ~/.zshrc source ~/.zshrc
``` ```
@ -139,6 +166,102 @@ poetry run ./manage.py populate
N'oubliez de quitter la session de l'utilisateur N'oubliez de quitter la session de l'utilisateur
postgres après avoir configuré la db. postgres après avoir configuré la db.
## Configurer nginx
Nginx est utilisé comme reverse-proxy.
!!!warning
Nginx ne sert pas les fichiers de la même manière que Django.
Les fichiers statiques servis seront ceux du dossier `/static`,
tels que générés par les commandes `collectstatic` et
`compilestatic`.
Si vous changez du css ou du js sans faire tourner
ces commandes, ces changements ne seront pas reflétés.
De manière générale, utiliser nginx en dev n'est pas très utile,
voire est gênant si vous travaillez sur le front.
Ne vous embêtez pas avec ça, sauf par curiosité intellectuelle,
ou bien si vous voulez tester spécifiquement
des interactions avec le reverse proxy.
Placez-vous dans le répertoire `/etc/nginx`,
et créez les dossiers et fichiers nécessaires :
```bash
cd /etc/nginx/
sudo mkdir sites-enabled sites-available
sudo touch sites-available/sith.conf
sudo ln -s /etc/nginx/sites-available/sith.conf sites-enabled/sith.conf
```
Puis ouvrez le fichier `sites-available/sith.conf` et mettez-y le contenu suivant :
```nginx
server {
listen 8000;
server_name _;
location /static/;
root /repertoire/du/projet;
}
location ~ ^/data/(products|com|club_logos)/ {
root /repertoire/du/projet;
}
location ~ ^/data/(SAS|profiles|users|.compressed|.thumbnails)/ {
# https://nginx.org/en/docs/http/ngx_http_core_module.html#internal
internal;
root /repertoire/du/projet;
}
location / {
proxy_pass http://127.0.0.1:8001;
include uwsgi_params;
}
}
```
Ouvrez le fichier `nginx.conf`, et ajoutez la configuration suivante :
```nginx
http {
# Toute la configuration
# éventuellement déjà là
include /etc/nginx/sites-enabled/sith.conf;
}
```
Vérifiez que votre configuration est bonne :
```bash
sudo nginx -t
```
Si votre configuration n'est pas bonne, corrigez-la.
Puis lancez ou relancez nginx :
```bash
sudo systemctl restart nginx
```
Dans votre `settings_custom.py`, remplacez `DEBUG=True` par `DEBUG=False`.
Enfin, démarrez le serveur Django :
```bash
cd /repertoire/du/projet
poetry run ./manage.py runserver 8001
```
Et c'est bon, votre reverse-proxy est prêt à tourner devant votre serveur.
Nginx écoutera sur le port 8000.
Toutes les requêtes vers des fichiers statiques et les medias publiques
seront seront servies directement par nginx.
Toutes les autres requêtes seront transmises au serveur django.
## Mettre à jour la base de données antispam ## Mettre à jour la base de données antispam