diff --git a/package/lean/luci-app-unblockmusic/Makefile b/package/lean/luci-app-unblockmusic/Makefile new file mode 100644 index 0000000000..1e1b72ca0e --- /dev/null +++ b/package/lean/luci-app-unblockmusic/Makefile @@ -0,0 +1,25 @@ +# Copyright (C) 2016 Openwrt.org +# +# This is free software, licensed under the Apache License, Version 2.0 . +# +# licheng +# www.maxlicheng.com +# 2019-06-08 +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for Unblock NeteaseCloudMusic +LUCI_DEPENDS:=+node +LUCI_PKGARCH:=all +PKG_NAME:=luci-app-unblockmusic +PKG_VERSION:=1 +PKG_RELEASE:=36 + +PKG_MAINTAINER:= + +include $(TOPDIR)/feeds/luci/luci.mk + + +# call BuildPackage - OpenWrt buildroot signature + diff --git a/package/lean/luci-app-unblockmusic/luasrc/controller/unblockmusic.lua b/package/lean/luci-app-unblockmusic/luasrc/controller/unblockmusic.lua new file mode 100644 index 0000000000..8cc7024d8a --- /dev/null +++ b/package/lean/luci-app-unblockmusic/luasrc/controller/unblockmusic.lua @@ -0,0 +1,22 @@ + +module("luci.controller.unblockmusic", package.seeall) + +function index() + if not nixio.fs.access("/etc/config/unblockmusic") then + return + end + + entry({"admin", "services", "unblockmusic"},firstchild(), _("解锁网易云灰色歌曲"), 50).dependent = false + + entry({"admin", "services", "unblockmusic", "general"},cbi("unblockmusic"), _("Base Setting"), 1) + entry({"admin", "services", "unblockmusic", "log"},form("unblockmusiclog"), _("Log"), 2) + + entry({"admin", "services", "unblockmusic", "status"},call("act_status")).leaf=true +end + +function act_status() + local e={} + e.running=luci.sys.call("ps | grep app.js | grep -v grep >/dev/null")==0 + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end diff --git a/package/lean/luci-app-unblockmusic/luasrc/model/cbi/unblockmusic.lua b/package/lean/luci-app-unblockmusic/luasrc/model/cbi/unblockmusic.lua new file mode 100644 index 0000000000..4ec1c4f289 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/luasrc/model/cbi/unblockmusic.lua @@ -0,0 +1,29 @@ + +mp = Map("unblockmusic", translate("解锁网易云灰色歌曲")) +mp.description = translate("原理:采用 [网易云旧链/QQ/虾米/百度/酷狗/酷我/咕咪/JOOX]等音源 替换网易云变灰歌曲链接
具体使用方法可查看github:
https://github.com/maxlicheng/luci-app-unblockmusic") + +mp:section(SimpleSection).template = "unblockmusic/unblockmusic_status" + +s = mp:section(TypedSection, "unblockmusic") +s.anonymous=true +s.addremove=false + +enabled = s:option(Flag, "enabled", translate("启用解锁")) +enabled.default = 0 +enabled.rmempty = false + +speedtype = s:option(ListValue, "musicapptype", translate("音源选择")) +speedtype:value("default", translate("默认")) +speedtype:value("NeteaseCloud", translate("网易云音乐")) +speedtype:value("QQ", translate("QQ音乐")) +speedtype:value("xiami", translate("虾米音乐")) +speedtype:value("baidu", translate("百度音乐")) +speedtype:value("kugou", translate("酷狗音乐")) +speedtype:value("koowo", translate("酷我音乐")) +speedtype:value("migu", translate("咕咪音乐")) +speedtype:value("joox", translate("JOOX音乐")) + +account = s:option(Value, "port", translate("端口号")) +account.datatype = "string" + +return mp diff --git a/package/lean/luci-app-unblockmusic/luasrc/model/cbi/unblockmusiclog.lua b/package/lean/luci-app-unblockmusic/luasrc/model/cbi/unblockmusiclog.lua new file mode 100644 index 0000000000..8d1d851934 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/luasrc/model/cbi/unblockmusiclog.lua @@ -0,0 +1,14 @@ +local fs = require "nixio.fs" +local conffile = "/tmp/unblockmusic.log" + +f = SimpleForm("logview") + +t = f:field(TextValue, "conf") +t.rmempty = true +t.rows = 15 +function t.cfgvalue() + return fs.readfile(conffile) or "" +end +t.readonly="readonly" + +return f \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/luasrc/view/unblockmusic/unblockmusic_status.htm b/package/lean/luci-app-unblockmusic/luasrc/view/unblockmusic/unblockmusic_status.htm new file mode 100644 index 0000000000..01aca57c81 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/luasrc/view/unblockmusic/unblockmusic_status.htm @@ -0,0 +1,22 @@ + + +
+

+ <%:Collecting data...%> +

