Skip to content

Commit

Permalink
symc source && bump version
Browse files Browse the repository at this point in the history
  • Loading branch information
1715173329 committed Jul 17, 2019
1 parent 0f0ea19 commit 6c180c6
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 20 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ LUCI_TITLE:=LuCI support for UnblockNeteaseMusic
LUCI_DEPENDS:=+node +bash +dnsmasq-full +ipset
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-unblockmusic
PKG_VERSION:=1.2
PKG_RELEASE:=3
PKG_VERSION:=1.3
PKG_RELEASE:=1

PKG_MAINTAINER:=<https://github.com/shell-script/luci-app-unblockmusic>

Expand Down
190 changes: 190 additions & 0 deletions root/usr/share/unblockmusic/README.md
Original file line number Diff line number Diff line change
@@ -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/60316017-87de8a80-999b-11e9-9381-16d40efbe7f6.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/60316169-18b56600-999c-11e9-8ae5-5cd168b0edae.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
10 changes: 6 additions & 4 deletions root/usr/share/unblockmusic/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ hook.target.path = [
'/api/v1/playlist/manipulate/tracks',
'/api/song/like',
'/api/v1/play/record',
'/api/playlist/v4/detail'
'/api/playlist/v4/detail',
'/api/v1/radio/get',
'/api/v1/discovery/recommend/songs'
]

hook.request.before = ctx => {
Expand Down Expand Up @@ -257,7 +259,7 @@ const tryMatch = ctx => {
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.url = global.endpoint ? `${global.endpoint}/package/${crypto.base64.encode(song.url)}/${item.id}.mp3` : song.url
item.md5 = song.md5 || crypto.md5.digest(song.url)
item.size = song.size
item.code = 200
Expand All @@ -280,8 +282,8 @@ const tryMatch = ctx => {
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))
let os = header.os, version = header.appver
if(os in limit && newer(limit[os], version))
return cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then(value => item.md5 = value)
}
catch(e){}
Expand Down
2 changes: 1 addition & 1 deletion root/usr/share/unblockmusic/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion root/usr/share/unblockmusic/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unblockneteasemusic",
"version": "0.15.2",
"version": "0.16.0",
"description": "Revive unavailable songs for Netease Cloud Music",
"main": "provider/match.js",
"bin": {
Expand Down
4 changes: 2 additions & 2 deletions root/usr/share/unblockmusic/provider/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const find = id => {
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.artists.map(artist => artist.name).join(' / ') + ' - ' + info.name
info.keyword = info.name + ' - ' + info.artists.map(artist => artist.name).join(' / ')
return info
})
}

module.exports = id => cache(find, id)
module.exports = id => cache(find, id)
2 changes: 1 addition & 1 deletion root/usr/share/unblockmusic/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const request = (method, url, headers, body) => {
.end(body)
)
.on('error', error => reject(error))
.end(body)
.end(options.method.toUpperCase() === 'CONNECT' ? null : body)
})
.then(response => {
if([201, 301, 302, 303, 307, 308].includes(response.statusCode))
Expand Down
30 changes: 21 additions & 9 deletions root/usr/share/unblockmusic/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const proxy = {
else{
const ctx = {res, req}
Promise.resolve()
.then(() => proxy.protect(ctx))
.then(() => proxy.authenticate(ctx))
.then(() => hook.request.before(ctx))
.then(() => proxy.filter(ctx))
Expand All @@ -37,6 +38,7 @@ const proxy = {
tunnel: (req, socket, head) => {
const ctx = {req, socket, head}
Promise.resolve()
.then(() => proxy.protect(ctx))
.then(() => proxy.authenticate(ctx))
.then(() => hook.connect.before(ctx))
.then(() => proxy.filter(ctx))
Expand All @@ -47,6 +49,19 @@ const proxy = {
.catch(() => proxy.tunnel.close(ctx))
}
},
abort: (socket, from) => {
// console.log('call abort', from)
if(socket) socket.end()
if(socket && !socket.destroyed) socket.destroy()
},
protect: ctx => {
const req = ctx.req
const res = ctx.res
const socket = ctx.socket
if(req) req.on('error', () => proxy.abort(req.socket, 'req'))
if(res) res.on('error', () => proxy.abort(res.socket, 'res'))
if(socket) socket.on('error', () => proxy.abort(socket, 'socket'))
},
log: ctx => {
const mark = {close: '|', blank: '-', proxy: '>'}[ctx.decision] || '>'
if(ctx.socket)
Expand All @@ -59,7 +74,8 @@ const proxy = {
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('proxy-authorization' in req.headers) delete req.headers['proxy-authorization']
if(server.authentication && credential != server.authentication && (socket || req.url.startsWith('http://'))){
if(socket)
socket.write('HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="realm"\r\n\r\n')
else
Expand Down Expand Up @@ -101,24 +117,20 @@ const proxy = {
}),
response: ctx => {
const res = ctx.res
const proxyRes = ctx.proxyRes
const proxyRes = ctx.proxyRes.on('error', () => proxy.abort(ctx.proxyRes.socket, 'proxyRes'))
res.writeHead(proxyRes.statusCode, proxyRes.headers)
proxyRes.readable ? proxyRes.pipe(res) : res.end(proxyRes.body)
},
close: ctx => {
ctx.res.socket.end()
proxy.abort(ctx.res.socket, 'mitm')
}
},
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)
Expand Down Expand Up @@ -150,12 +162,12 @@ const proxy = {
pipe: ctx => new Promise((resolve, reject) => {
if(ctx.decision === 'blank') return reject(ctx.error = ctx.decision)
const socket = ctx.socket
const proxySocket = ctx.proxySocket
const proxySocket = ctx.proxySocket.on('error', () => proxy.abort(ctx.proxySocket, 'proxySocket'))
socket.pipe(proxySocket)
proxySocket.pipe(socket)
}),
close: ctx => {
ctx.socket.end()
proxy.abort(ctx.socket, 'tunnel')
}
}
}
Expand Down

0 comments on commit 6c180c6

Please sign in to comment.