边缘开发者平台
  • 边缘函数
    • 概述
    • 快速指引
    • 操作指引
      • 函数管理
      • 触发配置
    • Runtime APIs
      • addEventListener
      • Cache
      • Cookies
      • Encoding
      • Fetch
      • FetchEvent
      • Headers
      • Request
      • Response
      • Streams
        • ReadableStream
        • ReadableStreamBYOBReader
        • ReadableStreamDefaultReader
        • TransformStream
        • WritableStream
        • WritableStreamDefaultWriter
      • Web Crypto
      • Web standards
      • Images
        • ImageProperties
    • 示例函数
      • 返回 HTML 页面
      • 返回 JSON
      • Fetch 远程资源
      • 请求头鉴权
      • 修改响应头
      • AB 测试
      • 设置 Cookie
      • 基于请求区域重定向
      • Cache API 使用
      • 缓存 POST 请求
      • 流式响应
      • 合并资源流式响应
      • 防篡改校验
      • m3u8 改写与鉴权
      • 图片自适应缩放
      • 图片自适应 WebP
      • 自定义 Referer 限制规则
      • 远程鉴权
      • HMAC 数字签名
      • 自定义下载文件名
      • 获取客户端 IP
    • 最佳实践
      • 通过边缘函数实现自适应图片格式转换

m3u8 改写与鉴权

该示例对 m3u8 改写,添加 Type A 鉴权,实现访问 .m3u8 与 .ts 片段资源的权限控制。开发者可根据需要修改代码,支持其他鉴权方式。

示例代码

// Type A鉴权私钥,请自行设定并防止泄漏
const PK = '0123456789';
// 加密校验 key 的有效时间(秒)
const TTL = 60;
const KEY_NAME = 'key';
const UID = 0;
const SUFFIX_LIST = ['.m3u8', '.ts'];

addEventListener('fetch', (event) => {
event.respondWith(handleEvent(event));
});

async function handleEvent(event) {
try {
const { request } = event;
const urlInfo = new URL(request.url);
const suffix = getSuffix(urlInfo.pathname);
// 检查文件后缀为:.m3u8,.ts
if (!SUFFIX_LIST.includes(suffix)) {
return fetch(request);
}
// Type A 鉴权
const checkResult = await checkTypeA(urlInfo);
if (!checkResult.flag) {
return new Response(checkResult.message, {
status: 403,
headers: {
'X-Auth-Err': checkResult.message
},
});
}
// 改写 .m3u8 并响应
if (suffix === '.m3u8') {
return fetchM3u8({
request,
querySign: {
basePath: urlInfo.pathname.substring(0, urlInfo.pathname.lastIndexOf('/')),
...checkResult.querySign,
}
});
}
// 响应 .ts 资源
if (suffix === '.ts') {
return fetchTs(request);
}
} catch (error) {
return new Response(error.stack, { status: 544 });
}
return fetch(request);
}

async function checkTypeA(urlInfo) {
const sign = urlInfo.searchParams.get(KEY_NAME) || '';
const elements = sign.split('-');

if (elements.length !== 4) {
return {
flag: false,
message: 'Invalid Sign Format',
};
}

const [ts, rand, uid, md5hash] = elements;
if (ts === undefined || rand === undefined || uid === undefined || md5hash === undefined) {
return {
flag: false,
message: 'Invalid Sign Format',
};
}

if (!isNumber(ts)) {
return {
flag: false,
message: 'Sign Expired',
};
}

if (Date.now() > (Number(ts) + TTL) * 1000) {
return {
flag: false,
message: 'Sign Expired',
};
}

const hash = await md5([urlInfo.pathname, ts, rand, uid, PK].join('-'));
if (hash !== md5hash) {
return {
flag: false,
message: 'Verify Sign Failed',
};
}
return {
flag: true,
message: 'success',
querySign: {
rand,
uid,
md5hash,
ts,
},
};
}

async function fetchM3u8({ request, querySign }) {
request.headers.delete('Accept-Encoding');
let response = null;
try {
response = await fetch(request);
if (response.status !== 200) {
return response;
}
} catch (error) {
return new Response('', {
status: 504,
headers: { 'X-Fetch-Err': 'Invalid Origin' }
});
}

const content = await response.text();
const lines = content.split('\n');

const contentArr = await Promise.all(
lines.map(line => rewriteLine({ line, querySign }))
);
return new Response(contentArr.join('\n'), response);
}

async function fetchTs(request) {
let response = null;
try {
response = await fetch(request);
if (response.status !== 200) {
return response;
}
} catch (error) {
return new Response('', {
status: 504,
headers: { 'X-Fetch-Err': 'Invalid Origin' }
});
}
return response;
}

async function rewriteLine({ line, querySign }) {
// 跳过空行
if (/^\s*$/.test(line)) {
return line;
}
if (line.charAt(0) === '#') {
// 处理 #EXT-X-MAP
if (line.startsWith('#EXT-X-MAP')) {
const key = await createSign(querySign, line);
line = line.replace(/URI="([^"]+)"/, (matched, p1) => {
return p1 ? matched.replace(p1, `${p1}?key=${key}`) : matched;
});
}
return line;
}
const key = await createSign(querySign, line);

return `${line}?${KEY_NAME}=${key}`;
}

async function createSign(querySign, line) {
const { ts, rand, uid = 0 } = querySign;
const pathname = `${querySign.basePath}/${line}`;

const md5hash = await md5([pathname, ts, rand, uid, PK].join('-'));
const key = [ts, rand, uid, md5hash].join('-');

return key;
}

function getSuffix(pathname) {
const suffix = pathname.match(/\.m3u8|\.ts$/);
return suffix ? suffix[0] : null;
}

function isNumber(num) {
return Number.isInteger(Number(num));
}

function bufferToHex(arr) {
return Array.prototype.map.call(arr, (x) => (x >= 16 ? x.toString(16) : '0' + x.toString(16))).join('');
}

async function md5(text) {
const buffer = await crypto.subtle.digest('MD5', TextEncoder().encode(text));
return bufferToHex(new Uint8Array(buffer));
}


示例预览

在浏览器地址栏中输入匹配到边缘函数触发规则的 URL(如:http://www.example.com/index.m3u8?key=1678873033-123456-0-32f4xxxxcabcxxxx1602xxxx6756d8f4),即可预览到示例效果。




相关参考