remove CORS and add CSRF token

This commit is contained in:
Blake Blackshear 2023-10-05 17:33:54 -05:00
parent 62a5cb87be
commit 86576889b1
18 changed files with 1064 additions and 333 deletions

View File

@ -93,10 +93,6 @@ http {
secure_token $args; secure_token $args;
secure_token_types application/vnd.apple.mpegurl; secure_token_types application/vnd.apple.mpegurl;
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
add_header Access-Control-Allow-Origin '*';
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
expires off; expires off;
} }
@ -104,16 +100,6 @@ http {
location /stream/ { location /stream/ {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
expires off; expires off;
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types { types {
application/dash+xml mpd; application/dash+xml mpd;
@ -126,16 +112,6 @@ http {
} }
location /clips/ { location /clips/ {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types { types {
video/mp4 mp4; video/mp4 mp4;
@ -152,17 +128,6 @@ http {
} }
location /recordings/ { location /recordings/ {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types { types {
video/mp4 mp4; video/mp4 mp4;
} }
@ -173,17 +138,6 @@ http {
} }
location /exports/ { location /exports/ {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
types { types {
video/mp4 mp4; video/mp4 mp4;
} }
@ -235,8 +189,6 @@ http {
} }
location ~* /api/.*\.(jpg|jpeg|png)$ { location ~* /api/.*\.(jpg|jpeg|png)$ {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
rewrite ^/api/(.*)$ $1 break; rewrite ^/api/(.*)$ $1 break;
proxy_pass http://frigate_api; proxy_pass http://frigate_api;
proxy_pass_request_headers on; proxy_pass_request_headers on;
@ -248,10 +200,6 @@ http {
location /api/ { location /api/ {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
expires off; expires off;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
proxy_pass http://frigate_api/; proxy_pass http://frigate_api/;
proxy_pass_request_headers on; proxy_pass_request_headers on;
proxy_set_header Host $host; proxy_set_header Host $host;

View File

@ -155,10 +155,6 @@ cd web && npm install
cd web && npm run dev cd web && npm run dev
``` ```
#### 3a. Run the development server against a non-local instance
To run the development server against a non-local instance, you will need to modify the API_HOST default return in `web/src/env.js`.
#### 4. Making changes #### 4. Making changes
The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com). The Web UI is built using [Vite](https://vitejs.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).

View File

@ -74,6 +74,13 @@ def create_app(
): ):
app = Flask(__name__) app = Flask(__name__)
@app.before_request
def check_csrf():
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
pass
if "origin" in request.headers and "x-csrf-token" not in request.headers:
return jsonify({"success": False, "message": "Missing CSRF header"}), 401
@app.before_request @app.before_request
def _db_connect(): def _db_connect():
if database.is_closed(): if database.is_closed():

View File

@ -20,7 +20,6 @@
}, },
"ignorePatterns": ["*.d.ts"], "ignorePatterns": ["*.d.ts"],
"rules": { "rules": {
"indent": ["error", 2, { "SwitchCase": 1 }],
"comma-dangle": [ "comma-dangle": [
"error", "error",
{ {

View File

@ -1,4 +1,5 @@
{ {
"printWidth": 120, "printWidth": 120,
"trailingComma": "es5",
"singleQuote": true "singleQuote": true
} }

1290
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,7 @@
"eslint-config-preact": "^1.3.0", "eslint-config-preact": "^1.3.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jest": "^27.2.3", "eslint-plugin-jest": "^27.2.3",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vitest-globals": "^1.4.0", "eslint-plugin-vitest-globals": "^1.4.0",
"fake-indexeddb": "^4.0.1", "fake-indexeddb": "^4.0.1",
"jsdom": "^22.0.0", "jsdom": "^22.0.0",

View File

@ -1,2 +1 @@
import { API_HOST } from '../env'; export const baseUrl = `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`;
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || '/'}`;

View File

@ -5,6 +5,9 @@ import { WsProvider } from './ws';
import axios from 'axios'; import axios from 'axios';
axios.defaults.baseURL = `${baseUrl}api/`; axios.defaults.baseURL = `${baseUrl}api/`;
axios.defaults.headers.common = {
'X-CSRF-TOKEN': 1,
};
export function ApiProvider({ children, options }) { export function ApiProvider({ children, options }) {
return ( return (

View File

@ -58,7 +58,7 @@ export default function CameraImage({ camera, onload, searchParams = '', stretch
if (!config || scaledHeight === 0 || !canvasRef.current) { if (!config || scaledHeight === 0 || !canvasRef.current) {
return; return;
} }
img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`; img.src = `${apiHost}api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
}, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]); }, [apiHost, canvasRef, name, img, searchParams, scaledHeight, config]);
return ( return (

View File

@ -56,10 +56,10 @@ export const HistoryVideo = ({
} }
video.src({ video.src({
src: `${apiHost}/vod/event/${id}/master.m3u8`, src: `${apiHost}vod/event/${id}/master.m3u8`,
type: 'application/vnd.apple.mpegurl', type: 'application/vnd.apple.mpegurl',
}); });
video.poster(`${apiHost}/api/events/${id}/snapshot.jpg`); video.poster(`${apiHost}api/events/${id}/snapshot.jpg`);
if (videoIsPlaying) { if (videoIsPlaying) {
video.play(); video.play();
} }

View File

@ -153,7 +153,7 @@ export function EventCard({ camera, event }) {
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}> <Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}>
<div className="flex flex-row mb-2"> <div className="flex flex-row mb-2">
<div className="w-28 mr-4"> <div className="w-28 mr-4">
<img className="antialiased" loading="lazy" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} /> <img className="antialiased" loading="lazy" src={`${apiHost}api/events/${event.id}/thumbnail.jpg`} />
</div> </div>
<div className="flex flex-row w-full border-b"> <div className="flex flex-row w-full border-b">
<div className="w-full text-gray-700 font-semibold relative pt-0"> <div className="w-full text-gray-700 font-semibold relative pt-0">

View File

@ -1,2 +0,0 @@
export const ENV = import.meta.env.MODE;
export const API_HOST = ENV === 'production' ? '' : 'http://localhost:5000/';

View File

@ -212,7 +212,7 @@ export default function Camera({ camera }) {
key={objectType} key={objectType}
header={objectType} header={objectType}
href={`/events?cameras=${camera}&labels=${encodeURIComponent(objectType)}`} href={`/events?cameras=${camera}&labels=${encodeURIComponent(objectType)}`}
media={<img src={`${apiHost}/api/${camera}/${encodeURIComponent(objectType)}/thumbnail.jpg`} />} media={<img src={`${apiHost}api/${camera}/${encodeURIComponent(objectType)}/thumbnail.jpg`} />}
/> />
))} ))}
</div> </div>

View File

@ -336,7 +336,7 @@ ${Object.keys(objectMaskPoints)
<div className="space-y-4"> <div className="space-y-4">
<div className="relative"> <div className="relative">
<img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} /> <img ref={imageRef} src={`${apiHost}api/${camera}/latest.jpg`} />
<EditableMask <EditableMask
onChange={handleUpdateEditable} onChange={handleUpdateEditable}
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]} points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}

View File

@ -74,7 +74,7 @@ export default function Config() {
format: true, format: true,
schemas: [ schemas: [
{ {
uri: `${apiHost}/api/config/schema.json`, uri: `${apiHost}api/config/schema.json`,
fileMatch: [String(modelUri)], fileMatch: [String(modelUri)],
}, },
], ],

View File

@ -402,7 +402,7 @@ export default function Events({ path, ...props }) {
icon={Snapshot} icon={Snapshot}
label="Download Snapshot" label="Download Snapshot"
value="snapshot" value="snapshot"
href={`${apiHost}/api/events/${downloadEvent.id}/snapshot.jpg?download=true`} href={`${apiHost}api/events/${downloadEvent.id}/snapshot.jpg?download=true`}
download download
/> />
)} )}
@ -411,7 +411,7 @@ export default function Events({ path, ...props }) {
icon={Clip} icon={Clip}
label="Download Clip" label="Download Clip"
value="clip" value="clip"
href={`${apiHost}/api/events/${downloadEvent.id}/clip.mp4?download=true`} href={`${apiHost}api/events/${downloadEvent.id}/clip.mp4?download=true`}
download download
/> />
)} )}
@ -492,7 +492,7 @@ export default function Events({ path, ...props }) {
<img <img
className="flex-grow-0" className="flex-grow-0"
src={`${apiHost}/api/events/${plusSubmitEvent.id}/snapshot.jpg`} src={`${apiHost}api/events/${plusSubmitEvent.id}/snapshot.jpg`}
alt={`${plusSubmitEvent.label}`} alt={`${plusSubmitEvent.label}`}
/> />
@ -619,7 +619,7 @@ export default function Events({ path, ...props }) {
<div <div
className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain bg-no-repeat bg-center" className="relative rounded-l flex-initial min-w-[125px] h-[125px] bg-contain bg-no-repeat bg-center"
style={{ style={{
'background-image': `url(${apiHost}/api/events/${event.id}/thumbnail.jpg)`, 'background-image': `url(${apiHost}api/events/${event.id}/thumbnail.jpg)`,
}} }}
> >
<StarRecording <StarRecording
@ -776,8 +776,8 @@ export default function Events({ path, ...props }) {
className="flex-grow-0" className="flex-grow-0"
src={ src={
event.has_snapshot event.has_snapshot
? `${apiHost}/api/events/${event.id}/snapshot.jpg` ? `${apiHost}api/events/${event.id}/snapshot.jpg`
: `${apiHost}/api/events/${event.id}/thumbnail.jpg` : `${apiHost}api/events/${event.id}/thumbnail.jpg`
} }
alt={`${event.label} at ${((event?.data?.top_score || event.top_score) * 100).toFixed( alt={`${event.label} at ${((event?.data?.top_score || event.top_score) * 100).toFixed(
0 0

View File

@ -9,6 +9,13 @@ export default defineConfig({
define: { define: {
'import.meta.vitest': 'undefined', 'import.meta.vitest': 'undefined',
}, },
server: {
proxy: {
'/api': {
target: 'http://localhost:5000'
}
}
},
plugins: [ plugins: [
preact(), preact(),
monacoEditorPlugin.default({ monacoEditorPlugin.default({