Skip to content
Merged
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
4 changes: 2 additions & 2 deletions app/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions app/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fireshare",
"version": "1.7.5",
"version": "1.7.6",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
Expand Down Expand Up @@ -39,4 +39,4 @@
"build": "vite build",
"preview": "vite preview"
}
}
}
4 changes: 4 additions & 0 deletions app/nginx/dev.template.conf
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ http {
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
# Empty here (this location block captures no id, so the auth endpoint falls back
# to parsing the URI), but declaring it stops a client-supplied header of this
# name from being forwarded and trusted.
proxy_set_header X-Fireshare-Video-Id $video_id;
proxy_set_header Cookie $http_cookie;
}

Expand Down
10 changes: 8 additions & 2 deletions app/nginx/prod.conf
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ http {
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
# The id captured by the serving location, so the auth check gates the same
# video the try_files below actually serves. Declaring it here also stops a
# client-supplied header of this name from being forwarded.
proxy_set_header X-Fireshare-Video-Id $video_id;
proxy_set_header Cookie $http_cookie;
}

Expand All @@ -122,6 +126,10 @@ http {
}

location ~ ^/_content/video/([\w-]+)(\.[^/]+)$ {
# Captured before auth_request runs so the gate sees the same id as try_files
set $video_id $1;
set $video_ext $2;

auth_request /internal/video-auth;

sendfile off;
Expand All @@ -139,8 +147,6 @@ http {

limit_rate_after 5m;

set $video_id $1;
set $video_ext $2;
root /processed/;
try_files /derived/$video_id/$video_id-cropped.mp4 /video_links/$video_id$video_ext =404;
}
Expand Down
57 changes: 46 additions & 11 deletions app/server/fireshare/api/video.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import posixpath
import re
import secrets
import shutil
Expand All @@ -9,6 +10,7 @@
import threading
from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import unquote

from flask import current_app, jsonify, request, Response, send_file, session
from flask_login import login_required, current_user
Expand Down Expand Up @@ -795,20 +797,53 @@ def nginx_video_auth_admin():
return '', 403


_VIDEO_ID_RE = re.compile(r'^[\w-]+$')
_CONTENT_VIDEO_RE = re.compile(r'^/_content/video/([\w-]+)\.[a-z0-9]+', re.IGNORECASE)
_CONTENT_DERIVED_RE = re.compile(r'^/_content/derived/([\w-]+)/', re.IGNORECASE)


def _video_id_from_original_uri(original_uri):
"""
Resolve the video id from nginx's X-Original-URI.

nginx matches its location blocks against the decoded, normalized URI but forwards the
raw one in $request_uri, so the URI seen here has to be decoded and normalized the same
way before it is parsed — otherwise a request like /_content/vide%6F/<id>.mp4 routes to
the file on the nginx side while failing to resolve an id on this side.
"""
path = unquote(original_uri.split('?', 1)[0])
if not path.startswith('/'):
return None
path = posixpath.normpath(path)
# normpath drops a meaningful trailing slash, which the derived pattern relies on
if original_uri.split('?', 1)[0].endswith('/') and not path.endswith('/'):
path += '/'
for pattern in (_CONTENT_VIDEO_RE, _CONTENT_DERIVED_RE):
m = pattern.match(path)
if m:
return m.group(1)
return None


@api.route('/api/video/nginx-auth')
def nginx_video_auth():
"""Internal endpoint called by nginx auth_request to gate password-protected video files."""
original_uri = request.headers.get('X-Original-URI', '')
video_id = None
m = re.match(r'^/_content/video/([\w-]+)\.[a-z0-9]+', original_uri)
if m:
video_id = m.group(1)
if not video_id:
m = re.match(r'^/_content/derived/([\w-]+)/', original_uri)
if m:
video_id = m.group(1)
"""
Internal endpoint called by nginx auth_request to gate password-protected video files.

Prefers the id nginx already captured from its own location regex (X-Fireshare-Video-Id),
which is the same value it uses to pick the file, so the two cannot disagree. Falls back
to parsing the URI, and refuses the request if no id can be resolved — an unresolvable id
means the gate cannot do its job, so it must not let the request through.
"""
video_id = request.headers.get('X-Fireshare-Video-Id')
if not video_id or not _VIDEO_ID_RE.match(video_id):
video_id = _video_id_from_original_uri(request.headers.get('X-Original-URI', ''))
if not video_id:
return '', 200
logger.warning(
f"nginx-auth could not resolve a video id for "
f"{request.headers.get('X-Original-URI', '')!r}; denying request"
)
return '', 403
video_info = VideoInfo.query.filter_by(video_id=video_id).first()
if not video_info or not video_info.password_hash:
return '', 200
Expand Down
Loading