Skip to content

Commit fed8dc9

Browse files
authored
Merge pull request #46 from dfpc-coe/local-cache
Add Local Auth Cache
2 parents d108a0b + 858c1c5 commit fed8dc9

14 files changed

Lines changed: 582 additions & 277 deletions

File tree

.github/workflows/ecr_api.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
ref: ${{github.event.pull_request.head.sha || github.sha}}
2626

2727
- name: Configure AWS Credentials
28-
uses: aws-actions/configure-aws-credentials@v4
28+
uses: aws-actions/configure-aws-credentials@v6
2929
with:
3030
role-to-assume: arn:aws-us-gov:iam::${{secrets.AWS_ACCOUNT_ID}}:role/Github-ECR-Builder
3131
role-session-name: GithubECRBuilder
@@ -46,7 +46,7 @@ jobs:
4646
AWS_REGION: ${{secrets.AWS_REGION}}
4747

4848
- name: Configure AWS Credentials (STAGING)
49-
uses: aws-actions/configure-aws-credentials@v4
49+
uses: aws-actions/configure-aws-credentials@v6
5050
with:
5151
role-to-assume: arn:aws:iam::${{secrets.AWS_STAGING_ACCOUNT_ID}}:role/Github-ECR-Builder
5252
role-session-name: GithubECRBuilder

.github/workflows/release.yml

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,40 @@ jobs:
1818
attestations: write
1919
id-token: write
2020
steps:
21-
- uses: actions/checkout@v4
22-
23-
- name: Get tag
24-
id: tag
25-
uses: dawidd6/action-get-tag@v1
21+
- uses: actions/checkout@v6
2622

2723
- name: Generate CHANGELOG
28-
run: grep -Pzo "### ${{steps.tag.outputs.tag}}(?s).*?(?=###)" CHANGELOG.md > RELEASE
24+
run: grep -Pzo "### ${{ github.ref_name }}(?s).*?(?=###)" CHANGELOG.md > RELEASE
2925

3026
- name: Github Release
3127
uses: softprops/action-gh-release@v2
3228
with:
3329
body_path: RELEASE
3430

3531
- name: Log in to the Container registry
36-
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
32+
uses: docker/login-action@v4
3733
with:
3834
registry: ${{ env.REGISTRY }}
3935
username: ${{ github.actor }}
4036
password: ${{ secrets.GITHUB_TOKEN }}
4137

4238
- name: Extract metadata (tags, labels) for Docker
4339
id: meta
44-
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
40+
uses: docker/metadata-action@v6
4541
with:
4642
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
4743

4844
- name: Build and push Docker image
4945
id: push
50-
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
46+
uses: docker/build-push-action@v7
5147
with:
5248
context: .
5349
push: true
5450
tags: ${{ steps.meta.outputs.tags }}
5551
labels: ${{ steps.meta.outputs.labels }}
5652

5753
- name: Generate artifact attestation
58-
uses: actions/attest-build-provenance@v2
54+
uses: actions/attest-build-provenance@v4
5955
with:
6056
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
6157
subject-digest: ${{ steps.push.outputs.digest }}

.github/workflows/root.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ jobs:
1616
runs-on: ubuntu-latest
1717
if: github.event.pull_request.draft == false
1818
steps:
19-
- uses: actions/checkout@v4
19+
- uses: actions/checkout@v6
2020
with:
2121
ref: ${{github.event.pull_request.head.sha || github.sha}}
2222

23-
- uses: actions/setup-node@v4
23+
- uses: actions/setup-node@v6
2424
with:
2525
node-version: 22
2626
registry-url: https://registry.npmjs.org/

Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
ARG MEDIAMTX_REPO=https://github.com/EricHenry/mediamtx.git
2-
ARG MEDIAMTX_BRANCH=mpegts-demuxing
1+
ARG MEDIAMTX_REPO=https://github.com/bluenviron/mediamtx.git
2+
ARG MEDIAMTX_BRANCH=v1.17.0
33

44
# Build Stage
55
FROM golang:1.25-alpine AS builder
@@ -12,12 +12,12 @@ RUN apk add --no-cache git make
1212
WORKDIR /build
1313
RUN git clone ${MEDIAMTX_REPO} . \
1414
&& git checkout ${MEDIAMTX_BRANCH} \
15-
&& echo "v0.0.0-custom" > internal/core/VERSION \
15+
&& case "${MEDIAMTX_REPO}" in *bluenviron/mediamtx*) ;; *) echo "v0.0.0-custom" > internal/core/VERSION ;; esac \
1616
&& go generate ./... \
1717
&& go build -o /mediamtx .
1818