+
\ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/etc/config/unblockmusic b/package/lean/luci-app-unblockmusic/root/etc/config/unblockmusic new file mode 100644 index 0000000000..6731957271 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/etc/config/unblockmusic @@ -0,0 +1,6 @@ + +config unblockmusic + option usicapptype 'default' + option port '5200' + option enabled '0' + diff --git a/package/lean/luci-app-unblockmusic/root/etc/init.d/unblockmusic b/package/lean/luci-app-unblockmusic/root/etc/init.d/unblockmusic new file mode 100755 index 0000000000..839b295856 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/etc/init.d/unblockmusic @@ -0,0 +1,24 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=10 + +TYPE=$(uci get unblockmusic.@unblockmusic[0].musicapptype) +PORT=$(uci get unblockmusic.@unblockmusic[0].port) + + +start() +{ + stop + enable=$(uci get unblockmusic.@unblockmusic[0].enabled) + [ $enable -eq 0 ] && exit 0 + node /usr/share/unblockmusic/app.js -p $PORT & +} + +stop() +{ + kill -9 $(ps | grep app.js | grep -v grep | awk '{print $1}') >/dev/null 2>&1 +} + + + diff --git a/package/lean/luci-app-unblockmusic/root/etc/ppp/ip-up.d/unblockmusic.sh b/package/lean/luci-app-unblockmusic/root/etc/ppp/ip-up.d/unblockmusic.sh new file mode 100755 index 0000000000..5e82ede98c --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/etc/ppp/ip-up.d/unblockmusic.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sleep 60 && /etc/init.d/unblockmusic restart diff --git a/package/lean/luci-app-unblockmusic/root/etc/uci-defaults/unblockmusic b/package/lean/luci-app-unblockmusic/root/etc/uci-defaults/unblockmusic new file mode 100755 index 0000000000..da27538389 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/etc/uci-defaults/unblockmusic @@ -0,0 +1,11 @@ +#!/bin/sh + +uci -q batch <<-EOF >/dev/null + delete ucitrack.@unblockmusic[-1] + add ucitrack unblockmusic + set ucitrack.@unblockmusic[-1].init=unblockmusic + commit ucitrack +EOF + +rm -f /tmp/luci-indexcache +exit 0 diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/.dockerignore b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/.dockerignore new file mode 100644 index 0000000000..0b75cc99b0 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/.dockerignore @@ -0,0 +1,15 @@ +.git +.vscode +.npmignore +.gitignore +.dockerignore + +LICENSE +Procfile +README.md + +node_modules +npm-debug.log + +Dockerfile* +docker-compose* \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/.gitignore b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/.gitignore new file mode 100644 index 0000000000..b8c8f6b011 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/.gitignore @@ -0,0 +1,72 @@ +# IDE +.vscode +.idea + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# pkg dist directory +dist/ + +# es6 transformation +browser/provider +browser/cache.js \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/Dockerfile b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/Dockerfile new file mode 100644 index 0000000000..8dda10ab93 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/Dockerfile @@ -0,0 +1,12 @@ +FROM node:lts-alpine + +ENV NODE_ENV production + +WORKDIR /usr/src/app +COPY package*.json ./ +RUN npm install --production +COPY . . + +EXPOSE 8080 + +ENTRYPOINT ["node", "app.js"] \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/LICENSE b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/LICENSE new file mode 100644 index 0000000000..cba3c7afcf --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Nzix + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/README.md b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/README.md new file mode 100644 index 0000000000..358ff60651 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/README.md @@ -0,0 +1,190 @@ +logo + +# UnblockNeteaseMusic + +解锁网易云音乐客户端变灰歌曲 + +## 特性 + +- 使用网易云旧链 / QQ / 虾米 / 百度 / 酷狗 / 酷我 / 咕咪 / JOOX 音源替换变灰歌曲链接 (默认仅启用前四) +- 为请求增加 `X-Real-IP` 参数解锁海外限制,支持指定网易云服务器 IP,支持设置上游 HTTP / HTTPS 代理 +- 完整的流量代理功能 (HTTP / HTTPS),可直接作为系统代理 (同时支持 PAC) + +## 运行 + +从源码运行 + +``` +$ node app.js +``` + +或使用 Docker + +``` +$ docker run nondanee/unblockneteasemusic +``` + +``` +$ docker-compose up +``` + +### 配置参数 + +``` +$ node app.js -h +usage: unblockneteasemusic [-v] [-p port] [-u url] [-f host] + [-o source [source ...]] [-t token] [-e url] [-s] + [-h] + +optional arguments: + -v, --version output the version number + -p port, --port port specify server port + -u url, --proxy-url url request through upstream proxy + -f host, --force-host host force the netease server ip + -o source [source ...], --match-order source [source ...] + set priority of sources + -t token, --token token set up http basic authentication + -e url, --endpoint url replace virtual endpoint with public host + -s, --strict enable proxy limitation + -h, --help output usage information +``` + +## 使用 + +**警告:本项目不提供线上 demo,请不要轻易信任使用他人提供的公开代理服务,以免发生安全问题** + +**若将服务部署到公网,强烈建议使用严格模式 (此模式下仅放行网易云音乐所属域名的请求) `-s` 限制代理范围 (需使用 PAC 或 hosts),~~或启用 Proxy Authentication `-t :` 设置代理用户名密码~~ (目前密码认证在 Windows 客户端设置和 macOS 系统设置都无法生效,请不要使用),以防代理被他人滥用** + +支持 Windows 客户端,UWP 客户端,Linux 客户端 (1.2 版本以上需要自签证书 MITM,启动客户端需要增加 `--ignore-certificate-errors` 参数),macOS 客户端 (726 版本以上需要自签证书),iOS 客户端 (配置 https endpoint 或使用自签证书),Android 客户端和网页版 (需要自签证书,需要脚本配合) + +目前除 UWP 外其它客户端均优先请求 HTTPS 接口,默认配置下本代理对网易云所有 HTTPS API 连接返回空数据,促使客户端降级使用 HTTP 接口 (新版 Linux 客户端和 macOS 客户端已无法降级) + +因 UWP 应用存在网络隔离,限制流量发送到本机,若使用的代理在 localhost,或修改的 hosts 指向 localhost,需为 "网易云音乐 UWP" 手动开启 loopback 才能使用,请以**管理员身份**执行命令 + +```powershell +checknetisolation loopbackexempt -a -n="1F8B0F94.122165AE053F_j2p0p5q0044a6" +``` + +### 方法 1. 修改 hosts + +向 hosts 文件添加两条规则 + +``` + music.163.com + interface.music.163.com +``` + +> 使用此方法必须监听 80 端口 `-p 80` +> +> **若在本机运行程序**,请指定网易云服务器 IP `-f xxx.xxx.xxx.xxx` (可在修改 hosts 前通过 `ping music.163.com` 获得) **或** 使用代理 `-u http(s)://xxx.xxx.xxx.xxx:xxx`,以防请求死循环 +> +> **Android 客户端下修改 hosts 无法直接使用**,原因和解决方法详见[云音乐安卓又搞事啦](https://jixun.moe/post/netease-android-hosts-bypass/),[安卓免 root 绕过网易云音乐 IP 限制](https://jixun.moe/post/android-block-netease-without-root/) + +### 方法 2. 设置代理 + +PAC 自动代理脚本地址 `http:///proxy.pac` + +全局代理地址填写服务器地址和端口号即可 + +| 平台 | 基础设置 | +| :------ | :------------------------------- | +| Windows | 设置 > 工具 > 自定义代理 (客户端内) | +| UWP | Windows 设置 > 网络和 Internet > 代理 | +| Linux | 系统设置 > 网络 > 网络代理 | +| macOS | 系统偏好设置 > 网络 > 高级 > 代理 | +| Android | WLAN > 修改网络 > 高级选项 > 代理 | +| iOS | 无线局域网 > HTTP 代理 > 配置代理 | + +> 代理工具和方法有很多请自行探索,欢迎在 issues 讨论 + +### ✳方法 3. 调用接口 + +作为依赖库使用 + +``` +$ npm install nondanee/UnblockNeteaseMusic +``` + +```javascript +const match = require('unblockneteasemusic') + +/** + * Set proxy or hosts if needed + */ +global.proxy = require('url').parse('http://127.0.0.1:1080') +global.hosts = {'i.y.qq.com': '59.37.96.220'} + +/** + * Find matching song from other platforms + * @param {Number} id netease song id + * @param {Array||undefined} source support netease, qq, xiami, baidu, kugou, kuwo, migu, joox + * @return {Promise} + */ +match(418602084, ['netease', 'qq', 'xiami', 'baidu']).then(song => console.log(song)) +``` + +## 效果 + +#### Windows 客户端 + + + +#### UWP 客户端 + + + +#### Linux 客户端 + + + +#### macOS 客户端 + + + +#### Android 客户端 + + + +#### iOS 客户端 + + + +## 致谢 + +感谢大佬们为逆向 eapi 所做的努力 + +使用的其它平台音源 API 出自 + +[trazyn/ieaseMusic](https://github.com/trazyn/ieaseMusic) + +[listen1/listen1_chrome_extension](https://github.com/listen1/listen1_chrome_extension) + +向所有同类项目致敬 + +[EraserKing/CloudMusicGear](https://github.com/EraserKing/CloudMusicGear) + +[EraserKing/Unblock163MusicClient](https://github.com/EraserKing/Unblock163MusicClient) + +[ITJesse/UnblockNeteaseMusic](https://github.com/ITJesse/UnblockNeteaseMusic/) + +[bin456789/Unblock163MusicClient-Xposed](https://github.com/bin456789/Unblock163MusicClient-Xposed) + +[YiuChoi/Unlock163Music](https://github.com/YiuChoi/Unlock163Music) + +[yi-ji/NeteaseMusicAbroad](https://github.com/yi-ji/NeteaseMusicAbroad) + +[stomakun/NeteaseReverseLadder](https://github.com/stomakun/NeteaseReverseLadder/) + +[fengjueming/unblock-NetEaseMusic](https://github.com/fengjueming/unblock-NetEaseMusic) + +[acgotaku/NetEaseMusicWorld](https://github.com/acgotaku/NetEaseMusicWorld) + +[mengskysama/163-Cloud-Music-Unlock](https://github.com/mengskysama/163-Cloud-Music-Unlock) + +[azureplus/163-music-unlock](https://github.com/azureplus/163-music-unlock) + +[typcn/163music-mac-client-unlock](https://github.com/typcn/163music-mac-client-unlock) + +## 许可 + +The MIT License \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/app.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/app.js new file mode 100644 index 0000000000..61e00e265e --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/app.js @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +const package = require('./package.json') +const config = require('./cli.js') +.program({name: package.name, version: package.version}) +.option(['-v', '--version'], {action: 'version'}) +.option(['-p', '--port'], {metavar: 'port', help: 'specify server port'}) +.option(['-u', '--proxy-url'], {metavar: 'url', help: 'request through upstream proxy'}) +.option(['-f', '--force-host'], {metavar: 'host', help: 'force the netease server ip'}) +.option(['-o', '--match-order'], {metavar: 'source', nargs: '+', help: 'set priority of sources'}) +.option(['-t', '--token'], {metavar: 'token', help: 'set up http basic authentication'}) +.option(['-e', '--endpoint'], {metavar: 'url', help: 'replace virtual endpoint with public host'}) +.option(['-s', '--strict'], {action: 'store_true', help: 'enable proxy limitation'}) +.option(['-h', '--help'], {action: 'help'}) +.parse(process.argv) + +config.port = (config.port || '8080').split(':').map(string => parseInt(string)) +const invalid = value => (isNaN(value) || value < 1 || value > 65535) +if(config.port.some(invalid)){ + console.log('Port must be a number higher than 0 and lower than 65535.') + process.exit(1) +} +if(config.proxyUrl && !/http(s?):\/\/.+:\d+/.test(config.proxyUrl)){ + console.log('Please check the proxy url.') + process.exit(1) +} +if(config.endpoint && !/http(s?):\/\/.+/.test(config.endpoint)){ + console.log('Please check the endpoint host.') + process.exit(1) +} +if(config.forceHost && !/\d+\.\d+\.\d+\.\d+/.test(config.forceHost)){ + console.log('Please check the server host.') + process.exit(1) +} +if(config.matchOrder){ + const provider = ['netease', 'qq', 'xiami', 'baidu', 'kugou', 'kuwo', 'migu', 'joox'] + const candidate = config.matchOrder + if(candidate.some((key, index) => index != candidate.indexOf(key))){ + console.log('Please check the duplication in match order.') + process.exit(1) + } + else if(candidate.some(key => !provider.includes(key))){ + console.log('Please check the availability of match sources.') + process.exit(1) + } + global.source = candidate +} +if(config.token && !/\S+:\S+/.test(config.token)){ + console.log('Please check the authentication token.') + process.exit(1) +} + +const parse = require('url').parse +const hook = require('./hook') +const server = require('./server') +const escape = string => string.replace(/\./g, '\\.') + +global.port = config.port +global.proxy = config.proxyUrl ? parse(config.proxyUrl) : null +global.hosts = {}, hook.target.host.forEach(host => global.hosts[host] = config.forceHost) +server.whitelist = ['music.126.net', 'vod.126.net'].map(escape) +if(config.strict) server.blacklist.push('.*') +server.authentication = config.token || null +global.endpoint = config.endpoint +if(config.endpoint) server.whitelist.push(escape(config.endpoint)) + +const dns = host => new Promise((resolve, reject) => require('dns').lookup(host, {all: true}, (error, records) => error? reject(error) : resolve(records.map(record => record.address)))) +const httpdns = host => require('./request')('POST', 'http://music.httpdns.c.163.com/d', {}, host).then(response => response.json()).then(jsonBody => jsonBody.dns[0].ips) + +Promise.all([httpdns(hook.target.host[0])].concat(hook.target.host.map(host => dns(host)))) +.then(result => { + let extra = Array.from(new Set(result.reduce((merged, array) => merged.concat(array), []))) + hook.target.host = hook.target.host.concat(extra) + server.whitelist = server.whitelist.concat(hook.target.host.map(escape)) + if(port[0]){ + server.http.listen(port[0]) + console.log(`HTTP Server running @ http://0.0.0.0:${port[0]}`) + } + if(port[1]){ + server.https.listen(port[1]) + console.log(`HTTPS Server running @ https://0.0.0.0:${port[1]}`) + } +}) +.catch(error => console.log(error)) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/bridge.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/bridge.js new file mode 100644 index 0000000000..f680658d30 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/bridge.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +const cache = require('./cache') +const parse = require('url').parse + +const router = { + qq: require('./provider/qq'), + xiami: require('./provider/xiami'), + baidu: require('./provider/baidu'), + kugou: require('./provider/kugou'), + kuwo: require('./provider/kuwo'), + migu: require('./provider/migu'), + joox: require('./provider/joox') +} + +const distributor = (url, router) => Promise.resolve().then(() => { + const route = url.pathname.slice(1).split('/').map(path => decodeURIComponent(path)) + let pointer = router, argument = decodeURIComponent(url.query) + try{argument = JSON.parse(argument)}catch(e){} + const miss = route.some(path => { + if(path in pointer) pointer = pointer[path] + else return true + }) + if(miss || typeof pointer != 'function') return Promise.reject() + //return pointer.call(null, argument) + return cache(pointer, argument, 15 * 60 * 1000) +}) + +require('http').createServer().on('request', (req, res) => + distributor(parse(req.url), router) + .then(data => res.write(data)) + .catch(() => res.writeHead(404)) + .then(() => res.end()) +).listen(parseInt(process.argv[2]) || 9000) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/README.md b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/README.md new file mode 100644 index 0000000000..7f8fb170c3 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/README.md @@ -0,0 +1,25 @@ +# Web Extension Port + +For test + +## Implementation + +- Convert node module to ES6 module which can be directly executed in Chrome 61+ without Babel +- Rewrite crypto module (using CryptoJS) and request (using XMLHttpRequest) module for browser environment +- Do matching in background and transfer result with chrome runtime communication +- Inject content script for hijacking Netease Music Web Ajax response + +## Build + +``` +$ node convert.js +``` + +## Install + +Load unpacked extension in Developer mode + +## Reference + +- [brix/crypto-js](https://github.com/brix/crypto-js) +- [JixunMoe/cuwcl4c](https://github.com/JixunMoe/cuwcl4c) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/background.html b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/background.html new file mode 100644 index 0000000000..d1b0245c76 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/background.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/background.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/background.js new file mode 100644 index 0000000000..000740c5b0 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/background.js @@ -0,0 +1,22 @@ +import match from './provider/match.js' + +chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => { + match(request.match, ['netease', 'qq', 'xiami']) + .then(song => sendResponse(song)) + .catch(e => console.log(e)) + return true +}) + +chrome.webRequest.onBeforeSendHeaders.addListener(details => { + let headers = details.requestHeaders + headers.push({name: 'X-Real-IP', value: '118.88.88.88'}) + return {requestHeaders: headers} +}, {urls: ['*://music.163.com/*']}, ['blocking', 'requestHeaders']) + +chrome.webRequest.onHeadersReceived.addListener(details => { + let headers = details.responseHeaders + if(details.initiator == "https://music.163.com" && details.type == 'media'){ + headers.push({name: 'Access-Control-Allow-Origin', value: '*'}) + } + return {responseHeaders: headers} +}, {urls: ['*://*/*']}, ['blocking', 'responseHeaders']) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/convert.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/convert.js new file mode 100644 index 0000000000..28919353ee --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/convert.js @@ -0,0 +1,43 @@ +const fs = require('fs') +const path = require('path') + +const importReplacer = (match, state, alias, file) => { + file = file.endsWith('.js') ? file : file + '.js' + return `import ${alias} from '${file}'` +} + +const converter = (input, output, processor) => { + let data = fs.readFileSync(input).toString() + if(processor){ + data = processor(data) + } + else{ + data = data.replace(/global\./g, 'window.') + data = data.replace(/(const|let|var)\s+(\w+)\s*=\s*require\(\s*['|"](.+)['|"]\s*\)/g, importReplacer) + data = data.replace(/module\.exports\s*=\s*/g, 'export default ') + } + fs.writeFileSync(output, data) +} + +converter(path.resolve(__dirname, '..', 'cache.js'), path.resolve(__dirname, '.', 'cache.js')) + +if(!fs.existsSync(path.resolve(__dirname, 'provider'))) fs.mkdirSync(path.resolve(__dirname, 'provider')) + +fs.readdirSync(path.resolve(__dirname, '..', 'provider')).filter(file => !file.includes('test')).forEach(file => { + converter(path.resolve(__dirname, '..', 'provider', file), path.resolve(__dirname, 'provider', file)) +}) + +const providerReplacer = (match, state, data) => { + let provider = [] + let imports = data.match(/\w+\s*:\s*require\(['|"].+['|"]\)/g).map(line => { + line = line.match(/(\w+)\s*:\s*require\(['|"](.+)['|"]\)/) + provider.push(line[1]) + return importReplacer(null, null, line[1], line[2]) + }) + return imports.join('\n') + '\n\n' + `${state} provider = {${provider.join(', ')}}` +} + +converter(path.resolve(__dirname, 'provider', 'match.js'), path.resolve(__dirname, 'provider', 'match.js'), data => { + data = data.replace(/(const|let|var)\s+provider\s*=\s*{([^}]+)}/g, providerReplacer) + return data +}) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/core.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/core.js new file mode 100644 index 0000000000..28e34c13c1 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/core.js @@ -0,0 +1,760 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(); + } + else if (typeof define === "function" && define.amd) { + // AMD + define([], factory); + } + else { + // Global (browser) + root.CryptoJS = factory(); + } +}(this, function () { + + /** + * CryptoJS core components. + */ + var CryptoJS = CryptoJS || (function (Math, undefined) { + /* + * Local polyfil of Object.create + */ + var create = Object.create || (function () { + function F() {}; + + return function (obj) { + var subtype; + + F.prototype = obj; + + subtype = new F(); + + F.prototype = null; + + return subtype; + }; + }()) + + /** + * CryptoJS namespace. + */ + var C = {}; + + /** + * Library namespace. + */ + var C_lib = C.lib = {}; + + /** + * Base object for prototypal inheritance. + */ + var Base = C_lib.Base = (function () { + + + return { + /** + * Creates a new object that inherits from this object. + * + * @param {Object} overrides Properties to copy into the new object. + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend: function (overrides) { + // Spawn + var subtype = create(this); + + // Augment + if (overrides) { + subtype.mixIn(overrides); + } + + // Create default initializer + if (!subtype.hasOwnProperty('init') || this.init === subtype.init) { + subtype.init = function () { + subtype.$super.init.apply(this, arguments); + }; + } + + // Initializer's prototype is the subtype object + subtype.init.prototype = subtype; + + // Reference supertype + subtype.$super = this; + + return subtype; + }, + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return {Object} The new object. + * + * @static + * + * @example + * + * var instance = MyType.create(); + */ + create: function () { + var instance = this.extend(); + instance.init.apply(instance, arguments); + + return instance; + }, + + /** + * Initializes a newly created object. + * Override this method to add some logic when your objects are created. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * init: function () { + * // ... + * } + * }); + */ + init: function () { + }, + + /** + * Copies properties into this object. + * + * @param {Object} properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn: function (properties) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + this[propertyName] = properties[propertyName]; + } + } + + // IE won't copy toString using the loop above + if (properties.hasOwnProperty('toString')) { + this.toString = properties.toString; + } + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone: function () { + return this.init.prototype.extend(this); + } + }; + }()); + + /** + * An array of 32-bit words. + * + * @property {Array} words The array of 32-bit words. + * @property {number} sigBytes The number of significant bytes in this word array. + */ + var WordArray = C_lib.WordArray = Base.extend({ + /** + * Initializes a newly created word array. + * + * @param {Array} words (Optional) An array of 32-bit words. + * @param {number} sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + init: function (words, sigBytes) { + words = this.words = words || []; + + if (sigBytes != undefined) { + this.sigBytes = sigBytes; + } else { + this.sigBytes = words.length * 4; + } + }, + + /** + * Converts this word array to a string. + * + * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return {string} The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString: function (encoder) { + return (encoder || Hex).stringify(this); + }, + + /** + * Concatenates a word array to this word array. + * + * @param {WordArray} wordArray The word array to append. + * + * @return {WordArray} This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat: function (wordArray) { + // Shortcuts + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + // Clamp excess bits + this.clamp(); + + // Concat + if (thisSigBytes % 4) { + // Copy one byte at a time + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + } else { + // Copy one word at a time + for (var i = 0; i < thatSigBytes; i += 4) { + thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; + } + } + this.sigBytes += thatSigBytes; + + // Chainable + return this; + }, + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp: function () { + // Shortcuts + var words = this.words; + var sigBytes = this.sigBytes; + + // Clamp + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + words.length = Math.ceil(sigBytes / 4); + }, + + /** + * Creates a copy of this word array. + * + * @return {WordArray} The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone.words = this.words.slice(0); + + return clone; + }, + + /** + * Creates a word array filled with random bytes. + * + * @param {number} nBytes The number of random bytes to generate. + * + * @return {WordArray} The random word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random: function (nBytes) { + var words = []; + + var r = (function (m_w) { + var m_w = m_w; + var m_z = 0x3ade68b1; + var mask = 0xffffffff; + + return function () { + m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask; + m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask; + var result = ((m_z << 0x10) + m_w) & mask; + result /= 0x100000000; + result += 0.5; + return result * (Math.random() > .5 ? 1 : -1); + } + }); + + for (var i = 0, rcache; i < nBytes; i += 4) { + var _r = r((rcache || Math.random()) * 0x100000000); + + rcache = _r() * 0x3ade67b7; + words.push((_r() * 0x100000000) | 0); + } + + return new WordArray.init(words, nBytes); + } + }); + + /** + * Encoder namespace. + */ + var C_enc = C.enc = {}; + + /** + * Hex encoding strategy. + */ + var Hex = C_enc.Hex = { + /** + * Converts a word array to a hex string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The hex string. + * + * @static + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var hexChars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + hexChars.push((bite >>> 4).toString(16)); + hexChars.push((bite & 0x0f).toString(16)); + } + + return hexChars.join(''); + }, + + /** + * Converts a hex string to a word array. + * + * @param {string} hexStr The hex string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse: function (hexStr) { + // Shortcut + var hexStrLength = hexStr.length; + + // Convert + var words = []; + for (var i = 0; i < hexStrLength; i += 2) { + words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); + } + + return new WordArray.init(words, hexStrLength / 2); + } + }; + + /** + * Latin1 encoding strategy. + */ + var Latin1 = C_enc.Latin1 = { + /** + * Converts a word array to a Latin1 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Latin1 string. + * + * @static + * + * @example + * + * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + + // Convert + var latin1Chars = []; + for (var i = 0; i < sigBytes; i++) { + var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + latin1Chars.push(String.fromCharCode(bite)); + } + + return latin1Chars.join(''); + }, + + /** + * Converts a Latin1 string to a word array. + * + * @param {string} latin1Str The Latin1 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); + */ + parse: function (latin1Str) { + // Shortcut + var latin1StrLength = latin1Str.length; + + // Convert + var words = []; + for (var i = 0; i < latin1StrLength; i++) { + words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); + } + + return new WordArray.init(words, latin1StrLength); + } + }; + + /** + * UTF-8 encoding strategy. + */ + var Utf8 = C_enc.Utf8 = { + /** + * Converts a word array to a UTF-8 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The UTF-8 string. + * + * @static + * + * @example + * + * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); + */ + stringify: function (wordArray) { + try { + return decodeURIComponent(escape(Latin1.stringify(wordArray))); + } catch (e) { + throw new Error('Malformed UTF-8 data'); + } + }, + + /** + * Converts a UTF-8 string to a word array. + * + * @param {string} utf8Str The UTF-8 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); + */ + parse: function (utf8Str) { + return Latin1.parse(unescape(encodeURIComponent(utf8Str))); + } + }; + + /** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + * + * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset: function () { + // Initial values + this._data = new WordArray.init(); + this._nDataBytes = 0; + }, + + /** + * Adds new data to this block algorithm's buffer. + * + * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append: function (data) { + // Convert string to WordArray, else assume WordArray already + if (typeof data == 'string') { + data = Utf8.parse(data); + } + + // Append + this._data.concat(data); + this._nDataBytes += data.sigBytes; + }, + + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. + * + * @return {WordArray} The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process: function (doFlush) { + // Shortcuts + var data = this._data; + var dataWords = data.words; + var dataSigBytes = data.sigBytes; + var blockSize = this.blockSize; + var blockSizeBytes = blockSize * 4; + + // Count blocks ready + var nBlocksReady = dataSigBytes / blockSizeBytes; + if (doFlush) { + // Round up to include partial blocks + nBlocksReady = Math.ceil(nBlocksReady); + } else { + // Round down to include only full blocks, + // less the number of blocks that must remain in the buffer + nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); + } + + // Count words ready + var nWordsReady = nBlocksReady * blockSize; + + // Count bytes ready + var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); + + // Process blocks + if (nWordsReady) { + for (var offset = 0; offset < nWordsReady; offset += blockSize) { + // Perform concrete-algorithm logic + this._doProcessBlock(dataWords, offset); + } + + // Remove processed words + var processedWords = dataWords.splice(0, nWordsReady); + data.sigBytes -= nBytesReady; + } + + // Return processed words + return new WordArray.init(processedWords, nBytesReady); + }, + + /** + * Creates a copy of this object. + * + * @return {Object} The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone: function () { + var clone = Base.clone.call(this); + clone._data = this._data.clone(); + + return clone; + }, + + _minBufferSize: 0 + }); + + /** + * Abstract hasher template. + * + * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ + /** + * Configuration options. + */ + cfg: Base.extend(), + + /** + * Initializes a newly created hasher. + * + * @param {Object} cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + init: function (cfg) { + // Apply config defaults + this.cfg = this.cfg.extend(cfg); + + // Set initial values + this.reset(); + }, + + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset: function () { + // Reset data buffer + BufferedBlockAlgorithm.reset.call(this); + + // Perform concrete-hasher logic + this._doReset(); + }, + + /** + * Updates this hasher with a message. + * + * @param {WordArray|string} messageUpdate The message to append. + * + * @return {Hasher} This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update: function (messageUpdate) { + // Append + this._append(messageUpdate); + + // Update the hash + this._process(); + + // Chainable + return this; + }, + + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param {WordArray|string} messageUpdate (Optional) A final message update. + * + * @return {WordArray} The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize: function (messageUpdate) { + // Final message update + if (messageUpdate) { + this._append(messageUpdate); + } + + // Perform concrete-hasher logic + var hash = this._doFinalize(); + + return hash; + }, + + blockSize: 512/32, + + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param {Hasher} hasher The hasher to create a helper for. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper: function (hasher) { + return function (message, cfg) { + return new hasher.init(cfg).finalize(message); + }; + }, + + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param {Hasher} hasher The hasher to use in this HMAC helper. + * + * @return {Function} The shortcut function. + * + * @static + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper: function (hasher) { + return function (message, key) { + return new C_algo.HMAC.init(hasher, key).finalize(message); + }; + } + }); + + /** + * Algorithm namespace. + */ + var C_algo = C.algo = {}; + + return C; + }(Math)); + + + return CryptoJS; + +})); \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/enc-base64.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/enc-base64.js new file mode 100644 index 0000000000..d5d40824d6 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/enc-base64.js @@ -0,0 +1,135 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function () { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var C_enc = C.enc; + + /** + * Base64 encoding strategy. + */ + var Base64 = C_enc.Base64 = { + /** + * Converts a word array to a Base64 string. + * + * @param {WordArray} wordArray The word array. + * + * @return {string} The Base64 string. + * + * @static + * + * @example + * + * var base64String = CryptoJS.enc.Base64.stringify(wordArray); + */ + stringify: function (wordArray) { + // Shortcuts + var words = wordArray.words; + var sigBytes = wordArray.sigBytes; + var map = this._map; + + // Clamp excess bits + wordArray.clamp(); + + // Convert + var base64Chars = []; + for (var i = 0; i < sigBytes; i += 3) { + var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; + var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; + + var triplet = (byte1 << 16) | (byte2 << 8) | byte3; + + for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { + base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); + } + } + + // Add padding + var paddingChar = map.charAt(64); + if (paddingChar) { + while (base64Chars.length % 4) { + base64Chars.push(paddingChar); + } + } + + return base64Chars.join(''); + }, + + /** + * Converts a Base64 string to a word array. + * + * @param {string} base64Str The Base64 string. + * + * @return {WordArray} The word array. + * + * @static + * + * @example + * + * var wordArray = CryptoJS.enc.Base64.parse(base64String); + */ + parse: function (base64Str) { + // Shortcuts + var base64StrLength = base64Str.length; + var map = this._map; + var reverseMap = this._reverseMap; + + if (!reverseMap) { + reverseMap = this._reverseMap = []; + for (var j = 0; j < map.length; j++) { + reverseMap[map.charCodeAt(j)] = j; + } + } + + // Ignore padding + var paddingChar = map.charAt(64); + if (paddingChar) { + var paddingIndex = base64Str.indexOf(paddingChar); + if (paddingIndex !== -1) { + base64StrLength = paddingIndex; + } + } + + // Convert + return parseLoop(base64Str, base64StrLength, reverseMap); + + }, + + _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + }; + + function parseLoop(base64Str, base64StrLength, reverseMap) { + var words = []; + var nBytes = 0; + for (var i = 0; i < base64StrLength; i++) { + if (i % 4) { + var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); + var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); + words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8); + nBytes++; + } + } + return WordArray.create(words, nBytes); + } + }()); + + + return CryptoJS.enc.Base64; + +})); \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/md5.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/md5.js new file mode 100644 index 0000000000..12b0fdd4b4 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto-js/md5.js @@ -0,0 +1,268 @@ +;(function (root, factory) { + if (typeof exports === "object") { + // CommonJS + module.exports = exports = factory(require("./core")); + } + else if (typeof define === "function" && define.amd) { + // AMD + define(["./core"], factory); + } + else { + // Global (browser) + factory(root.CryptoJS); + } +}(this, function (CryptoJS) { + + (function (Math) { + // Shortcuts + var C = CryptoJS; + var C_lib = C.lib; + var WordArray = C_lib.WordArray; + var Hasher = C_lib.Hasher; + var C_algo = C.algo; + + // Constants table + var T = []; + + // Compute constants + (function () { + for (var i = 0; i < 64; i++) { + T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0; + } + }()); + + /** + * MD5 hash algorithm. + */ + var MD5 = C_algo.MD5 = Hasher.extend({ + _doReset: function () { + this._hash = new WordArray.init([ + 0x67452301, 0xefcdab89, + 0x98badcfe, 0x10325476 + ]); + }, + + _doProcessBlock: function (M, offset) { + // Swap endian + for (var i = 0; i < 16; i++) { + // Shortcuts + var offset_i = offset + i; + var M_offset_i = M[offset_i]; + + M[offset_i] = ( + (((M_offset_i << 8) | (M_offset_i >>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Shortcuts + var H = this._hash.words; + + var M_offset_0 = M[offset + 0]; + var M_offset_1 = M[offset + 1]; + var M_offset_2 = M[offset + 2]; + var M_offset_3 = M[offset + 3]; + var M_offset_4 = M[offset + 4]; + var M_offset_5 = M[offset + 5]; + var M_offset_6 = M[offset + 6]; + var M_offset_7 = M[offset + 7]; + var M_offset_8 = M[offset + 8]; + var M_offset_9 = M[offset + 9]; + var M_offset_10 = M[offset + 10]; + var M_offset_11 = M[offset + 11]; + var M_offset_12 = M[offset + 12]; + var M_offset_13 = M[offset + 13]; + var M_offset_14 = M[offset + 14]; + var M_offset_15 = M[offset + 15]; + + // Working varialbes + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + + // Computation + a = FF(a, b, c, d, M_offset_0, 7, T[0]); + d = FF(d, a, b, c, M_offset_1, 12, T[1]); + c = FF(c, d, a, b, M_offset_2, 17, T[2]); + b = FF(b, c, d, a, M_offset_3, 22, T[3]); + a = FF(a, b, c, d, M_offset_4, 7, T[4]); + d = FF(d, a, b, c, M_offset_5, 12, T[5]); + c = FF(c, d, a, b, M_offset_6, 17, T[6]); + b = FF(b, c, d, a, M_offset_7, 22, T[7]); + a = FF(a, b, c, d, M_offset_8, 7, T[8]); + d = FF(d, a, b, c, M_offset_9, 12, T[9]); + c = FF(c, d, a, b, M_offset_10, 17, T[10]); + b = FF(b, c, d, a, M_offset_11, 22, T[11]); + a = FF(a, b, c, d, M_offset_12, 7, T[12]); + d = FF(d, a, b, c, M_offset_13, 12, T[13]); + c = FF(c, d, a, b, M_offset_14, 17, T[14]); + b = FF(b, c, d, a, M_offset_15, 22, T[15]); + + a = GG(a, b, c, d, M_offset_1, 5, T[16]); + d = GG(d, a, b, c, M_offset_6, 9, T[17]); + c = GG(c, d, a, b, M_offset_11, 14, T[18]); + b = GG(b, c, d, a, M_offset_0, 20, T[19]); + a = GG(a, b, c, d, M_offset_5, 5, T[20]); + d = GG(d, a, b, c, M_offset_10, 9, T[21]); + c = GG(c, d, a, b, M_offset_15, 14, T[22]); + b = GG(b, c, d, a, M_offset_4, 20, T[23]); + a = GG(a, b, c, d, M_offset_9, 5, T[24]); + d = GG(d, a, b, c, M_offset_14, 9, T[25]); + c = GG(c, d, a, b, M_offset_3, 14, T[26]); + b = GG(b, c, d, a, M_offset_8, 20, T[27]); + a = GG(a, b, c, d, M_offset_13, 5, T[28]); + d = GG(d, a, b, c, M_offset_2, 9, T[29]); + c = GG(c, d, a, b, M_offset_7, 14, T[30]); + b = GG(b, c, d, a, M_offset_12, 20, T[31]); + + a = HH(a, b, c, d, M_offset_5, 4, T[32]); + d = HH(d, a, b, c, M_offset_8, 11, T[33]); + c = HH(c, d, a, b, M_offset_11, 16, T[34]); + b = HH(b, c, d, a, M_offset_14, 23, T[35]); + a = HH(a, b, c, d, M_offset_1, 4, T[36]); + d = HH(d, a, b, c, M_offset_4, 11, T[37]); + c = HH(c, d, a, b, M_offset_7, 16, T[38]); + b = HH(b, c, d, a, M_offset_10, 23, T[39]); + a = HH(a, b, c, d, M_offset_13, 4, T[40]); + d = HH(d, a, b, c, M_offset_0, 11, T[41]); + c = HH(c, d, a, b, M_offset_3, 16, T[42]); + b = HH(b, c, d, a, M_offset_6, 23, T[43]); + a = HH(a, b, c, d, M_offset_9, 4, T[44]); + d = HH(d, a, b, c, M_offset_12, 11, T[45]); + c = HH(c, d, a, b, M_offset_15, 16, T[46]); + b = HH(b, c, d, a, M_offset_2, 23, T[47]); + + a = II(a, b, c, d, M_offset_0, 6, T[48]); + d = II(d, a, b, c, M_offset_7, 10, T[49]); + c = II(c, d, a, b, M_offset_14, 15, T[50]); + b = II(b, c, d, a, M_offset_5, 21, T[51]); + a = II(a, b, c, d, M_offset_12, 6, T[52]); + d = II(d, a, b, c, M_offset_3, 10, T[53]); + c = II(c, d, a, b, M_offset_10, 15, T[54]); + b = II(b, c, d, a, M_offset_1, 21, T[55]); + a = II(a, b, c, d, M_offset_8, 6, T[56]); + d = II(d, a, b, c, M_offset_15, 10, T[57]); + c = II(c, d, a, b, M_offset_6, 15, T[58]); + b = II(b, c, d, a, M_offset_13, 21, T[59]); + a = II(a, b, c, d, M_offset_4, 6, T[60]); + d = II(d, a, b, c, M_offset_11, 10, T[61]); + c = II(c, d, a, b, M_offset_2, 15, T[62]); + b = II(b, c, d, a, M_offset_9, 21, T[63]); + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + + var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); + var nBitsTotalL = nBitsTotal; + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( + (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | + (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) + ); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | + (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) + ); + + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 4; i++) { + // Shortcut + var H_i = H[i]; + + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + function FF(a, b, c, d, x, s, t) { + var n = a + ((b & c) | (~b & d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function GG(a, b, c, d, x, s, t) { + var n = a + ((b & d) | (c & ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function HH(a, b, c, d, x, s, t) { + var n = a + (b ^ c ^ d) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function II(a, b, c, d, x, s, t) { + var n = a + (c ^ (b | ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + C.MD5 = Hasher._createHelper(MD5); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + C.HmacMD5 = Hasher._createHmacHelper(MD5); + }(Math)); + + + return CryptoJS.MD5; + +})); \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto.js new file mode 100644 index 0000000000..afd52f604e --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/crypto.js @@ -0,0 +1,15 @@ +const uriKey = '3go8&$8*3*3h0k(2)2' + +export default { + uri: { + retrieve: id => { + id = id.toString().trim() + let string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ uriKey.charCodeAt(index % uriKey.length))).join('') + let result = CryptoJS.MD5(string).toString(CryptoJS.enc.Base64).replace(/\//g, '_').replace(/\+/g, '-') + return `http://p1.music.126.net/${result}/${id}` + } + }, + md5: { + digest: value => CryptoJS.MD5(value).toString() + } +} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/inject.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/inject.js new file mode 100644 index 0000000000..5d5e6cfbb5 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/inject.js @@ -0,0 +1,73 @@ +(() => { + const remote = 'oleomikdicccalekkpcbfgdmpjehnpkp' + const remoteMatch = id => new Promise(resolve => { + chrome.runtime.sendMessage(remote, {match: id}, response => { + resolve(response) + }) + }) + + const waitTimeout = wait => new Promise(resolve => { + setTimeout(() => { + resolve() + }, wait) + }) + + const searchFunction = (object, keyword) => + Object.keys(object) + .filter(key => object[key] && typeof object[key] == 'function') + .find(key => String(object[key]).match(keyword)) + + if(self.frameElement && self.frameElement.tagName == 'IFRAME'){ //in iframe + const keyOne = searchFunction(window.nej.e, '\\.dataset;if') + const keyTwo = searchFunction(window.nm.x, '\\.copyrightId==') + const keyThree = searchFunction(window.nm.x, '\\.privilege;if') + const functionOne = window.nej.e[keyOne] + + window.nej.e[keyOne] = (z, name) => { + if (name == 'copyright' || name == 'resCopyright') return 1 + return functionOne(z, name) + } + window.nm.x[keyTwo] = () => false + window.nm.x[keyThree] = song => { + song.status = 0 + if (song.privilege) song.privilege.pl = 320000 + return 0 + } + const table = document.querySelector('table tbody') + if(table) Array.from(table.childNodes) + .filter(element => element.classList.contains('js-dis')) + .forEach(element => element.classList.remove('js-dis')) + } + else{ + const keyAjax = searchFunction(window.nej.j, '\\.replace\\("api","weapi') + const functionAjax = window.nej.j[keyAjax] + window.nej.j[keyAjax] = (url, param) => { + const onload = param.onload + param.onload = data => { + Promise.resolve() + .then(() => { + if(url.includes('enhance/player/url')){ + if(data.data[0].url){ + data.data[0].url = data.data[0].url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net') + } + else{ + return Promise.race([remoteMatch(data.data[0].id), waitTimeout(4000)]) + .then(result => { + if(result){ + data.data[0].code = 200 + data.data[0].br = 320000 + data.data[0].type = 'mp3' + data.data[0].size = result.size + data.data[0].md5 = result.md5 + data.data[0].url = result.url.replace(/http:\/\//, 'https://') + } + }) + } + } + }) + .then(() => onload(data)) + } + functionAjax(url, param) + } + } +})() \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/manifest.json b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/manifest.json new file mode 100644 index 0000000000..f820a42c68 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "UnblockNeteaseMusic", + "description": "For test (es6 only)", + "version": "0.1", + "background": { + "page": "background.html" + }, + "content_scripts": [{ + "js": ["script.js"], + "matches": ["*://music.163.com/*"], + "all_frames": true + }], + "web_accessible_resources": ["inject.js"], + "externally_connectable": { + "matches": ["*://music.163.com/*"] + }, + "manifest_version": 2, + "permissions": ["*://*/*", "webRequest", "webRequestBlocking"] +} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/request.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/request.js new file mode 100644 index 0000000000..46da340083 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/request.js @@ -0,0 +1,18 @@ +export default (method, url, headers, body) => new Promise((resolve, reject) => { + headers = headers || {} + const xhr = new XMLHttpRequest() + xhr.onreadystatechange = () => {if(xhr.readyState == 4) resolve(xhr)} + xhr.onerror = error => reject(error) + xhr.open(method, url, true) + Object.keys(headers).filter(key => !['origin', 'referer'].includes(key)).forEach(key => xhr.setRequestHeader(key, headers[key])) + xhr.send(body) +}).then(xhr => Object.assign(xhr, { + statusCode: xhr.status, + headers: + xhr.getAllResponseHeaders().split('\r\n').filter(line => line).map(line => line.split(/\s*:\s*/)) + .reduce((result, pair) => Object.assign(result, {[pair[0].toLowerCase()]: pair[1]}), {}), + url: {href: xhr.responseURL}, + body: () => xhr.responseText, + json: () => JSON.parse(xhr.responseText), + jsonp: () => JSON.parse(xhr.responseText.slice(xhr.responseText.indexOf('(') + 1, -')'.length)) +})) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/script.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/script.js new file mode 100644 index 0000000000..01eb7d84f5 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/browser/script.js @@ -0,0 +1,5 @@ +(() => { + let script = (document.head || document.documentElement).appendChild(document.createElement('script')) + script.src = chrome.extension.getURL('inject.js') + script.onload = script.parentNode.removeChild(script) +})() \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/ca.crt b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/ca.crt new file mode 100644 index 0000000000..4101eb3451 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIJAKX8LdIETDklMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFzZU11c2ljIFJvb3QgQ0ExHTAb +BgNVBAoMFEdpdEh1Yi5jb20gQG5vbmRhbmVlMB4XDTE5MDUxODE2MDU0NVoXDTI0 +MDUxNjE2MDU0NVowUjELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRl +YXNlTXVzaWMgUm9vdCBDQTEdMBsGA1UECgwUR2l0SHViLmNvbSBAbm9uZGFuZWUw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD23K6Ti2TfLJToCmpCAVgE +Xb8+qTMfrifCpnKlJ+hrL+4KI1j4vSqTOOatqmxGSXZdF/j2kJuI40YThaokcgYx +GFcPcEftSCYGWy8o20u2hzTkkW3KW9wlsDRIXICFXVIsHeSDwz+aVSudkyJHjfaS +aLNb5pPovE7MRj8tDbp55scaSqhEcOe3m1ZlwlCeeXvD7RLKr3xhBKbGEqlJAjFq +RNGzuqylqyJVBLScNHC7Lcf4n6pKr1yPGOeLePOUrIwtj0ynHUcBfeMuCVCsIKL8 +vy/oNwlDrZaAMfu5QQslzEf87KY1QgtI6Ppii+tzbmVx1ZxnlaCKqiuwlgBoi/5r +AgMBAAGjUDBOMB0GA1UdDgQWBBRDhbGjnXEUouE9wNFS2k9PtgYYjDAfBgNVHSME +GDAWgBRDhbGjnXEUouE9wNFS2k9PtgYYjDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQDRUh5+JFLEALXQkhPfwrVf4sCXTwLMwVujTPo3NMbhpWiP4cnn +XHGCD5V57bBwjeD6NSrczDIdnN9uTJyFmLNVFMZBguEIeZfLUJLJ6w1ZhfgciX1D +9djyyo6eclkHvi+aPZKfzgMmc5BvHcjyUyS5MzI23kUW6WXUDn3IDIUKrfaH9Mjc +/d4DDZVKQCYrLoBL+XO7pEHUY0u9XZVYWEavQ5tSN8XY1SDrO0yGUpRWET0ltubE +zV7W0LOhuoVCiemboc5H8+njBjCis8obAo1XMmDZzW189L9GPFxHNWlka+KlajZB +tMo90PooZYEOw1rTUrzHb+VZY/tYIAAomGZ0 +-----END CERTIFICATE----- diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/cache.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/cache.js new file mode 100644 index 0000000000..56b52a197b --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/cache.js @@ -0,0 +1,10 @@ +module.exports = (job, parameter, live = 30 * 60 * 1000) => { + const cache = job.cache ? job.cache : job.cache = {} + const key = parameter == null ? 'default' : (parameter.id || parameter.key || parameter) + if(!(key in cache) || cache[key].expiration < Date.now()) + cache[key] = { + execution: job(parameter), + expiration: Date.now() + live + } + return cache[key].execution +} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/cli.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/cli.js new file mode 100644 index 0000000000..b4cea23c9a --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/cli.js @@ -0,0 +1,156 @@ +const cli = { + _program: {}, + _options: [], + program: (information = {}) => { + cli._program = information + return cli + }, + option: (flags, addition = {}) => { + // name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo. + // dest - The name of the attribute to be added to the object returned by parse_options(). + + // nargs - The number of command-line arguments that should be consumed. // N, ?, *, +, REMAINDER + // action - The basic type of action to be taken when this argument is encountered at the command line. // store, store_true, store_false, append, append_const, count, help, version + + // const - A constant value required by some action and nargs selections. (supporting store_const and append_const action) + + // metavar - A name for the argument in usage messages. + // help - A brief description of what the argument does. + + // required - Whether or not the command-line option may be omitted (optionals only). + // default - The value produced if the argument is absent from the command line. + // type - The type to which the command-line argument should be converted. + // choices - A container of the allowable values for the argument. + + flags = Array.isArray(flags) ? flags : [flags] + addition.dest = addition.dest || flags.slice(-1)[0].toLowerCase().replace(/^-+/, '').replace(/-[a-z]/g, character => character.slice(1).toUpperCase()) + addition.help = addition.help || {'help': 'output usage information', 'version': 'output the version number'}[addition.action] + cli._options.push(Object.assign(addition, {flags: flags, positional: !flags[0].startsWith('-')})) + return cli + }, + parse: argv => { + let positionals = cli._options.map((option, index) => option.positional ? index : null).filter(index => index !== null), optionals = {} + cli._options.forEach((option, index) => option.positional ? null : option.flags.forEach(flag => optionals[flag] = index)) + + cli._program.name = cli._program.name || require('path').parse(argv[1]).base + let args = argv.slice(2).reduce((result, part) => /^-[^-]/.test(part) ? result.concat(part.slice(1).split('').map(string => '-' + string)) : result.concat(part), []) + + let pointer = 0 + while(pointer < args.length){ + let part = args[pointer], value = null + let index = part.startsWith('-') ? optionals[part] : positionals.shift() + if(index == undefined) part.startsWith('-') ? error(`no such option: ${part}`) : error(`extra arguments found: ${part}`) + if(part.startsWith('-')) pointer += 1 + let action = cli._options[index].action + + if(['help', 'version'].includes(action)){ + if(action === 'help') help() + else if(action === 'version') version() + } + else if(['store_true', 'store_false'].includes(action)){ + value = action === 'store_true' + } + else{ + let gap = args.slice(pointer).findIndex(part => part in optionals) + let next = gap === -1 ? args.length : pointer + gap + value = args.slice(pointer, next) + if(value.length === 0){ + if(cli._options[index].positional) + error(`the following arguments are required: ${part}`) + else if(cli._options[index].nargs === '+') + error(`argument ${part}: expected at least one argument`) + else + error(`argument ${part}: expected one argument`) + } + if(cli._options[index].nargs != '+'){ + value = value[0] + pointer += 1 + } + else{ + pointer = next + } + } + cli[cli._options[index].dest] = value + } + if(positionals.length) error(`the following arguments are required: ${positionals.map(index => cli._options[index].flags[0]).join(', ')}`) + // cli._options.forEach(option => console.log(option.dest, cli[option.dest])) + return cli + } +} + +const pad = length => (new Array(length + 1)).join(' ') + +const usage = () => { + let options = cli._options.map(option => { + let flag = option.flags[0] + let name = option.metavar || option.dest + if(option.positional){ + if(option.nargs === '+') + return `${name} [${name} ...]` + else + return `${name}` + } + else{ + if(['store_true', 'store_false', 'help', 'version'].includes(option.action)) + return `[${flag}]` + else if(option.nargs === '+') + return `[${flag} ${name} [${name} ...]]` + else + return `[${flag} ${name}]` + } + }) + let maximum = 80 + let title = `usage: ${cli._program.name}` + let lines = [title] + + options.map(name => ' ' + name).forEach(option => { + lines[lines.length - 1].length + option.length < maximum ? + lines[lines.length - 1] += option : + lines.push(pad(title.length) + option) + }) + console.log(lines.join('\n')) +} + +const help = () => { + usage() + let positionals = cli._options.filter(option => option.positional) + .map(option => [option.metavar || option.dest, option.help]) + let optionals = cli._options.filter(option => !option.positional) + .map(option => { + let flags = option.flags + let name = option.metavar || option.dest + let use = '' + if(['store_true', 'store_false', 'help', 'version'].includes(option.action)) + use = flags.map(flag => `${flag}`).join(', ') + else if(option.nargs === '+') + use = flags.map(flag => `${flag} ${name} [${name} ...]`).join(', ') + else + use = flags.map(flag => `${flag} ${name}`).join(', ') + return [use, option.help] + }) + let align = Math.max.apply(null, positionals.concat(optionals).map(option => option[0].length)) + align = align > 26 ? 26 : align + const publish = option => { + option[0].length > align ? + console.log(` ${option[0]}\n${pad(align + 4)}${option[1]}`) : + console.log(` ${option[0]}${pad(align - option[0].length)} ${option[1]}`) + } + if(positionals.length) console.log('\npositional arguments:') + positionals.forEach(publish) + if(optionals.length) console.log('\noptional arguments:') + optionals.forEach(publish) + process.exit() +} + +const version = () => { + console.log(cli._program.version) + process.exit() +} + +const error = message => { + usage() + console.log(cli._program.name + ':', 'error:', message) + process.exit(1) +} + +module.exports = cli \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/crypto.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/crypto.js new file mode 100644 index 0000000000..301fdf06ba --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/crypto.js @@ -0,0 +1,68 @@ +'use strict' + +const crypto = require('crypto') +const parse = require('url').parse +const uriKey = '3go8&$8*3*3h0k(2)2' +const eapiKey = 'e82ckenh8dichen8' +const linuxapiKey = 'rFgB&h#%2?^eDg:Q' + +const decrypt = (buffer, key) => { + let decipher = crypto.createDecipheriv('aes-128-ecb', key, '') + return Buffer.concat([decipher.update(buffer), decipher.final()]) +} + +const encrypt = (buffer, key) => { + let cipher = crypto.createCipheriv('aes-128-ecb', key, '') + return Buffer.concat([cipher.update(buffer), cipher.final()]) +} + +module.exports = { + eapi: { + encrypt: buffer => encrypt(buffer, eapiKey), + decrypt: buffer => decrypt(buffer, eapiKey), + encryptRequest: (url, object) => { + url = parse(url) + let text = JSON.stringify(object) + let message = `nobody${url.path}use${text}md5forencrypt` + let digest = crypto.createHash('md5').update(message).digest('hex') + let data = `${url.path}-36cd479b6b5-${text}-36cd479b6b5-${digest}` + return { + url: url.href.replace(/\w*api/, 'eapi'), + body: 'params=' + encrypt(Buffer.from(data), eapiKey).toString('hex').toUpperCase() + } + } + }, + linuxapi: { + encrypt: buffer => encrypt(buffer, linuxapiKey), + decrypt: buffer => decrypt(buffer, linuxapiKey), + encryptRequest: (url, object) => { + url = parse(url) + let text = JSON.stringify({method: 'POST', url: url.href, params: object}) + return { + url: url.resolve('/api/linux/forward'), + body: 'eparams=' + encrypt(Buffer.from(text), linuxapiKey).toString('hex').toUpperCase() + } + } + }, + base64: { + encode: text => Buffer.from(text).toString('base64').replace(/\+/g, '-').replace(/\//g, '_'), + decode: text => Buffer.from(text.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('ascii') + }, + uri: { + retrieve: id => { + id = id.toString().trim() + let string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ uriKey.charCodeAt(index % uriKey.length))).join('') + let result = crypto.createHash('md5').update(string).digest('base64').replace(/\//g, '_').replace(/\+/g, '-') + return `http://p1.music.126.net/${result}/${id}` + } + }, + md5: { + digest: value => crypto.createHash('md5').update(value).digest('hex'), + pipe: source => new Promise((resolve, reject) => { + let digest = crypto.createHash('md5').setEncoding('hex') + source.pipe(digest) + .on('error', error => reject(error)) + .once('finish', () => resolve(digest.read())) + }) + } +} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/docker-compose.yml b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/docker-compose.yml new file mode 100644 index 0000000000..ec6a911959 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3' + +services: + unblockneteasemusic: + image: nondanee/unblockneteasemusic + environment: + NODE_ENV: production + ports: + - 8080:8080 diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/hook.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/hook.js new file mode 100644 index 0000000000..e777eaa379 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/hook.js @@ -0,0 +1,310 @@ +const cache = require('./cache') +const parse = require('url').parse +const crypto = require('./crypto') +const request = require('./request') +const match = require('./provider/match') + +const hook = { + request: { + before: () => {}, + after: () => {}, + }, + connect: { + before: () => {} + }, + target: { + host: [], + path: [] + } +} + +hook.target.host = [ + 'music.163.com', + 'interface.music.163.com', + 'interface3.music.163.com', + 'apm.music.163.com', + 'apm3.music.163.com', + 'mam.netease.com', + 'api.iplay.163.com' + // 'crash.163.com', + // 'clientlog.music.163.com' +] + +hook.target.path = [ + '/api/v3/playlist/detail', + '/api/v3/song/detail', + '/api/v6/playlist/detail', + '/api/album/play', + '/api/artist/privilege', + '/api/album/privilege', + '/api/v1/artist', + '/api/v1/artist/songs', + '/api/artist/top/song', + '/api/v1/album', + '/api/playlist/privilege', + '/api/song/enhance/player/url', + '/api/song/enhance/player/url/v1', + '/api/song/enhance/download/url', + '/batch', + '/api/batch', + '/api/v1/search/get', + '/api/cloudsearch/pc', + '/api/v1/playlist/manipulate/tracks', + '/api/song/like', + '/api/v1/play/record', + '/api/playlist/v4/detail' +] + +hook.request.before = ctx => { + const req = ctx.req + req.url = (req.url.startsWith('http://') ? '' : (req.socket.encrypted ? 'https:' : 'http:') + '//music.163.com') + req.url + const url = parse(req.url) + if((hook.target.host.includes(url.hostname)) && req.method == 'POST' && (url.path == '/api/linux/forward' || url.path.startsWith('/eapi/'))){ + return request.read(req) + .then(body => { + req.body = body + req.headers['X-Real-IP'] = '118.88.88.88' + if(body){ + let data = null + let netease = {} + netease.pad = (body.match(/%0+$/) || [''])[0] + netease.forward = (url.path == '/api/linux/forward') + if(netease.forward){ + data = JSON.parse(crypto.linuxapi.decrypt(Buffer.from(body.slice(8, body.length - netease.pad.length), 'hex')).toString()) + netease.path = parse(data.url).path + netease.param = data.params + } + else{ + data = crypto.eapi.decrypt(Buffer.from(body.slice(7, body.length - netease.pad.length), 'hex')).toString().split('-36cd479b6b5-') + netease.path = data[0] + netease.param = JSON.parse(data[1]) + } + netease.path = netease.path.replace(/\/\d*$/, '') + ctx.netease = netease + // console.log(netease.path, netease.param) + + if(netease.path == '/api/song/enhance/download/url') + return pretendPlay(ctx) + } + }) + .catch(error => { + console.log(error) + }) + } + if((hook.target.host.includes(url.hostname)) && url.path.startsWith('/weapi/')){ + ctx.req.headers['X-Real-IP'] = '118.88.88.88' + ctx.netease = {web: true, path: url.path.replace(/^\/weapi\//, '/api/').replace(/\?.+$/, '').replace(/\/\d*$/, '')} + } + else if(req.url.includes('package')){ + try{ + let data = req.url.split('package/').pop().split('/') + let url = parse(crypto.base64.decode(data[0])) + let id = data[1].replace('.mp3', '') + req.url = url.href + req.headers['host'] = url.hostname + ctx.package = {id} + ctx.decision = 'proxy' + } + catch(error){ + ctx.error = error + ctx.decision = 'close' + } + } +} + +hook.request.after = ctx => { + const netease = ctx.netease + const package = ctx.package + const proxyRes = ctx.proxyRes + if(netease && hook.target.path.includes(netease.path) && proxyRes.statusCode == 200){ + return request.read(proxyRes, true) + .then(buffer => { + proxyRes.body = buffer + try{ + netease.encrypted = false + netease.jsonBody = JSON.parse(buffer.toString()) + } + catch(error){ + netease.encrypted = true + netease.jsonBody = JSON.parse(crypto.eapi.decrypt(buffer).toString()) + } + + if(netease.path.includes('manipulate') && [401, 512].includes(netease.jsonBody.code) && !netease.web) + return tryCollect(ctx) + else if(netease.path == '/api/song/like' && [401, 512].includes(netease.jsonBody.code) && !netease.web) + return tryLike(ctx) + else if(netease.path.includes('url')) + return tryMatch(ctx) + }) + .then(() => { + if('transfer-encoding' in proxyRes.headers) delete proxyRes.headers['transfer-encoding'] + if('content-encoding' in proxyRes.headers) delete proxyRes.headers['content-encoding'] + if('content-length' in proxyRes.headers) delete proxyRes.headers['content-length'] + + const inject = (key, value) => { + if(typeof(value) === 'object' && value != null){ + if('pic_str' in value && 'pic' in value) // for js precision + value['pic'] = value['pic_str'] + if('coverImgId_str' in value && 'coverImgId' in value) // for js precision + value['coverImgId'] = value['coverImgId_str'] + if('fee' in value) value['fee'] = 0 + if('st' in value && 'pl' in value && 'dl' in value && 'subp' in value){ // batch modify + value['st'] = 0 + value['subp'] = 1 + value['pl'] = (value['pl'] == 0) ? 320000 : value['pl'] + value['dl'] = (value['dl'] == 0) ? 320000 : value['dl'] + } + } + return value + } + + let body = JSON.stringify(netease.jsonBody, inject) + body = body.replace(/"pic":"(\d+)"/g, '"pic":$1') + body = body.replace(/"coverImgId":"(\d+)"/g, '"coverImgId":$1') + proxyRes.body = (netease.encrypted ? crypto.eapi.encrypt(Buffer.from(body)) : body) + }) + } + else if(package){ + if(/p\d+c*.music.126.net/.test(ctx.req.url)){ + proxyRes.headers['content-type'] = 'audio/mpeg' + } + } +} + +hook.connect.before = ctx => { + let url = parse('https://' + ctx.req.url) + if(hook.target.host.includes(url.hostname)){ + if(url.port == 80){ + ctx.req.url = `localhost:${global.port[0]}` + ctx.req.local = true + } + else if(global.port[1]){ + ctx.req.url = `localhost:${global.port[1]}` + ctx.req.local = true + } + else{ + ctx.decision = 'blank' + } + } +} + +const pretendPlay = ctx => { + const req = ctx.req + const netease = ctx.netease + let turn = 'http://music.163.com/api/song/enhance/player/url' + let query = null + if(netease.linux){ + netease.param = { + ids: `["${netease.param.id}"]`, + br: netease.param.br + } + query = crypto.linuxapi.encryptRequest(turn, netease.param) + } + else{ + netease.param = { + ids: `["${netease.param.id}"]`, + br: netease.param.br, + e_r: netease.param.e_r, + header: netease.param.header + } + query = crypto.eapi.encryptRequest(turn, netease.param) + } + req.url = query.url + req.body = query.body + netease.pad +} + +const tryCollect = ctx => { + const req = ctx.req + const netease = ctx.netease + let trackId = (netease.param.trackIds instanceof Array ? netease.param.trackIds : JSON.parse(netease.param.trackIds))[0] + return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${netease.param.pid}&op=${netease.param.op}`).then(response => response.json()) + .then(jsonBody => { + netease.jsonBody = jsonBody + }) + .catch(() => {}) +} + +const tryLike = ctx => { + const req = ctx.req + const netease = ctx.netease + let pid, userId, trackId = netease.param.trackId + return request('GET', 'http://music.163.com/api/v1/user/info', req.headers).then(response => response.json()) + .then(jsonBody => { + userId = jsonBody.userPoint.userId + return request('GET', `http://music.163.com/api/user/playlist?uid=${userId}&limit=1`, req.headers).then(response => response.json()) + }) + .then(jsonBody => { + pid = jsonBody.playlist[0].id + return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=add`).then(response => response.json()) + }) + .then(jsonBody => { + if(jsonBody.code == 200 || jsonBody.code == 502){ + netease.jsonBody = {code: 200, playlistId: pid} + } + }) + .catch(() => {}) +} + +const computeHash = task => request('GET', task.url).then(response => crypto.md5.pipe(response)) + +const tryMatch = ctx => { + const netease = ctx.netease + const jsonBody = netease.jsonBody + let tasks = [], target = 0 + + const inject = item => { + item.flag = 0 + if((item.code != 200 || item.freeTrialInfo) && (target == 0 || item.id == target)){ + return match(item.id) + .then(song => { + item.url = `${global.endpoint || 'http://music.163.com'}/package/${crypto.base64.encode(song.url)}/${item.id}.mp3` + item.md5 = song.md5 || crypto.md5.digest(song.url) + item.size = song.size + item.code = 200 + item.br = 320000 + item.type = 'mp3' + return song + }) + .then(song => { + if(!netease.path.includes('download') || song.md5) return + const newer = (base, target) => { + let difference = + Array.from([base, target]) + .map(version => version.split('.').slice(0, 3).map(number => parseInt(number) || 0)) + .reduce((aggregation, current) => !aggregation.length ? current.map(element => [element]) : aggregation.map((element, index) => element.concat(current[index])), []) + .filter(pair => pair[0] != pair[1])[0] + return !difference || difference[0] <= difference[1] + } + const limit = {android: '0.0.0', osx: '2.0.0'} + const task = {key: song.url.replace(/\?.*$/, ''), url: song.url} + try{ + let header = netease.param.header + header = typeof header === 'string' ? JSON.parse(header) : header + let {os, appver} = header + if(os in limit && newer(limit[os], appver)) + return cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then(value => item.md5 = value) + } + catch(e){} + }) + .catch(() => {}) + } + else if(item.code == 200 && netease.web){ + item.url = item.url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net') + } + } + + if(!jsonBody.data instanceof Array){ + tasks = [inject(jsonBody.data)] + } + else if(netease.path.includes('download')){ + jsonBody.data = jsonBody.data[0] + tasks = [inject(jsonBody.data)] + } + else{ + target = netease.web ? 0 : parseInt((netease.param.ids instanceof Array ? netease.param.ids : JSON.parse(netease.param.ids))[0].toString().replace('_0', '')) // reduce time cost + tasks = jsonBody.data.map(item => inject(item)) + } + return Promise.all(tasks).catch(() => {}) +} + +module.exports = hook \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/package-lock.json b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/package-lock.json new file mode 100644 index 0000000000..3ac6c1216c --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "unblockneteasemusic", + "version": "0.15.1", + "lockfileVersion": 1 +} diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/package.json b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/package.json new file mode 100644 index 0000000000..837d680dee --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/package.json @@ -0,0 +1,25 @@ +{ + "name": "unblockneteasemusic", + "version": "0.15.1", + "description": "Revive unavailable songs for Netease Cloud Music", + "main": "provider/match.js", + "bin": { + "UnblockNeteaseMusic": "app.js" + }, + "scripts": { + "pkg": "pkg . --out-path=dist/" + }, + "pkg": { + "assets": [ + "server.key", + "server.crt" + ] + }, + "repository": { + "type": "git", + "url": "https://github.com/nondanee/UnblockNeteaseMusic.git" + }, + "author": "nondanee", + "license": "MIT", + "dependencies": {} +} diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/baidu.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/baidu.js new file mode 100644 index 0000000000..7da90e0bea --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/baidu.js @@ -0,0 +1,41 @@ +const cache = require('../cache') +const insure = require('./insure') +const request = require('../request') + +const search = info => { + let url = + 'http://sug.qianqian.com/info/suggestion?' + + 'word=' + encodeURIComponent(info.keyword) + '&version=2&from=0' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + if('data' in jsonBody){ + let matched = jsonBody.data.song[0] + return matched.songid + } + else{ + return Promise.reject() + } + }) +} + +const track = id => { + let url = + 'http://music.taihe.com/data/music/fmlink?' + + 'songIds=' + id + '&type=mp3' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + if('songList' in jsonBody.data) + return jsonBody.data.songList[0].songLink + else + return Promise.reject() + }) + .catch(() => insure().baidu.track(id)) +} + +const check = info => cache(search, info).then(track) + +module.exports = {check} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/find.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/find.js new file mode 100644 index 0000000000..8f98754054 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/find.js @@ -0,0 +1,22 @@ +const cache = require('../cache') +const request = require('../request') + +const filter = (object, keys) => Object.keys(object).filter(key => keys.includes(key)).reduce((result, key) => Object.assign(result, {[key]: object[key]}), {}) +// Object.keys(object).filter(key => !keys.includes(key)).forEach(key => delete object[key]) + +const find = id => { + let url = + 'https://music.163.com/api/song/detail?ids=[' + id + ']' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + let info = filter(jsonBody.songs[0], ['id', 'name', 'alias', 'duration']) + info.album = filter(jsonBody.songs[0].album, ['id', 'name']) + info.artists = jsonBody.songs[0].artists.map(artist => filter(artist, ['id', 'name'])) + info.keyword = info.name + ' - ' + info.artists[0].name + return info + }) +} + +module.exports = id => cache(find, id) \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/insure.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/insure.js new file mode 100644 index 0000000000..e65e65e07b --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/insure.js @@ -0,0 +1,19 @@ +const request = require('../request') +const host = 'https://public.nondanee.tk' + +module.exports = () => { + const proxy = new Proxy(() => {}, { + get: (target, property) => { + target.route = (target.route || []).concat(property) + return proxy + }, + apply: (target, _, payload) => { + let path = target.route.join('/'), query = payload[0] + query = encodeURIComponent(typeof(query) === 'object' ? JSON.stringify(query) : query) + if(path != 'qq/ticket') return Promise.reject() + return request('GET', `${host}/${path}?${query}`) + .then(response => response.body()) + } + }) + return proxy +} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/joox.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/joox.js new file mode 100644 index 0000000000..260a02c9cb --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/joox.js @@ -0,0 +1,56 @@ +const cache = require('../cache') +const insure = require('./insure') +const request = require('../request') + +let headers = { + 'origin': 'http://www.joox.com', + 'referer': 'http://www.joox.com' +} + +const fit = info => { + if(/[\u0800-\u4e00]/.test(info.name)) //is japanese + return info.name + else + return info.keyword +} + +const search = info => { + let keyword = fit(info) + let url = + 'http://api-jooxtt.sanook.com/web-fcgi-bin/web_search?' + + 'country=hk&lang=zh_TW&' + + 'search_input=' + encodeURIComponent(keyword) + '&sin=0&ein=30' + + return request('GET', url, headers) + .then(response => response.body()) + .then(body => { + let jsonBody = JSON.parse(body.replace(/(\')/g, '"')) + let matched = jsonBody.itemlist[0] + if(matched) + return matched.songid + else + return Promise.reject() + }) +} + +const track = id => { + let url = + 'http://api.joox.com/web-fcgi-bin/web_get_songinfo?' + + 'songid=' + id + '&country=hk&lang=zh_cn&from_type=-1&' + + 'channel_id=-1&_=' + (new Date).getTime() + + return request('GET', url, headers) + .then(response => response.jsonp()) + .then(jsonBody => { + let songUrl = (jsonBody.r320Url || jsonBody.r192Url || jsonBody.mp3Url || jsonBody.m4aUrl).replace(/M\d00([\w]+).mp3/, 'M800$1.mp3') + if(songUrl) + return songUrl + else + return Promise.reject() + }) + .catch(() => insure().joox.track(id)) +} + +const check = info => cache(search, info).then(track) + +module.exports = {check, track} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/kugou.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/kugou.js new file mode 100644 index 0000000000..46c95557b5 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/kugou.js @@ -0,0 +1,38 @@ +const cache = require('../cache') +const insure = require('./insure') +const request = require('../request') + +const search = info => { + let url = + 'http://songsearch.kugou.com/song_search_v2?' + + 'keyword=' + encodeURIComponent(info.keyword) + '&page=1' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + let matched = jsonBody.data.lists[0] + if(matched) + return matched.FileHash + else + return Promise.reject() + }) + .catch(() => insure().kugou.search(info)) +} + +const track = id => { + let url = + 'http://www.kugou.com/yy/index.php?r=play/getdata&hash=' + id + + return request('GET', url, {cookie: `kg_mid=${id.toLowerCase()}`}) + .then(response => response.json()) + .then(jsonBody => { + if(jsonBody.status == '1') + return jsonBody.data.play_url + else + return Promise.reject() + }) +} + +const check = info => cache(search, info).then(track) + +module.exports = {check, search} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/kuwo.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/kuwo.js new file mode 100644 index 0000000000..e3f2c198ea --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/kuwo.js @@ -0,0 +1,48 @@ +const cache = require('../cache') +const insure = require('./insure') +const request = require('../request') + +const search = info => { + let url = + // 'http://search.kuwo.cn/r.s?' + + // 'ft=music&itemset=web_2013&client=kt&' + + // 'rformat=json&encoding=utf8&' + + // 'all=' + encodeURIComponent(info.keyword) + '&pn=0&rn=20' + 'http://search.kuwo.cn/r.s?' + + 'ft=music&rformat=json&encoding=utf8&' + + 'rn=8&callback=song&vipver=MUSIC_8.0.3.1&' + + 'SONGNAME=' + encodeURIComponent(info.name) + '&' + + 'ARTIST=' + encodeURIComponent(info.artists[0].name) + + return request('GET', url) + .then(response => response.body()) + .then(body => { + let jsonBody = JSON.parse(body.replace(/\'/g, '"').replace('try {var jsondata =', '').replace(';song(jsondata);}catch(e){jsonError(e)}', '')) + let matched = jsonBody.abslist[0] + if(matched) + return matched.MUSICRID.split('_').pop() + else + return Promise.reject() + }) +} + +const track = id => { + let url = + 'http://antiserver.kuwo.cn/anti.s?' + + 'type=convert_url&format=mp3&response=url&rid=MUSIC_' + id + // 'type=convert_url&format=aac|mp3|wma&response=url&rid=MUSIC_' + id + + return request('GET', url) + .then(response => response.body()) + .then(body => { + if(body.startsWith('http')) + return body + else + return Promise.reject() + }) + .catch(() => insure().kuwo.track(id)) +} + +const check = info => cache(search, info).then(track) + +module.exports = {check, track} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/match.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/match.js new file mode 100644 index 0000000000..ee7eabdf1f --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/match.js @@ -0,0 +1,52 @@ +const find = require('./find') +const crypto = require('../crypto') +const request = require('../request') + +const provider = { + netease: require('./netease'), + qq: require('./qq'), + xiami: require('./xiami'), + baidu: require('./baidu'), + kugou: require('./kugou'), + kuwo: require('./kuwo'), + migu: require('./migu'), + joox: require('./joox') +} + +const match = (id, source) => { + let meta = {} + let candidate = (source || global.source || ['netease', 'qq', 'xiami', 'baidu']).filter(name => name in provider) + return find(id) + .then(info => { + meta = info + return Promise.all(candidate.map(name => provider[name].check(info).catch(() => {}))) + }) + .then(urls => { + urls = urls.filter(url => url) + return Promise.all(urls.map(url => check(url))) + }) + .then(songs => { + songs = songs.filter(song => song.url) + if(!songs.length) return Promise.reject() + console.log(`[${meta.id}] ${meta.name}\n${songs[0].url}`) + return songs[0] + }) +} + +const check = url => { + let song = {size: 0, url: null, md5: null} + return request('HEAD', url) + .then(response => { + if(response.statusCode != 200) return + if(url.includes('qq.com')) + song.md5 = response.headers['server-md5'] + else if(url.includes('xiami.net') || url.includes('qianqian.com')) + song.md5 = response.headers['etag'].replace(/"/g, '').toLowerCase() + song.size = parseInt(response.headers['content-length']) || 0 + song.url = response.url.href + }) + .catch(() => {}) + .then(() => song) +} + +module.exports = match \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/migu.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/migu.js new file mode 100644 index 0000000000..f287ab4934 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/migu.js @@ -0,0 +1,24 @@ +const cache = require('../cache') +const request = require('../request') + +const search = info => { + let url = + 'http://m.10086.cn/migu/remoting/scr_search_tag?' + + 'keyword=' + encodeURIComponent(info.keyword) + '&type=2&rows=20&pgc=1' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + if('musics' in jsonBody){ + let matched = jsonBody.musics[0] + return matched.mp3 + } + else{ + return Promise.reject() + } + }) +} + +const check = info => cache(search, info) + +module.exports = {check} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/netease.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/netease.js new file mode 100644 index 0000000000..f455134d27 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/netease.js @@ -0,0 +1,29 @@ +const cache = require('../cache') +const crypto = require('../crypto') +const request = require('../request') + +const search = info => { + let url = + 'http://music.163.com/api/album/' + info.album.id + + return request('GET', url) + .then(response => response.body()) + .then(body => { + let jsonBody = JSON.parse(body.replace(/"dfsId":(\d+)/g, '"dfsId":"$1"')) // for js precision + let matched = jsonBody.album.songs.find(song => song.id === info.id) + if(matched) + return matched.hMusic.dfsId || matched.mMusic.dfsId || matched.lMusic.dfsId + else + return Promise.reject() + }) +} + +const track = id => { + if(!id || id === '0') return Promise.reject() + let songUrl = crypto.uri.retrieve(id) + return songUrl +} + +const check = info => cache(search, info).then(track) + +module.exports = {check} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/qq.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/qq.js new file mode 100644 index 0000000000..60bb7d0c26 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/qq.js @@ -0,0 +1,131 @@ +const cache = require('../cache') +const insure = require('./insure') +const request = require('../request') + +let headers = { + 'origin': 'http://y.qq.com/', + 'referer': 'http://y.qq.com/' +} + +const playable = song => { + let switchFlag = song['switch'].toString(2).split('') + switchFlag.pop() + switchFlag.reverse() + let playFlag = switchFlag[0] + let tryFlag = switchFlag[13] + return ((playFlag == 1) || ((playFlag == 1) && (tryFlag == 1))) +} + +const search = info => { + let url = + 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + + 'ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center' + + '&searchid=46804741196796149&t=0&aggr=1&cr=1&catZhida=1&lossless=0' + + '&flag_qc=0&p=1&n=20&w=' + encodeURIComponent(info.keyword) + + '&g_tk=5381&jsonpCallback=MusicJsonCallback10005317669353331&loginUin=0&hostUin=0' + + '&format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0' + + return request('GET', url) + .then(response => response.jsonp()) + .then(jsonBody => { + let matched = jsonBody.data.song.list[0] + if(matched) + return matched.file.media_mid + else + return Promise.reject() + }) +} + +const ticket = () => { + const classic = ['001yS0N33yPm1B', '000bog5B2DYgHN', '002bongo1BDtKz', '004RDW5Q2ol2jj', '001oEME64eXNbp', '001e9dH11YeXGp', '0021onBk2QNjBu', '001YoUs11jvsIK', '000SNxc91Mw3UQ', '002k94ea4379uy'] + const id = classic[Math.floor(classic.length * Math.random())] + + // let url = + // 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' + + // '?g_tk=0&loginUin=0&hostUin=0&format=json&inCharset=utf8' + + // '&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0' + + // '&cid=205361747&uin=0&guid=7332953645' + + // '&songmid='+ id + '&filename=C400'+ id + '.m4a' + + // return request('GET', url, headers) + // .then(response => response.json()) + // .then(jsonBody => { + // let vkey = jsonBody.data.items[0].vkey + // if(vkey) + // return vkey + // else + // return Promise.reject() + // }) + // .catch(() => insure().qq.ticket()) + + let url = + 'https://u.y.qq.com/cgi-bin/musicu.fcg?data=' + + encodeURIComponent(JSON.stringify({ + // req: { + // method: 'GetCdnDispatch', + // module: 'CDN.SrfCdnDispatchServer', + // param: { + // calltype: 0, + // guid: '7332953645', + // userip: '' + // } + // }, + req_0: { + module: 'vkey.GetVkeyServer', + method: 'CgiGetVkey', + param: { + guid: '7332953645', + loginflag: 1, + songmid: [id], + songtype: [0], + uin: '0', + platform: '20' + } + } + })) + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + let vkey = + jsonBody.req_0.data.midurlinfo[0].vkey || + (jsonBody.req_0.data.testfile2g.match(/vkey=(\w+)/) || [])[1] + if(vkey) + return vkey + else + return Promise.reject() + }) + .catch(() => insure().qq.ticket()) +} + +const track = id => { + return cache(ticket) + .then(vkey => { + let host = ['streamoc.music.tc.qq.com', 'isure.stream.qqmusic.qq.com', 'dl.stream.qqmusic.qq.com', 'aqqmusic.tc.qq.com/amobile.music.tc.qq.com'][1] + let songUrl = + 'http://' + host + '/M500' + id + + '.mp3?vkey=' + vkey + + '&uin=0&fromtag=8&guid=7332953645' + return songUrl + }) + + // return request( + // 'POST', 'http://acc.music.qq.com/base/fcgi-bin/fcg_music_express_mobile2.fcg', {}, + // ` + // 9070003190 + // 4600100100105 + // 02123456782 + // 352iosM800${id}.mp30 + // `.replace(/\s/, '') + // ) + // .then(response => response.body(true)) + // .then(body => { + // let xml = require('zlib').inflateSync(body.slice(5)).toString() + // let focus = xml.match(/(.+)<\/item>/) + // return `http://streamoc.music.tc.qq.com/${focus[1]}?vkey=${focus[2]}&guid=0&uin=12345678&fromtag=6` + // }) +} + +const check = info => cache(search, info).then(track) + +module.exports = {check, ticket} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/xiami.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/xiami.js new file mode 100644 index 0000000000..b16919863a --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/provider/xiami.js @@ -0,0 +1,107 @@ +const cache = require('../cache') +const insure = require('./insure') +const crypto = require('../crypto') +const request = require('../request') + +let headers = { + 'origin': 'http://www.xiami.com/', + 'referer': 'http://www.xiami.com/' +} + +const caesar = pattern => { + let height = parseInt(pattern[0]) + pattern = pattern.slice(1) + let width = Math.ceil(pattern.length / height) + let unpad = height - (width * height - pattern.length) + + let matrix = Array.from(Array(height).keys()).map(i => + pattern.slice(i < unpad ? i * width : unpad * width + (i - unpad) * (width - 1)).slice(0, i < unpad ? width : width - 1) + ) + + let transpose = Array.from(Array(width).keys()).map(x => + Array.from(Array(height).keys()).map(y => matrix[y][x]).join('') + ) + + return unescape(transpose.join('')).replace(/\^/g, '0') +} + +const token = () => { + return request('GET', 'https://www.xiami.com') + .then(response => + response.headers['set-cookie'].map(line => line.replace(/;.+$/, '')).reduce((cookie, line) => { + line = line.split(/\s*=\s*/) + return Object.assign(cookie, {[decodeURIComponent(line[0])]: decodeURIComponent(line[1])}) + }, {}) + ) +} + +const search = info => { + return cache(token) + .then(cookie => { + const query = JSON.stringify({key: info.keyword, pagingVO: {page: 1, pageSize: 60}}) + const message = cookie['xm_sg_tk'].split('_')[0] + '_xmMain_/api/search/searchSongs_' + query + return request('GET', 'https://www.xiami.com/api/search/searchSongs?_q=' + encodeURIComponent(query) + '&_s=' + crypto.md5.digest(message), { + referer: 'https://www.xiami.com/search?key=' + encodeURIComponent(info.keyword), + cookie: Object.keys(cookie).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(cookie[key])).join('; ') + }) + .then(response => response.json()) + .then(jsonBody => { + let matched = jsonBody.result.data.songs[0] + if(matched) + return matched.songId + else + return Promise.reject() + }) + }) +} + +// const search = info => { +// let url = +// 'http://api.xiami.com/web?v=2.0&app_key=1' + +// '&key=' + encodeURIComponent(info.keyword) + '&page=1' + +// '&limit=20&callback=jsonp154&r=search/songs' + +// return request('GET', url, headers) +// .then(response => { +// let jsonBody = JSON.parse(response.body.slice('jsonp154('.length, -')'.length)) +// let matched = jsonBody.data.songs[0] +// if(matched){ +// if(matched.listen_file) +// return matched.listen_file +// else +// return matched.song_id +// } +// else +// return Promise.reject() +// }) +// } + +const track = id => { + let url = + 'https://www.xiami.com/song/playlist/id/' + id + + '/object_name/default/object_id/0/cat/json' + + return request('GET', url, headers) + .then(response => response.json()) + .then(jsonBody => { + if(jsonBody.data.trackList == null){ + return Promise.reject() + } + else{ + let location = jsonBody.data.trackList[0].location + let songUrl = 'http:' + caesar(location) + return songUrl + } + }) + .then(origin => { + let updated = origin.replace('m128', 'm320') + return request('HEAD', updated) + .then(response => response.statusCode == 200 ? updated : origin) + .catch(() => origin) + }) + .catch(() => insure().xiami.track(id)) +} + +const check = info => cache(search, info).then(track) + +module.exports = {check, track} \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/request.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/request.js new file mode 100644 index 0000000000..c3ded51f7a --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/request.js @@ -0,0 +1,95 @@ +const zlib = require('zlib') +const http = require('http') +const https = require('https') +const parse = require('url').parse + +const translate = host => (global.hosts || {})[host] || host + +const create = url => global.proxy ? (proxy.protocol == 'https:' ? https.request : http.request) : (url.protocol == 'https:' ? https.request : http.request) + +const configure = (method, url, headers) => { + headers = headers || {} + if('content-length' in headers) delete headers['content-length'] + + let options = {} + options._headers = headers + if(global.proxy && url.protocol == 'https:'){ + options.method = 'CONNECT' + options.headers = Object.keys(headers).filter(key => ['host', 'user-agent'].includes(key)).reduce((result, key) => Object.assign(result, {[key]: headers[key]}), {}) + } + else{ + options.method = method + options.headers = headers + } + + if(global.proxy){ + options.hostname = translate(proxy.hostname) + options.port = proxy.port || ((proxy.protocol == 'https:') ? 443 : 80) + options.path = (url.protocol != 'https:') ? ('http://' + translate(url.hostname) + url.path) : (translate(url.hostname) + ':' + (url.port || 443)) + } + else{ + options.hostname = translate(url.hostname) + options.port = url.port || ((url.protocol == 'https:') ? 443 : 80) + options.path = url.path + } + return options +} + +const request = (method, url, headers, body) => { + url = parse(url) + let options = configure(method, url, Object.assign({ + 'host': url.hostname, + 'accept': 'application/json, text/plain, */*', + 'accept-encoding': 'gzip, deflate', + 'accept-language': 'zh-CN,zh;q=0.9', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36' + }, headers)) + + return new Promise((resolve, reject) => { + create(url)(options) + .on('response', response => resolve(response)) + .on('connect', (_, socket) => + https.request({ + method: method, + host: translate(url.hostname), + path: url.path, + headers: options._headers, + socket: socket, + agent: false + }) + .on('response', response => resolve(response)) + .on('error', error => reject(error)) + .end(body) + ) + .on('error', error => reject(error)) + .end(body) + }) + .then(response => { + if([201, 301, 302, 303, 307, 308].includes(response.statusCode)) + return request(method, url.resolve(response.headers.location), headers, body) + else + return Object.assign(response, {url: url, body: raw => read(response, raw), json: () => json(response), jsonp: () => jsonp(response)}) + }) +} + +const read = (connect, raw) => new Promise((resolve, reject) => { + let chunks = [] + connect + .on('data', chunk => chunks.push(chunk)) + .on('end', () => { + let buffer = Buffer.concat(chunks) + buffer = (buffer.length && ['gzip', 'deflate'].includes(connect.headers['content-encoding'])) ? zlib.unzipSync(buffer) : buffer + resolve(raw == true ? buffer : buffer.toString()) + }) + .on('error', error => reject(error)) +}) + +const json = connect => read(connect, false).then(body => JSON.parse(body)) +const jsonp = connect => read(connect, false).then(body => JSON.parse(body.slice(body.indexOf('(') + 1, -')'.length))) + +request.read = read +request.create = create +request.translate = translate +request.configure = configure + +module.exports = request \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.crt b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.crt new file mode 100644 index 0000000000..b58111aee7 --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfTCCAmWgAwIBAgIJAKTlW9B59i1HMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFzZU11c2ljIFJvb3QgQ0ExHTAb +BgNVBAoMFEdpdEh1Yi5jb20gQG5vbmRhbmVlMB4XDTE5MDUxODE2MDYxOFoXDTIw +MDUxNzE2MDYxOFowezELMAkGA1UEBhMCQ04xETAPBgNVBAcMCEhhbmd6aG91MSww +KgYDVQQKDCNOZXRFYXNlIChIYW5nemhvdSkgTmV0d29yayBDby4sIEx0ZDERMA8G +A1UECwwISVQgRGVwdC4xGDAWBgNVBAMMDyoubXVzaWMuMTYzLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALobECypwEoe8VqM/FJvBRR3p2T+ZWdi +MSPrwfiRJr5p7OMtWBlLveCBV85+R3feidYbQTXlvVTdToY+GN6mFE1x6zG2dvLD +s4UuRnipmvGcFYhIRTX8J4AJiN8VMtW0TNXscRMudpz/FAVtsRrggRaThYg4f/rI +oAPMqKMsS4JoYhxs9ED6E6/tpj3XmSg1ekaXhgacYSYHeyxizZwoOFVCLH3TG5sF +sD6CYNnukYol8bR+VRpvHftIYss5Yz+DyyhYEAMJm1CfQo+xoGR3D0ozbT3hUnzm +fEoOhmSp3sALrFVE4iJSuajoh2/3xhmcyi3xZdWyq4F8hpb+URyaoW0CAwEAAaMt +MCswKQYDVR0RBCIwIIINbXVzaWMuMTYzLmNvbYIPKi5tdXNpYy4xNjMuY29tMA0G +CSqGSIb3DQEBCwUAA4IBAQB32SVz5jHUYv3ZG7SNF/LFJ904/LI8QlTe9R+Abb9z +bpXmQeo4pvNNOk3LgcTyuSIPQSHEFn32hk/MedB6Q2cKaGVKQq7Usne1jsV0JirG +wMx3PTcKPnX+XexRY8s6v6cNKSx5YlMQNFeH7p8MgKqdM/UX/dNCxT04X/ClmP1K +/rKqonXn4i3wmWprl7Q7Z1wqt0ygQRkNJKqdYKTu4oQcPON8/dRcseYdJzSoK2/G +H6cOJwKrRLzuUqQlphe6wyUsyTIbIJiFu1a1Gml6zB4lhLZhL89H2lYwdS8wWlc+ +M+wYi+XTM/ylNHEIoKsOe2nscnwi/hTfHJOPPchHbEuM +-----END CERTIFICATE----- diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.js b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.js new file mode 100644 index 0000000000..a57e841a5f --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.js @@ -0,0 +1,177 @@ +const fs = require('fs') +const net = require('net') +const path = require('path') +const parse = require('url').parse + +const hook = require('./hook') +const request = require('./request') + +const proxy = { + core: { + mitm: (req, res) => { + if(req.url == '/proxy.pac'){ + let url = parse('http://' + req.headers.host) + res.writeHead(200, {'Content-Type': 'application/x-ns-proxy-autoconfig'}) + res.end(` + function FindProxyForURL(url, host) { + if (${hook.target.host.map(host => (`host == '${host}'`)).join(' || ')}) { + return 'PROXY ${url.hostname}:${url.port || 80}' + } + return 'DIRECT' + } + `) + } + else{ + const ctx = {res, req} + Promise.resolve() + .then(() => proxy.authenticate(ctx)) + .then(() => hook.request.before(ctx)) + .then(() => proxy.filter(ctx)) + .then(() => proxy.log(ctx)) + .then(() => proxy.mitm.request(ctx)) + .then(() => hook.request.after(ctx)) + .then(() => proxy.mitm.response(ctx)) + .catch(() => proxy.mitm.close(ctx)) + } + }, + tunnel: (req, socket, head) => { + const ctx = {req, socket, head} + Promise.resolve() + .then(() => proxy.authenticate(ctx)) + .then(() => hook.connect.before(ctx)) + .then(() => proxy.filter(ctx)) + .then(() => proxy.log(ctx)) + .then(() => proxy.tunnel.connect(ctx)) + .then(() => proxy.tunnel.handshake(ctx)) + .then(() => proxy.tunnel.pipe(ctx)) + .catch(() => proxy.tunnel.close(ctx)) + } + }, + log: ctx => { + const mark = {close: '|', blank: '-', proxy: '>'}[ctx.decision] || '>' + if(ctx.socket) + console.log('TUNNEL', mark, ctx.req.url) + else + console.log('MITM', mark, parse(ctx.req.url).host, ctx.req.socket.encrypted ? '(ssl)' : '') + }, + authenticate: ctx => { + const req = ctx.req + const res = ctx.res + const socket = ctx.socket + let credential = Buffer.from((req.headers['proxy-authorization'] || '').split(/\s+/).pop() || '', 'base64').toString() + if(server.authentication && credential != server.authentication){ + if(socket) + socket.write('HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="realm"\r\n\r\n') + else + res.writeHead(407, {'proxy-authenticate': 'Basic realm="realm"'}) + return Promise.reject(ctx.error = 'authenticate') + } + }, + filter: ctx => { + const url = parse((ctx.socket ? 'https://' : '') + ctx.req.url) + const match = pattern => url.href.search(new RegExp(pattern, 'g')) != -1 + if(!(ctx.decision || ctx.req.local)){ + try{ + let allow = server.whitelist.some(match) + let deny = server.blacklist.some(match) + // console.log('allow', allow, 'deny', deny) + if(!allow && deny){ + return Promise.reject(ctx.error = 'filter') + } + } + catch(error){ + ctx.error = error + } + } + }, + mitm: { + request: ctx => new Promise((resolve, reject) => { + if(ctx.decision === 'close') return reject(ctx.error = ctx.decision) + const req = ctx.req + const url = parse(req.url) + const options = request.configure(req.method, url, req.headers) + ctx.proxyReq = request.create(url)(options) + .on('response', proxyRes => { + return resolve(ctx.proxyRes = proxyRes) + }) + .on('error', error => { + return reject(ctx.error = error) + }) + req.readable ? req.pipe(ctx.proxyReq) : ctx.proxyReq.end(req.body) + }), + response: ctx => { + const res = ctx.res + const proxyRes = ctx.proxyRes + res.writeHead(proxyRes.statusCode, proxyRes.headers) + proxyRes.readable ? proxyRes.pipe(res) : res.end(proxyRes.body) + }, + close: ctx => { + ctx.res.socket.end() + } + }, + tunnel: { + connect: ctx => new Promise((resolve, reject) => { + if(ctx.decision === 'close') return reject(ctx.error = ctx.decision) + const req = ctx.req + const socket = ctx.socket + const head = ctx.head + const url = parse('https://' + req.url) + socket.on('error', error => { + return reject(ctx.error = error) + }) + if(global.proxy && !req.local){ + const options = request.configure(req.method, url, req.headers) + request.create(proxy)(options) + .on('connect', (_, proxySocket) => { + return resolve(ctx.proxySocket = proxySocket) + }) + .on('error', error => { + return reject(ctx.error = error) + }) + .end() + } + else{ + const proxySocket = net.connect(url.port || 443, request.translate(url.hostname)) + .on('connect', () => { + proxySocket.write(head) + return resolve(ctx.proxySocket = proxySocket) + }) + .on('error', error => { + return reject(ctx.error = error) + }) + } + }), + handshake: ctx => { + const req = ctx.req + const socket = ctx.socket + const message = `HTTP/${req.httpVersion} 200 Connection established\r\n\r\n` + socket.write(message) + }, + pipe: ctx => new Promise((resolve, reject) => { + if(ctx.decision === 'blank') return reject(ctx.error = ctx.decision) + const socket = ctx.socket + const proxySocket = ctx.proxySocket + socket.pipe(proxySocket) + proxySocket.pipe(socket) + }), + close: ctx => { + ctx.socket.end() + } + } +} + +const options = { + key: fs.readFileSync(path.join(__dirname, 'server.key')), + cert: fs.readFileSync(path.join(__dirname, 'server.crt')) +} + +const server = { + http: require('http').createServer().on('request', proxy.core.mitm).on('connect', proxy.core.tunnel), + https: require('https').createServer(options).on('request', proxy.core.mitm).on('connect', proxy.core.tunnel) +} + +server.whitelist = [] +server.blacklist = ['//127\.\d+\.\d+\.\d+', '//localhost'] +server.authentication = null + +module.exports = server \ No newline at end of file diff --git a/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.key b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.key new file mode 100644 index 0000000000..ce16c73adc --- /dev/null +++ b/package/lean/luci-app-unblockmusic/root/usr/share/unblockmusic/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuhsQLKnASh7xWoz8Um8FFHenZP5lZ2IxI+vB+JEmvmns4y1Y +GUu94IFXzn5Hd96J1htBNeW9VN1Ohj4Y3qYUTXHrMbZ28sOzhS5GeKma8ZwViEhF +NfwngAmI3xUy1bRM1exxEy52nP8UBW2xGuCBFpOFiDh/+sigA8yooyxLgmhiHGz0 +QPoTr+2mPdeZKDV6RpeGBpxhJgd7LGLNnCg4VUIsfdMbmwWwPoJg2e6RiiXxtH5V +Gm8d+0hiyzljP4PLKFgQAwmbUJ9Cj7GgZHcPSjNtPeFSfOZ8Sg6GZKnewAusVUTi +IlK5qOiHb/fGGZzKLfFl1bKrgXyGlv5RHJqhbQIDAQABAoIBAEmAvtalBMlBh1mY +LV/xcTQwPfDpeOtoILhrOOUPjxnNhD4FfrIe9BNjgmaQAXIadp4VjZ/X6PtHnOfw +RqpJNeOQhq/PvRMMsC59pF+rvQKH/wkgYhV8Ta2IFoLlQHqfB2nGRLKquzYumJ28 +QSK4YMOl6CtxBTrrWiemAUTRDdGm8tARiipJH1SEJrS6d/NoRoJx2vixFgD2eS6X +bjnhGjIzvX/w5FWjctqj+SFITP1UI62b6DyWsPOkoosKNteK+Ulz+K6ZFvOx7day +XgUoTcVpwCVr2dVGhJtOrbKPcl1jYCYHJAHwzUZND4x4yftm1mnnsi3bthYqbtHQ +vxLE9YECgYEA9hiZxwiVvLjSe1xT/D75HbB8S1XSnwzpMmqgzStguxCQ0Qg5yiLI +UKRDY8UZvEDV4i2bQGy7mk8lFvX1q2z7Q30+dtT9r2N9a6ujMk5RMfo2BZg/poI6 +yDWe2tKUg9cTwfgni4TutLOYkpz3VDPIQHs3k2mpNh7f+8X4RIybDqkCgYEAwZhp +uWMV38Bb0WytswHXL1dRuwBskKqALUBY61dtXkyBuocj8AuRRxfxfZpgJRrHFxDX +O9bQ2nxpVlwKsR6DJDUdxU3+kvwyPfseU5XUBey8WdkuAKD7cKZOHMhFVWccks0U +YJzykNrxB+rGTiwVKa0MOhipuJ7boerwwaN2SyUCgYBP9Ow5o4tq9q3EUNoksZ0k +zUuE+oxlCr/VlplKL9bM0HQMxlxoVWa59LTEfKyA4pvbUbAIfYtydlZ5oE5CdTUp +105tM4R88Jk2W1y5ooJ093OH29CKW/OXSvyi4hpIv592vRa0GOupoFRpBkDBhdWB +RcdnyMOmht+FIOwp8XkLiQKBgAUK3j4Y6ZnxXbLfvMp70soF4TgYs7s05a/IDEjc +9xlMrthX6sS22GrcocqeucBdqS/dnW2Ok9QNB4VbUl/4pnvL8mGQPYBAl2Jr5wdQ +ULxyxRkmAf+8MbBmdIRlZwDpdaIRO2Wk0OCbA0osgEvK9CYovrfIqqsHYDsgbnLs +ugkNAoGBAJok06BN05caPXXLQ2pMwI/7mjcZFjcOMxSloYi7LFkxlyvoTqReAeSa +yOb6W/7obS1X8ms/EAkqiyzJuPtNZJCW/nvV0iCoZ/NxLuyHnFaO344GBAweol+S +Jx0MY8KuDCyeGErc2xdz/yr3ld2PSTq71dhBluGyba2YX+peJ2Yv +-----END RSA PRIVATE KEY----- diff --git a/package/lean/luci-app-unblockmusic/views/views1.jpg b/package/lean/luci-app-unblockmusic/views/views1.jpg new file mode 100644 index 0000000000..8a428e9606 Binary files /dev/null and b/package/lean/luci-app-unblockmusic/views/views1.jpg differ diff --git a/package/lean/luci-app-unblockmusic/views/views2.jpg b/package/lean/luci-app-unblockmusic/views/views2.jpg new file mode 100644 index 0000000000..a36e259264 Binary files /dev/null and b/package/lean/luci-app-unblockmusic/views/views2.jpg differ