add luci app unblockmusic for neteast

This commit is contained in:
LEAN-ESX 2019-06-11 06:39:54 -07:00
parent 5a984a5815
commit 7a0f8ee18c
53 changed files with 3461 additions and 0 deletions

View File

@ -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:=<https://github.com/maxlicheng/luci-app-unblockmusic>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -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

View File

@ -0,0 +1,29 @@
mp = Map("unblockmusic", translate("解锁网易云灰色歌曲"))
mp.description = translate("原理:采用 [网易云旧链/QQ/虾米/百度/酷狗/酷我/咕咪/JOOX]等音源 替换网易云变灰歌曲链接<br />具体使用方法可查看github<br />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

View File

@ -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

View File

@ -0,0 +1,22 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[services]], [[unblockmusic]], [[status]])%>', null,
function(x, data) {
var tb = document.getElementById('unblockmusic_status');
if (data && tb) {
if (data.running) {
var links = '<em><b><font color=green>Unblock NeteaseCloudMusic <%:RUNNING%></font></b></em>';
tb.innerHTML = links;
} else {
tb.innerHTML = '<em><b><font color=red>Unblock NeteaseCloudMusic <%:NOT RUNNING%></font></b></em>';
}
}
}
);
//]]>
</script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="unblockmusic_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>

View File

@ -0,0 +1,6 @@
config unblockmusic
option usicapptype 'default'
option port '5200'
option enabled '0'

View File

@ -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
}

View File

@ -0,0 +1,3 @@
#!/bin/sh
sleep 60 && /etc/init.d/unblockmusic restart

View File

@ -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

View File

@ -0,0 +1,15 @@
.git
.vscode
.npmignore
.gitignore
.dockerignore
LICENSE
Procfile
README.md
node_modules
npm-debug.log
Dockerfile*
docker-compose*

View File

@ -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

View File

@ -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"]

View File

@ -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.

View File

@ -0,0 +1,190 @@
<img src="https://user-images.githubusercontent.com/26399680/47980314-0e3f1700-e102-11e8-8857-e3436ecc8beb.png" alt="logo" width="140" height="140" align="right">
# 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 <name>:<password>` 设置代理用户名密码~~ (目前密码认证在 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 文件添加两条规则
```
<Server IP> music.163.com
<Server IP> 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://<Server Name:PORT>/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<String>||undefined} source support netease, qq, xiami, baidu, kugou, kuwo, migu, joox
* @return {Promise<Object>}
*/
match(418602084, ['netease', 'qq', 'xiami', 'baidu']).then(song => console.log(song))
```
## 效果
#### Windows 客户端
<img src="https://user-images.githubusercontent.com/26399680/57972590-425b9480-79cf-11e9-9761-b46a12d36249.png" width="100%">
#### UWP 客户端
<img src="https://user-images.githubusercontent.com/26399680/52215123-5a028780-28ce-11e9-8491-08c4c5dac3b4.png" width="100%">
#### Linux 客户端
<img src="https://user-images.githubusercontent.com/26399680/52214856-a7cac000-28cd-11e9-92dd-0c41dc619481.png" width="100%">
#### macOS 客户端
<img src="https://user-images.githubusercontent.com/26399680/52196035-51418f80-2895-11e9-8f33-78a631cdf151.png" width="100%">
#### Android 客户端
<img src="https://user-images.githubusercontent.com/26399680/57972549-eabd2900-79ce-11e9-8fef-95cb60906298.png" width="50%">
#### iOS 客户端
<img src="https://user-images.githubusercontent.com/26399680/57972440-f90a4580-79cc-11e9-8dbf-6150ee299b9c.jpg" width="50%">
## 致谢
感谢大佬们为逆向 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

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,4 @@
<script src="./crypto-js/core.js"></script>
<script src="./crypto-js/md5.js"></script>
<script src="./crypto-js/enc-base64.js"></script>
<script type="module" src="background.js"></script>

View File

@ -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'])

View File

@ -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
})

View File

@ -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;
}));

View File

@ -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;
}));

View File

@ -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;
}));

View File

@ -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()
}
}

View File

@ -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)
}
}
})()

View File

@ -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"]
}

View File

@ -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))
}))

View File

@ -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)
})()

View File

@ -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-----

View File

@ -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
}

View File

@ -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

View File

@ -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()))
})
}
}

View File

@ -0,0 +1,9 @@
version: '3'
services:
unblockneteasemusic:
image: nondanee/unblockneteasemusic
environment:
NODE_ENV: production
ports:
- 8080:8080

View File

@ -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

View File

@ -0,0 +1,5 @@
{
"name": "unblockneteasemusic",
"version": "0.15.1",
"lockfileVersion": 1
}

View File

@ -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": {}
}

View File

@ -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}

View File

@ -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)

View File

@ -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
}

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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&notice=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&notice=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', {},
// `<root>
// <uid></uid><sid></sid><v>90</v><cv>70003</cv><ct>19</ct><OpenUDID>0</OpenUDID>
// <mcc>460</mcc><mnc>01</mnc><chid>001</chid><webp>0</webp><gray>0</gray><patch>105</patch>
// <jailbreak>0</jailbreak><nettype>2</nettype><qq>12345678</qq><authst></authst><localvip>2</localvip>
// <cid>352</cid><platform>ios</platform><musicname>M800${id}.mp3</musicname><downloadfrom>0</downloadfrom>
// </root>`.replace(/\s/, '')
// )
// .then(response => response.body(true))
// .then(body => {
// let xml = require('zlib').inflateSync(body.slice(5)).toString()
// let focus = xml.match(/<item name="(.+)">(.+)<\/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}

View File

@ -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}

View File

@ -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

View File

@ -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-----

View File

@ -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

View File

@ -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-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB