added partial content support (not supported with tagging rn), added content-length header (fixes super aids), updated dependencies
This commit is contained in:
parent
873e1002b9
commit
ea88be5762
|
@ -8,10 +8,11 @@ edition = "2021"
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
block-modes = "0.8"
|
blowfish = "0.9"
|
||||||
blowfish = "0.8"
|
aes = "0.8"
|
||||||
aes = "0.7"
|
|
||||||
md-5 = "0.10"
|
md-5 = "0.10"
|
||||||
|
cipher = { version = "0.4", features = ["block-padding"] }
|
||||||
|
cbc = "0.1"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
|
File diff suppressed because one or more lines are too long
218
index.js
218
index.js
|
@ -63,10 +63,6 @@ async function gw_api_call(method, params) {
|
||||||
return json.results
|
return json.results
|
||||||
}
|
}
|
||||||
|
|
||||||
let license_token
|
|
||||||
let checkForm
|
|
||||||
let sid
|
|
||||||
let access_token
|
|
||||||
router.get('/:type/:id', async request => {
|
router.get('/:type/:id', async request => {
|
||||||
const { query } = request
|
const { query } = request
|
||||||
let { type, id } = request.params
|
let { type, id } = request.params
|
||||||
|
@ -80,42 +76,6 @@ router.get('/:type/:id', async request => {
|
||||||
return new Response("Invalid ID", { status: 400, headers: { 'content-type': 'text/plain' } })
|
return new Response("Invalid ID", { status: 400, headers: { 'content-type': 'text/plain' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// cookie/token stuff isn't needed for m3u8 playlists
|
|
||||||
if (type === 'track') {
|
|
||||||
if (id >= 0) { // checks if not user-upped track
|
|
||||||
license_token = await KV.get('license_token')
|
|
||||||
checkForm = await KV.get('checkForm')
|
|
||||||
sid = await KV.get('sid')
|
|
||||||
|
|
||||||
if (license_token === null) {
|
|
||||||
const user_data = await gw_api_call('deezer.getUserData')
|
|
||||||
if (user_data.constructor.name === 'Response') {
|
|
||||||
return user_data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user_data.USER.USER_ID === 0) {
|
|
||||||
return new Response('Invalid arl', { status: 500, headers: { 'content-type': 'text/plain' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
license_token = user_data.USER.OPTIONS.license_token
|
|
||||||
|
|
||||||
await KV.put('license_token', license_token, { expirationTtl: 3600 })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
access_token = await KV.get('access_token')
|
|
||||||
if (access_token === null) {
|
|
||||||
const response = await fetch(`https://connect.deezer.com/oauth/access_token.php?grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}&output=json`)
|
|
||||||
const json = await response.json()
|
|
||||||
if (json.error !== undefined) {
|
|
||||||
return new Response("Couldn't get access token from Deezer", { status: 500, headers: { 'content-type': 'text/plain' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
access_token = json.access_token
|
|
||||||
await KV.put('access_token', access_token, { expirationTtl: parseInt(json.expires) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let format = query.f
|
let format = query.f
|
||||||
if (format === undefined) {
|
if (format === undefined) {
|
||||||
format = '320'
|
format = '320'
|
||||||
|
@ -138,22 +98,71 @@ router.get('/:type/:id', async request => {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'track':
|
case 'track':
|
||||||
return await track(id, format, tagging)
|
return await track(id, format, tagging, request.headers.get('range'))
|
||||||
case 'album':
|
case 'album':
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
return await m3u8(type, id, format, tagging, request.headers.get('host'))
|
return await m3u8(type, id, format, tagging, request.headers.get('host'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function track(id, format, tagging) {
|
let license_token
|
||||||
|
let checkForm
|
||||||
|
let sid
|
||||||
|
async function auth_gw() {
|
||||||
|
license_token = await KV.get('license_token')
|
||||||
|
checkForm = await KV.get('checkForm')
|
||||||
|
sid = await KV.get('sid')
|
||||||
|
|
||||||
|
if (license_token === null) {
|
||||||
|
const user_data = await gw_api_call('deezer.getUserData')
|
||||||
|
if (user_data.constructor.name === 'Response') {
|
||||||
|
return user_data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user_data.USER.USER_ID === 0) {
|
||||||
|
return new Response('Invalid arl', { status: 500, headers: { 'content-type': 'text/plain' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
license_token = user_data.USER.OPTIONS.license_token
|
||||||
|
|
||||||
|
await KV.put('license_token', license_token, { expirationTtl: 3600 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let access_token
|
||||||
|
async function auth_dzapi() {
|
||||||
|
access_token = await KV.get('access_token')
|
||||||
|
if (access_token === null) {
|
||||||
|
const response = await fetch(`https://connect.deezer.com/oauth/access_token.php?grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}&output=json`)
|
||||||
|
const json = await response.json()
|
||||||
|
if (json.error !== undefined) {
|
||||||
|
return new Response("Couldn't get access token from Deezer", { status: 500, headers: { 'content-type': 'text/plain' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
access_token = json.access_token
|
||||||
|
await KV.put('access_token', access_token, { expirationTtl: parseInt(json.expires) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function track(id, format, tagging, range_header) {
|
||||||
// other users' user-upped tracks cannot be downloaded with the gw-light API
|
// other users' user-upped tracks cannot be downloaded with the gw-light API
|
||||||
let json;
|
let json;
|
||||||
if (id >= 0) {
|
if (id >= 0) {
|
||||||
|
let err_auth = await auth_gw()
|
||||||
|
if (err_auth) {
|
||||||
|
return err_auth
|
||||||
|
}
|
||||||
|
|
||||||
json = await gw_api_call('song.getData', { 'SNG_ID': id })
|
json = await gw_api_call('song.getData', { 'SNG_ID': id })
|
||||||
if (json.constructor.name === 'Response') {
|
if (json.constructor.name === 'Response') {
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
} else { // user-upped track
|
} else { // user-upped track
|
||||||
|
let err_auth = await auth_dzapi()
|
||||||
|
if (err_auth) {
|
||||||
|
return err_auth
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`https://api.deezer.com/track/${id}?access_token=${access_token}`)
|
const response = await fetch(`https://api.deezer.com/track/${id}?access_token=${access_token}`)
|
||||||
json = await response.json()
|
json = await response.json()
|
||||||
if (json.error !== undefined) {
|
if (json.error !== undefined) {
|
||||||
|
@ -176,10 +185,21 @@ async function track(id, format, tagging) {
|
||||||
format = 'misc' // user-upped tracks always use 'misc' as format
|
format = 'misc' // user-upped tracks always use 'misc' as format
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json['FILESIZE_' + formats[format].gw] == false) {
|
let filesize = json['FILESIZE_' + formats[format].gw]
|
||||||
|
if (filesize == false) {
|
||||||
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
|
return new Response('Format unavailable', { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let range_start = null
|
||||||
|
let range_end = null
|
||||||
|
if (!tagging && range_header !== null) {
|
||||||
|
const range_match = range_header.match(/^bytes=(\d+)-(\d*)/)
|
||||||
|
if (range_match !== null) {
|
||||||
|
range_start = parseInt(range_match[1])
|
||||||
|
range_end = parseInt(range_match[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const wasm = await import('./pkg')
|
const wasm = await import('./pkg')
|
||||||
|
|
||||||
let track_url
|
let track_url
|
||||||
|
@ -230,14 +250,60 @@ async function track(id, format, tagging) {
|
||||||
track_url = await legacy_track_url(json, format, wasm.legacy_stream_url)
|
track_url = await legacy_track_url(json, format, wasm.legacy_stream_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const track = await fetch(track_url)
|
filesize = parseInt(filesize)
|
||||||
if (track.status !== 200) {
|
|
||||||
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = json.SNG_TITLE
|
let title = json.SNG_TITLE
|
||||||
if (json.VERSION) title += ` ${json.VERSION}`
|
if (json.VERSION) title += ` ${json.VERSION}`
|
||||||
|
|
||||||
|
let init = {}
|
||||||
|
let resp_init = {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'content-type': formats[format].mime,
|
||||||
|
'content-disposition': `inline; filename="${title.replaceAll('"', '\\"')} [${id}].${formats[format].ext}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tagging) {
|
||||||
|
resp_init.headers['accept-ranges'] = 'bytes'
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp_size = filesize
|
||||||
|
let fixed_range_start
|
||||||
|
let fixed_range_end = filesize
|
||||||
|
let bytes_remove_start = 0
|
||||||
|
let bytes_remove_end = 0
|
||||||
|
if (range_start !== null) {
|
||||||
|
if (isNaN(range_end)) {
|
||||||
|
range_end = filesize
|
||||||
|
} else {
|
||||||
|
range_end++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range_start < 0 || range_start > filesize || range_end < range_start || range_end > filesize) {
|
||||||
|
return new Response('Range invalid', { status: 416, headers: { 'content-type': 'text/plain' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes_remove_start = range_start % 2048
|
||||||
|
bytes_remove_end = 2048 - range_end % 2048
|
||||||
|
|
||||||
|
fixed_range_start = range_start - bytes_remove_start
|
||||||
|
fixed_range_end = range_end + bytes_remove_end
|
||||||
|
if (fixed_range_end >= filesize) {
|
||||||
|
fixed_range_end = filesize
|
||||||
|
bytes_remove_end = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
init = { headers: new Headers({ range: `bytes=${fixed_range_start}-${fixed_range_end - 1}` }) }
|
||||||
|
resp_init.status = 206
|
||||||
|
resp_init.headers['content-range'] = `bytes ${range_start}-${range_end - 1}/${filesize}`
|
||||||
|
resp_size = range_end - range_start
|
||||||
|
}
|
||||||
|
|
||||||
|
const track = await fetch(track_url, init)
|
||||||
|
if (![200, 206].includes(track.status)) {
|
||||||
|
return new Response("Couldn't get track stream", { status: 403, headers: { 'content-type': 'text/plain' } })
|
||||||
|
}
|
||||||
|
|
||||||
let id3
|
let id3
|
||||||
if (tagging) {
|
if (tagging) {
|
||||||
id3 = new ID3Writer(Buffer.alloc(0));
|
id3 = new ID3Writer(Buffer.alloc(0));
|
||||||
|
@ -287,9 +353,11 @@ async function track(id, format, tagging) {
|
||||||
}
|
}
|
||||||
|
|
||||||
id3.addTag();
|
id3.addTag();
|
||||||
|
|
||||||
|
resp_size += id3.arrayBuffer.byteLength
|
||||||
}
|
}
|
||||||
|
|
||||||
let { readable, writable } = new TransformStream()
|
let { readable, writable } = new FixedLengthStream(resp_size)
|
||||||
const writer = writable.getWriter()
|
const writer = writable.getWriter()
|
||||||
|
|
||||||
if (tagging) {
|
if (tagging) {
|
||||||
|
@ -303,43 +371,46 @@ async function track(id, format, tagging) {
|
||||||
|
|
||||||
const cipher = new wasm.Cipher(String(id))
|
const cipher = new wasm.Cipher(String(id))
|
||||||
|
|
||||||
const length = parseInt(track.headers.get('Content-Length'))
|
pipeDecryptedStream(writer, track.body, fixed_range_end, cipher, fixed_range_start, bytes_remove_start, bytes_remove_end)
|
||||||
|
|
||||||
pipeDecryptedStream(writer, track.body, length, cipher)
|
return new Response(readable, resp_init)
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'content-type': formats[format].mime,
|
|
||||||
'content-disposition': `inline; filename="${title.replaceAll('"', '\\"')} [${id}].${formats[format].ext}"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(readable, { status: 200, headers })
|
async function pipeDecryptedStream(writer, body, length, cipher, fixed_range_start, bytes_remove_start, bytes_remove_end) {
|
||||||
}
|
|
||||||
|
|
||||||
async function pipeDecryptedStream(writer, body, length, cipher) {
|
|
||||||
const reader = body.getReader({ mode: 'byob' })
|
const reader = body.getReader({ mode: 'byob' })
|
||||||
let byteCount = 0
|
let byte_count = 0
|
||||||
|
if (fixed_range_start) {
|
||||||
|
byte_count = fixed_range_start
|
||||||
|
}
|
||||||
let end = false
|
let end = false
|
||||||
while (!end) {
|
while (!end) {
|
||||||
end = byteCount + 2048 > length
|
end = byte_count + 2048 >= length
|
||||||
let minBytes
|
let min_bytes
|
||||||
if (!end) {
|
if (!end) {
|
||||||
minBytes = 2048
|
min_bytes = 2048
|
||||||
} else {
|
} else {
|
||||||
minBytes = length - byteCount
|
min_bytes = length - byte_count
|
||||||
}
|
}
|
||||||
|
|
||||||
let chunk = (await reader.readAtLeast(minBytes, new Uint8Array(2048))).value
|
let chunk = (await reader.readAtLeast(min_bytes, new Uint8Array(2048))).value
|
||||||
|
|
||||||
if (byteCount % 6144 === 0 && !end) {
|
if (byte_count % 6144 === 0 && !end) {
|
||||||
// encrypted chunk
|
// encrypted chunk
|
||||||
cipher.decrypt_chunk(chunk)
|
cipher.decrypt_chunk(chunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.write(chunk)
|
if (bytes_remove_start !== 0) {
|
||||||
byteCount += 2048
|
chunk = chunk.slice(bytes_remove_start)
|
||||||
|
bytes_remove_start = 0
|
||||||
|
}
|
||||||
|
if (end && bytes_remove_end !== 0) {
|
||||||
|
chunk = chunk.slice(0, chunk.length - bytes_remove_end)
|
||||||
|
}
|
||||||
|
|
||||||
|
byte_count += 2048
|
||||||
|
await writer.write(chunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.cancel()
|
|
||||||
writer.close()
|
writer.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,17 +456,6 @@ router.get('/', () => {
|
||||||
|
|
||||||
router.all("*", () => new Response("not found", { status: 404, headers: { 'content-type': 'text/plain' } }))
|
router.all("*", () => new Response("not found", { status: 404, headers: { 'content-type': 'text/plain' } }))
|
||||||
|
|
||||||
async function handleRequest(request) {
|
|
||||||
const r = new Router()
|
|
||||||
r.get('/', () => indexHandler())
|
|
||||||
r.get('/track/-?\\d+', () => handler('track', request))
|
|
||||||
r.get('/album/\\d+', () => handler('album', request))
|
|
||||||
r.get('/playlist/\\d+', () => handler('playlist', request))
|
|
||||||
|
|
||||||
const resp = await r.route(request)
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener('fetch', event =>
|
addEventListener('fetch', event =>
|
||||||
event.respondWith(router.handle(event.request))
|
event.respondWith(router.handle(event.request))
|
||||||
)
|
)
|
|
@ -14,9 +14,9 @@
|
||||||
"serverless-cloudflare-workers": "^1.2.0"
|
"serverless-cloudflare-workers": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.5.0",
|
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
|
||||||
"html-loader": "^1.3.2",
|
"html-loader": "^1.3.2",
|
||||||
"prettier": "^1.17.0"
|
"prettier": "^2.5.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
|
@ -62,9 +62,9 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@wasm-tool/wasm-pack-plugin": {
|
"node_modules/@wasm-tool/wasm-pack-plugin": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.6.0.tgz",
|
||||||
"integrity": "sha512-qsGJ953zrXZdXW58cfYOh2nBXp0SYBsFhkxqh9p4JK8cXllEzHeRXoVO+qtgEB31+s1tsL8eda3Uy97W/7yOAg==",
|
"integrity": "sha512-Iax4nEgIvVCZqrmuseJm7ln/muWpg7uT5fXMAT0crYo+k5JTuZE58DJvBQoeIAegA3IM9cZgfkcZjAOUCPsT1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^2.4.1",
|
"chalk": "^2.4.1",
|
||||||
|
@ -3170,15 +3170,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "1.19.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
|
||||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin-prettier.js"
|
"prettier": "bin-prettier.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/process": {
|
"node_modules/process": {
|
||||||
|
@ -4851,9 +4851,9 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"@wasm-tool/wasm-pack-plugin": {
|
"@wasm-tool/wasm-pack-plugin": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.6.0.tgz",
|
||||||
"integrity": "sha512-qsGJ953zrXZdXW58cfYOh2nBXp0SYBsFhkxqh9p4JK8cXllEzHeRXoVO+qtgEB31+s1tsL8eda3Uy97W/7yOAg==",
|
"integrity": "sha512-Iax4nEgIvVCZqrmuseJm7ln/muWpg7uT5fXMAT0crYo+k5JTuZE58DJvBQoeIAegA3IM9cZgfkcZjAOUCPsT1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"chalk": "^2.4.1",
|
"chalk": "^2.4.1",
|
||||||
|
@ -7458,9 +7458,9 @@
|
||||||
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
|
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "1.19.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
|
||||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"process": {
|
"process": {
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
"author": "uh_wot <uhwot@protonmail.com>",
|
"author": "uh_wot <uhwot@protonmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.5.0",
|
"@wasm-tool/wasm-pack-plugin": "^1.6.0",
|
||||||
"html-loader": "^1.3.2",
|
"html-loader": "^1.3.2",
|
||||||
"prettier": "^1.17.0"
|
"prettier": "^2.5.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
|
|
21
src/lib.rs
21
src/lib.rs
|
@ -1,20 +1,21 @@
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use aes::Aes128Enc;
|
||||||
use blowfish::Blowfish;
|
use blowfish::Blowfish;
|
||||||
use aes::Aes128;
|
|
||||||
use block_modes::{BlockMode, Cbc, Ecb};
|
|
||||||
use block_modes::block_padding::{NoPadding, ZeroPadding};
|
|
||||||
use md5::{Md5, Digest};
|
use md5::{Md5, Digest};
|
||||||
|
use cipher::{
|
||||||
|
BlockEncrypt, BlockDecryptMut, KeyInit, KeyIvInit,
|
||||||
|
block_padding::{NoPadding, ZeroPadding}
|
||||||
|
};
|
||||||
|
|
||||||
type BfCbc = Cbc<Blowfish, NoPadding>;
|
type BfCbcDec = cbc::Decryptor<Blowfish>;
|
||||||
type Aes128Ecb = Ecb<Aes128, ZeroPadding>;
|
|
||||||
|
|
||||||
const TRACK_CDN_KEY: [u8; 16] = [106, 111, 54, 97, 101, 121, 54, 104, 97, 105, 100, 50, 84, 101, 105, 104];
|
const TRACK_CDN_KEY: [u8; 16] = [106, 111, 54, 97, 101, 121, 54, 104, 97, 105, 100, 50, 84, 101, 105, 104];
|
||||||
const BF_SECRET: [u8; 16] = [103, 52, 101, 108, 53, 56, 119, 99, 48, 122, 118, 102, 57, 110, 97, 49];
|
const BF_SECRET: [u8; 16] = [103, 52, 101, 108, 53, 56, 119, 99, 48, 122, 118, 102, 57, 110, 97, 49];
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct Cipher {
|
pub struct Cipher {
|
||||||
cipher: BfCbc
|
cipher: BfCbcDec
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
@ -27,11 +28,11 @@ impl Cipher {
|
||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
key[i] = id_md5[i] ^ id_md5[i+16] ^ BF_SECRET[i]
|
key[i] = id_md5[i] ^ id_md5[i+16] ^ BF_SECRET[i]
|
||||||
};
|
};
|
||||||
Self { cipher: BfCbc::new_from_slices(&key, &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap() }
|
Self { cipher: BfCbcDec::new_from_slices(&key, &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrypt_chunk(&self, chunk: &mut [u8]) {
|
pub fn decrypt_chunk(&self, chunk: &mut [u8]) {
|
||||||
self.cipher.clone().decrypt(chunk).unwrap();
|
self.cipher.clone().decrypt_padded_mut::<NoPadding>(chunk).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,8 +59,8 @@ pub fn legacy_stream_url(md5_origin: &str, format: &str, id: &str, media_version
|
||||||
&[b'\xa4'],
|
&[b'\xa4'],
|
||||||
].concat();
|
].concat();
|
||||||
|
|
||||||
let cipher = Aes128Ecb::new_from_slices(&TRACK_CDN_KEY, Default::default()).unwrap();
|
let cipher = Aes128Enc::new_from_slice(&TRACK_CDN_KEY).unwrap();
|
||||||
let ciphertext = cipher.encrypt_vec(&metadata_hash);
|
let ciphertext = cipher.encrypt_padded_vec::<ZeroPadding>(&metadata_hash);
|
||||||
|
|
||||||
format!("https://cdns-proxy-{}.dzcdn.net/mobile/1/{}", md5_origin.chars().next().unwrap(), hex::encode(ciphertext))
|
format!("https://cdns-proxy-{}.dzcdn.net/mobile/1/{}", md5_origin.chars().next().unwrap(), hex::encode(ciphertext))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ workers_dev = true
|
||||||
kv_namespaces = [
|
kv_namespaces = [
|
||||||
{ binding = "KV", id = "974c0967a84e415daa054bbbcc7f80c6", preview_id = "cfcc6491f3484cbca664913836635113" }
|
{ binding = "KV", id = "974c0967a84e415daa054bbbcc7f80c6", preview_id = "cfcc6491f3484cbca664913836635113" }
|
||||||
]
|
]
|
||||||
compatibility_date = "2022-01-23"
|
compatibility_date = "2022-02-27"
|
||||||
|
|
||||||
# [secrets]
|
# [secrets]
|
||||||
# ARL
|
# ARL
|
Loading…
Reference in New Issue