1919
# Final Stage
20-
FROM bluenviron/mediamtx:1.16.0-ffmpeg
20+
FROM bluenviron/mediamtx:1.17.0-ffmpeg
2121

2222
# Copy custom binary
2323
COPY --from=builder /mediamtx /mediamtx

lib/auth.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { Request } from 'express';
1+
import type { Request } from 'express';
22
import Err from '@openaddresses/batch-error';
33
import jwt from 'jsonwebtoken';
44
import type { Config } from './config.js';
55

6+
type AuthRequestLike = Pick<Request, 'headers' | 'header'> & {
7+
query?: unknown;
8+
};
9+
610
export type AuthResourceAccepted = {
711
access: AuthResourceAccess;
812
id?: string | number | undefined;
@@ -66,7 +70,7 @@ export default class Auth {
6670
*/
6771
static async is_auth(
6872
config: Config,
69-
req: Request<any, any, any, any>,
73+
req: AuthRequestLike,
7074
opts: {
7175
token?: boolean;
7276
anyResources?: boolean;
@@ -109,7 +113,7 @@ export default class Auth {
109113

110114
async function auth_request(
111115
config: Config,
112-
req: Request<any, any, any, any>,
116+
req: AuthRequestLike,
113117
opts?: {
114118
token: boolean
115119
}
@@ -130,8 +134,9 @@ async function auth_request(
130134
} else if (
131135
opts
132136
&& opts.token
133-
&& req.query
134-
&& req.query.token
137+
&& typeof req.query === 'object'
138+
&& req.query !== null
139+
&& 'token' in req.query
135140
&& typeof req.query.token === 'string'
136141
) {
137142
return await tokenParser(config, req.query.token, config.SigningSecret);

lib/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export interface Config {
22
silent: boolean
3+
API_URL: string;
34
SigningSecret: string
45
CLOUDTAK_Config_media_url: string;
56
}
67

78
export const config: Config = {
89
silent: false,
10+
API_URL: String(process.env.API_URL),
911
CLOUDTAK_Config_media_url: String(process.env.CLOUDTAK_Config_media_url),
1012
SigningSecret: String(process.env.SigningSecret)
1113
};

lib/mediamtx-auth.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import jwt, { JwtPayload } from 'jsonwebtoken';
2+
import NodeCache from 'node-cache';
3+
import { fetch } from 'undici';
4+
import { Static } from '@sinclair/typebox';
5+
import type { Config } from './config.js';
6+
import { MediaMTXAuthRequest, StandardResponse } from './types.js';
7+
8+
export const MEDIA_MTX_AUTH_CACHE_TTL_SECONDS = 5 * 60;
9+
10+
type FetchResponse = {
11+
ok: boolean;
12+
status: number;
13+
text(): Promise<string>;
14+
}
15+
16+
type FetchLike = (input: URL, init?: RequestInit) => Promise<FetchResponse>;
17+
18+
export function getMediaMTXAuthCacheKey(auth: Static<typeof MediaMTXAuthRequest>): string {
19+
return JSON.stringify({
20+
user: auth.user,
21+
password: auth.password,
22+
token: auth.token || '',
23+
ip: auth.ip,
24+
action: auth.action,
25+
path: auth.path,
26+
protocol: auth.protocol,
27+
id: auth.id,
28+
query: auth.query
29+
});
30+
}
31+
32+
export function getMediaMTXAuthCacheTTL(
33+
config: Config,
34+
auth: Static<typeof MediaMTXAuthRequest>
35+
): number {
36+
const ttl = MEDIA_MTX_AUTH_CACHE_TTL_SECONDS;
37+
38+
if (auth.user !== 'management' || !auth.password) {
39+
return ttl;
40+
}
41+
42+
try {
43+
const decoded = jwt.verify(auth.password, config.SigningSecret) as JwtPayload | string;
44+
45+
if (typeof decoded === 'string' || typeof decoded.exp !== 'number') {
46+
return ttl;
47+
}
48+
49+
const remaining = Math.floor(decoded.exp - (Date.now() / 1000));
50+
return Math.max(1, Math.min(ttl, remaining));
51+
} catch {
52+
return ttl;
53+
}
54+
}
55+
56+
export async function authenticateMediaMTXRequest(
57+
config: Config,
58+
cache: NodeCache,
59+
auth: Static<typeof MediaMTXAuthRequest>,
60+
fetchImpl: FetchLike = fetch as FetchLike
61+
): Promise<{ status: number; body: Static<typeof StandardResponse>; cached: boolean }> {
62+
const cacheKey = getMediaMTXAuthCacheKey(auth);
63+
const cached = cache.get<Static<typeof StandardResponse>>(cacheKey);
64+
65+
if (cached) {
66+
return {
67+
status: 200,
68+
body: cached,
69+
cached: true
70+
};
71+
}
72+
73+
const url = new URL('/api/video/auth', config.API_URL);
74+
const resp = await fetchImpl(url, {
75+
method: 'POST',
76+
headers: {
77+
'Content-Type': 'application/json'
78+
},
79+
body: JSON.stringify(auth)
80+
});
81+
82+
const text = await resp.text();
83+
84+
let body: Static<typeof StandardResponse> = {
85+
status: resp.status,
86+
message: text || (resp.ok ? 'Authorized' : 'Unauthorized')
87+
};
88+
89+
if (text) {
90+
try {
91+
const parsed = JSON.parse(text) as Partial<Static<typeof StandardResponse>>;
92+
93+
if (typeof parsed.status === 'number' && typeof parsed.message === 'string') {
94+
body = {
95+
status: parsed.status,
96+
message: parsed.message
97+
};
98+
}
99+
} catch {
100+
// Non-JSON responses are normalized above.
101+
}
102+
}
103+
104+
if (resp.ok) {
105+
cache.set(cacheKey, body, getMediaMTXAuthCacheTTL(config, auth));
106+
}
107+
108+
return {
109+
status: resp.status,
110+
body,
111+
cached: false
112+
};
113+
}

lib/proxy.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Err from '@openaddresses/batch-error';
22
import { fetch, Headers } from 'undici';
33
import type { Response } from 'express';
44

5+
type ProxyBody = string | Buffer | object;
6+
57
const whitelist = new Set([
68
'content-type',
79
'content-length',
@@ -15,16 +17,20 @@ export default async function proxy(
1517
url: string,
1618
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
1719
headers?: Record<string, string | string[] | undefined>,
18-
body?: string | Buffer | object
20+
body?: ProxyBody
1921
},
2022
res: Response
2123
): Promise<void> {
2224
try {
2325
if (!opts.headers) opts.headers = {};
2426

27+
let body: string | Buffer | undefined;
28+
2529
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof Buffer)) {
26-
opts.body = JSON.stringify(opts.body);
30+
body = JSON.stringify(opts.body);
2731
if (!opts.headers['content-type']) opts.headers['content-type'] = 'application/json';
32+
} else {
33+
body = opts.body;
2834
}
2935

3036
delete opts.headers['content-length'];
@@ -48,7 +54,7 @@ export default async function proxy(
4854
const resp = await fetch(opts.url, {
4955
method: opts.method ?? 'GET',
5056
headers: outHeaders,
51-
body: opts.body as any
57+
body
5258
});
5359

5460
const filteredHeaders: Record<string, string> = {};
@@ -59,16 +65,11 @@ export default async function proxy(
5965
res.writeHead(resp.status, filteredHeaders);
6066

6167
if (resp.body) {
62-
const reader: any = (resp.body as any).getReader ? (resp.body as any).getReader() : null;
63-
if (reader) {
64-
while (true) {
65-
const { done, value } = await reader.read();
66-
if (done) break;
67-
res.write(Buffer.from(value));
68-
}
69-
} else {
70-
const ab = await resp.arrayBuffer();
71-
res.write(Buffer.from(ab));
68+
const reader = resp.body.getReader();
69+
while (true) {
70+
const { done, value } = await reader.read();
71+
if (done) break;
72+
if (value) res.write(Buffer.from(value));
7273
}
7374
}
7475

lib/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,15 @@ export const StandardResponse = Type.Object({
2424
status: Type.Integer(),
2525
message: Type.String()
2626
});
27+
28+
export const MediaMTXAuthRequest = Type.Object({
29+
user: Type.String(),
30+
password: Type.String(),
31+
token: Type.Optional(Type.String()),
32+
ip: Type.String(),
33+
action: Type.String(),
34+
path: Type.String(),
35+
protocol: Type.String(),
36+
id: Type.Union([Type.Null(), Type.String()]),
37+
query: Type.String()
38+
});

mediamtx.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ readTimeout: 30s
99
writeTimeout: 30s
1010

1111
authMethod: http
12-
authHTTPAddress: API_URL/api/video/auth
12+
authHTTPAddress: http://127.0.0.1:9997/auth
1313
authHTTPExclude: []
1414
authInternalUsers: []
1515

0 commit comments

Comments
 (0)