├── .github
└── workflows
│ └── android.yml
├── Easy163
├── .gitignore
├── .idea
│ ├── codeStyles
│ │ └── Project.xml
│ ├── compiler.xml
│ ├── gradle.xml
│ ├── jarRepositories.xml
│ ├── misc.xml
│ └── vcs.xml
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── org
│ │ │ └── ndroi
│ │ │ └── easy163
│ │ │ ├── core
│ │ │ ├── Cache.java
│ │ │ ├── Find.java
│ │ │ ├── Local.java
│ │ │ ├── Search.java
│ │ │ └── Server.java
│ │ │ ├── hooks
│ │ │ ├── BaseHook.java
│ │ │ ├── CollectHook.java
│ │ │ ├── DownloadHook.java
│ │ │ ├── PlaylistHook.java
│ │ │ ├── SongPlayHook.java
│ │ │ └── utils
│ │ │ │ └── JsonUtil.java
│ │ │ ├── providers
│ │ │ ├── KugouMusic.java
│ │ │ ├── KuwoMusic.java
│ │ │ ├── MiguMusic.java
│ │ │ ├── Provider.java
│ │ │ ├── QQMusic.java
│ │ │ └── utils
│ │ │ │ ├── BitRate.java
│ │ │ │ ├── KeywordMatch.java
│ │ │ │ └── MiguCrypto.java
│ │ │ ├── ui
│ │ │ ├── EasyTileService.java
│ │ │ └── MainActivity.java
│ │ │ ├── utils
│ │ │ ├── ConcurrencyTask.java
│ │ │ ├── Crypto.java
│ │ │ ├── EasyLog.java
│ │ │ ├── Keyword.java
│ │ │ ├── ReadStream.java
│ │ │ └── Song.java
│ │ │ └── vpn
│ │ │ ├── LocalVPNService.java
│ │ │ ├── bio
│ │ │ ├── BioTcpHandler.java
│ │ │ ├── BioUdpHandler.java
│ │ │ └── BioUtil.java
│ │ │ ├── block
│ │ │ ├── BlockHttp.java
│ │ │ └── BlockHttps.java
│ │ │ ├── config
│ │ │ └── Config.java
│ │ │ ├── hookhttp
│ │ │ ├── Hook.java
│ │ │ ├── HookHttp.java
│ │ │ ├── Request.java
│ │ │ └── Response.java
│ │ │ ├── tcpip
│ │ │ ├── IpUtil.java
│ │ │ ├── Packet.java
│ │ │ └── TCBStatus.java
│ │ │ └── util
│ │ │ ├── ByteBufferPool.java
│ │ │ └── ProxyException.java
│ │ └── res
│ │ ├── drawable
│ │ └── side_nav_bar.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── app_bar_main.xml
│ │ ├── content_main.xml
│ │ └── nav_header_main.xml
│ │ ├── menu
│ │ └── activity_main_drawer.xml
│ │ ├── mipmap-xxhdpi
│ │ ├── github.png
│ │ ├── icon.png
│ │ └── icon_tile.png
│ │ ├── values-v21
│ │ └── styles.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── network_security_config.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
├── LICENSE
└── README.md
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v3
16 | - name: set up JDK 11
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: '11'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x ./Easy163/gradlew
25 | - name: Build with Gradle
26 | run: cd Easy163 && ./gradlew build
27 |
--------------------------------------------------------------------------------
/Easy163/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches/build_file_checksums.ser
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | .DS_Store
9 | /build
10 | /captures
11 | .externalNativeBuild
12 |
--------------------------------------------------------------------------------
/Easy163/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | xmlns:android
11 |
12 | ^$
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | xmlns:.*
22 |
23 | ^$
24 |
25 |
26 | BY_NAME
27 |
28 |
29 |
30 |
31 |
32 |
33 | .*:id
34 |
35 | http://schemas.android.com/apk/res/android
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | .*:name
45 |
46 | http://schemas.android.com/apk/res/android
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | name
56 |
57 | ^$
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | style
67 |
68 | ^$
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | .*
78 |
79 | ^$
80 |
81 |
82 | BY_NAME
83 |
84 |
85 |
86 |
87 |
88 |
89 | .*
90 |
91 | http://schemas.android.com/apk/res/android
92 |
93 |
94 | ANDROID_ATTRIBUTE_ORDER
95 |
96 |
97 |
98 |
99 |
100 |
101 | .*
102 |
103 | .*
104 |
105 |
106 | BY_NAME
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/Easy163/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Easy163/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/Easy163/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/Easy163/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/Easy163/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Easy163/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/Easy163/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 31
5 |
6 | compileOptions {
7 | sourceCompatibility JavaVersion.VERSION_1_8
8 | targetCompatibility JavaVersion.VERSION_1_8
9 | }
10 | defaultConfig {
11 | applicationId "org.ndroi.easy163"
12 | minSdkVersion 21
13 | targetSdkVersion 31
14 | versionCode 26
15 | versionName "1.8.8"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled true
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation fileTree(dir: 'libs', include: ['*.jar'])
27 | implementation 'com.alibaba:fastjson:1.1.73.android'
28 | implementation 'androidx.appcompat:appcompat:1.3.0'
29 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
30 | implementation 'com.google.android.material:material:1.2.1'
31 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
32 | }
33 |
--------------------------------------------------------------------------------
/Easy163/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/core/Cache.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.core;
2 |
3 | import org.ndroi.easy163.utils.Keyword;
4 | import org.ndroi.easy163.utils.Song;
5 | import java.util.LinkedHashMap;
6 | import java.util.Map;
7 |
8 | public class Cache
9 | {
10 | interface AddAction
11 | {
12 | Object add(String id);
13 | }
14 |
15 | private Map items = new LinkedHashMap<>();
16 | private AddAction addAction;
17 |
18 | public Cache(AddAction addAction)
19 | {
20 | this.addAction = addAction;
21 | }
22 |
23 | public void add(String id, Object value)
24 | {
25 | synchronized (items)
26 | {
27 | items.put(id, value);
28 | }
29 | }
30 |
31 | public Object get(String id)
32 | {
33 | synchronized (items)
34 | {
35 | if (items.containsKey(id))
36 | {
37 | Object value = items.get(id);
38 | return value;
39 | }
40 | if (addAction == null)
41 | {
42 | return null;
43 | }
44 | Object value = addAction.add(id);
45 | if (value != null)
46 | {
47 | add(id, value);
48 | }
49 | return value;
50 | }
51 | }
52 |
53 | /* id --> Keyword */
54 | public static Cache neteaseKeywords = null;
55 |
56 | /* id --> ProviderSong */
57 | public static Cache providerSongs = null;
58 |
59 | public static void init()
60 | {
61 | neteaseKeywords = new Cache(new AddAction()
62 | {
63 | @Override
64 | public Object add(String id)
65 | {
66 | return Find.find(id);
67 | }
68 | });
69 |
70 | providerSongs = new Cache(new AddAction()
71 | {
72 | @Override
73 | public Object add(String id)
74 | {
75 | Song song = Local.get(id);
76 | if(song != null)
77 | {
78 | return song;
79 | }
80 | Keyword keyword = (Keyword) neteaseKeywords.get(id);
81 | return Search.search(keyword);
82 | }
83 | });
84 | }
85 |
86 | public static void clear()
87 | {
88 | providerSongs.items.clear();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/core/Find.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.core;
2 |
3 | import com.alibaba.fastjson.JSONObject;
4 | import org.ndroi.easy163.utils.ReadStream;
5 | import org.ndroi.easy163.utils.Keyword;
6 | import java.io.IOException;
7 | import java.net.HttpURLConnection;
8 | import java.net.URL;
9 |
10 | /* given song id, find the keyword from netease server */
11 | public class Find
12 | {
13 | public static Keyword find(String id)
14 | {
15 | Keyword keyword = null;
16 | String url = "http://music.163.com/api/song/detail?ids=[" + id + "]";
17 | try
18 | {
19 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
20 | connection.setRequestMethod("GET");
21 | connection.connect();
22 | int responseCode = connection.getResponseCode();
23 | if (responseCode == HttpURLConnection.HTTP_OK)
24 | {
25 | byte[] content = ReadStream.read(connection.getInputStream());
26 | String str = new String(content);
27 | JSONObject jsonObject = JSONObject.parseObject(str);
28 | JSONObject songObj = jsonObject.getJSONArray("songs")
29 | .getJSONObject(0);
30 | keyword = new Keyword();
31 | keyword.id = id;
32 | keyword.applyRawSongName(songObj.getString("name"));
33 | for (Object singerObj : songObj.getJSONArray("artists"))
34 | {
35 | JSONObject singer = (JSONObject) singerObj;
36 | keyword.singers.add(singer.getString("name"));
37 | }
38 | }
39 | } catch (IOException e)
40 | {
41 | e.printStackTrace();
42 | }
43 | return keyword;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/core/Local.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.core;
2 |
3 | import android.util.Log;
4 | import com.alibaba.fastjson.JSONObject;
5 | import org.ndroi.easy163.providers.Provider;
6 | import org.ndroi.easy163.utils.EasyLog;
7 | import org.ndroi.easy163.utils.ReadStream;
8 | import org.ndroi.easy163.utils.Song;
9 | import org.ndroi.easy163.vpn.LocalVPNService;
10 | import java.io.File;
11 | import java.io.FileInputStream;
12 | import java.io.FileNotFoundException;
13 | import java.io.FileOutputStream;
14 | import java.io.IOException;
15 | import java.util.HashMap;
16 | import java.util.List;
17 | import java.util.Map;
18 |
19 | public class Local
20 | {
21 | static class Item
22 | {
23 | public String providerName;
24 | public JSONObject jsonObject;
25 | }
26 |
27 | private static Map items = new HashMap<>();
28 | private static String diskFilename = "easy163_id_mid";
29 |
30 | private static File getCacheFile()
31 | {
32 | File cacheDir = LocalVPNService.getContext().getCacheDir();
33 | return new File(cacheDir, diskFilename);
34 | }
35 |
36 | public static void load()
37 | {
38 | items.clear();
39 | String data = "";
40 | try
41 | {
42 | FileInputStream inputStream = new FileInputStream(getCacheFile());
43 | byte[] bytes = ReadStream.read(inputStream);
44 | data = new String(bytes);
45 | inputStream.close();
46 | } catch (FileNotFoundException e)
47 | {
48 | EasyLog.log("未发现本地缓存");
49 | Log.d("Local", "未发现本地缓存");
50 | return;
51 | } catch (IOException e)
52 | {
53 | e.printStackTrace();
54 | EasyLog.log("本地缓存读取失败");
55 | Log.d("Local", "本地缓存读取失败");
56 | return;
57 | }
58 | String[] lines = data.split("\n");
59 | for (String line : lines)
60 | {
61 | int p1 = line.indexOf(' ');
62 | String id = line.substring(0, p1);
63 | int p2 = line.indexOf(' ', p1 + 1);
64 | Item item = new Item();
65 | item.providerName = line.substring(p1 + 1, p2);
66 | item.jsonObject = JSONObject.parseObject(line.substring(p2 + 1));
67 | items.put(id, item);
68 | }
69 | EasyLog.log("本地缓存加载完毕");
70 | Log.d("Local", "本地缓存加载完毕");
71 | }
72 |
73 | public static Song get(String id)
74 | {
75 | Item item = items.get(id);
76 | if(item == null)
77 | {
78 | return null;
79 | }
80 | EasyLog.log("本地缓存命中:" + "[" + item.providerName + "] " + id);
81 | List providers = Provider.getProviders(null);
82 | Provider targetProvider = null;
83 | for (Provider provider : providers)
84 | {
85 | if(provider.getProviderName().equals(item.providerName))
86 | {
87 | targetProvider = provider;
88 | break;
89 | }
90 | }
91 | Song song = null;
92 | if(targetProvider != null)
93 | {
94 | song = targetProvider.fetchSongByJson(item.jsonObject);
95 | if(song == null)
96 | {
97 | items.remove(id);
98 | EasyLog.log("本地缓存失效:" + "[" + item.providerName + "] " + id);
99 | }
100 | }
101 | return song;
102 | }
103 |
104 | public static void put(String id, String providerName, JSONObject jsonObject)
105 | {
106 | if(items.containsKey(id))
107 | {
108 | return;
109 | }
110 | try
111 | {
112 | FileOutputStream outputStream = new FileOutputStream(getCacheFile(), true);
113 | String line = id + " " + providerName + " " + jsonObject.toString() + "\n";
114 | outputStream.write(line.getBytes());
115 | outputStream.close();
116 | } catch (FileNotFoundException e)
117 | {
118 | e.printStackTrace();
119 | } catch (IOException e)
120 | {
121 | e.printStackTrace();
122 | }
123 | }
124 |
125 | public static void clear()
126 | {
127 | items.clear();
128 | getCacheFile().delete();
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/core/Search.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.core;
2 |
3 | import android.util.Log;
4 | import org.ndroi.easy163.providers.Provider;
5 | import org.ndroi.easy163.utils.ConcurrencyTask;
6 | import org.ndroi.easy163.utils.EasyLog;
7 | import org.ndroi.easy163.utils.Keyword;
8 | import org.ndroi.easy163.utils.Song;
9 | import java.util.List;
10 |
11 | /* search Song from providers */
12 | public class Search
13 | {
14 | public static Song search(Keyword targetKeyword)
15 | {
16 | List providers = Provider.getProviders(targetKeyword);
17 | Log.d("search", "start to search: " + targetKeyword.toString());
18 | EasyLog.log("开始全网搜索:" + targetKeyword.toString());
19 | ConcurrencyTask concurrencyTask = new ConcurrencyTask();
20 | for (Provider provider : providers)
21 | {
22 | concurrencyTask.addTask(new Thread(){
23 | @Override
24 | public void run()
25 | {
26 | super.run();
27 | provider.collectCandidateKeywords();
28 | }
29 | });
30 | }
31 | long startTime = System.currentTimeMillis();
32 | while (true)
33 | {
34 | if(concurrencyTask.isAllFinished())
35 | {
36 | Log.d("search", "all providers finish collect");
37 | break;
38 | }
39 | long endTime = System.currentTimeMillis();
40 | if (endTime - startTime > 8 * 1000)
41 | {
42 | Log.d("search", "collect candidateKeywords timeout");
43 | break;
44 | }
45 | try
46 | {
47 | Thread.sleep(100);
48 | } catch (InterruptedException e)
49 | {
50 | e.printStackTrace();
51 | }
52 | }
53 | Provider bestProvider = Provider.selectCandidateKeywords(providers);
54 | if (bestProvider != null)
55 | {
56 | Log.d("search", "bestProvider " + bestProvider.toString());
57 | Song song = null;
58 | try
59 | {
60 | song = bestProvider.fetchSelectedSong();
61 | }catch (Exception e)
62 | {
63 | Log.d("search", "fetchSelectedSong failed");
64 | e.printStackTrace();
65 | }
66 | if(song != null)
67 | {
68 | Log.d("search", "from provider:\n" + song.toString());
69 | EasyLog.log("搜索到音源:" + "[" + bestProvider.getProviderName() + "] " +
70 | bestProvider.getSelectedKeyword().toString());
71 | }else
72 | {
73 | Log.d("search", "fetchSelectedSong failed");
74 | EasyLog.log("未搜索到音源:" + targetKeyword.toString());
75 | }
76 | return song;
77 | }
78 | EasyLog.log("未搜索到音源:" + targetKeyword.toString());
79 | return null;
80 | }
81 | }
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/core/Server.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.core;
2 |
3 | import org.ndroi.easy163.hooks.CollectHook;
4 | import org.ndroi.easy163.hooks.DownloadHook;
5 | import org.ndroi.easy163.hooks.PlaylistHook;
6 | import org.ndroi.easy163.hooks.SongPlayHook;
7 | import org.ndroi.easy163.vpn.block.BlockHttp;
8 | import org.ndroi.easy163.vpn.block.BlockHttps;
9 | import org.ndroi.easy163.vpn.hookhttp.HookHttp;
10 |
11 | public class Server
12 | {
13 | private static Server instance = new Server();
14 |
15 | public static Server getInstance()
16 | {
17 | return instance;
18 | }
19 |
20 | private void setHooks()
21 | {
22 | HookHttp.getInstance().addHook(new PlaylistHook());
23 | HookHttp.getInstance().addHook(new SongPlayHook());
24 | HookHttp.getInstance().addHook(new CollectHook());
25 | HookHttp.getInstance().addHook(new DownloadHook());
26 | }
27 |
28 | private void setHttpsBlock()
29 | {
30 | /*not used yet*/
31 | BlockHttps.getInstance().addHost("music.163.com");
32 | BlockHttps.getInstance().addHost("interface3.music.163.com");
33 | BlockHttps.getInstance().addHost("interface.music.163.com");
34 | BlockHttps.getInstance().addHost("apm.music.163.com");
35 | BlockHttps.getInstance().addHost("apm3.music.163.com");
36 | BlockHttps.getInstance().addHost("clientlog3.music.163.com");
37 | BlockHttps.getInstance().addHost("clientlog.music.163.com");
38 | }
39 |
40 | private void setHttpBlock()
41 | {
42 | BlockHttp.getInstance().addHost("apm.music.163.com");
43 | BlockHttp.getInstance().addHost("apm3.music.163.com");
44 | BlockHttp.getInstance().addHost("clientlog3.music.163.com");
45 | BlockHttp.getInstance().addHost("clientlog.music.163.com");
46 | }
47 |
48 | public void start()
49 | {
50 | setHooks();
51 | setHttpsBlock();
52 | setHttpBlock();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/hooks/BaseHook.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.hooks;
2 |
3 | import android.util.Log;
4 |
5 | import org.ndroi.easy163.vpn.hookhttp.Hook;
6 | import org.ndroi.easy163.vpn.hookhttp.Request;
7 |
8 | import java.util.Random;
9 |
10 | public abstract class BaseHook extends Hook
11 | {
12 | private static final String ip = String.format("175.17.%d.%d", new Random().nextInt(256), new Random().nextInt(256));
13 |
14 | @Override
15 | public void hookRequest(Request request)
16 | {
17 | request.getHeaderFields().remove("X-NAPM-RETRY");
18 | request.getHeaderFields().remove("Accept-Encoding");
19 | request.getHeaderFields().put("X-Real-IP", ip);
20 | }
21 | }
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/hooks/CollectHook.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.hooks;
2 |
3 | import android.util.Log;
4 |
5 | import com.alibaba.fastjson.JSONObject;
6 | import com.alibaba.fastjson.serializer.SerializerFeature;
7 |
8 | import org.ndroi.easy163.utils.Crypto;
9 | import org.ndroi.easy163.vpn.hookhttp.Request;
10 | import org.ndroi.easy163.vpn.hookhttp.Response;
11 |
12 | public class CollectHook extends BaseHook
13 | {
14 | @Override
15 | public boolean rule(Request request)
16 | {
17 | String method = request.getMethod();
18 | String host = request.getHeaderFields().get("Host");
19 | if (!method.equals("POST") || !host.endsWith("music.163.com"))
20 | {
21 | return false;
22 | }
23 | String path = getPath(request);
24 | return path.endsWith("/playlist/manipulate/tracks");
25 | }
26 |
27 | public void hookRequest(Request request)
28 | {
29 | super.hookRequest(request);
30 | Crypto.Request cryptoRequest = Crypto.decryptRequestBody(new String(request.getContent()));
31 | String trackId = cryptoRequest.json.getString("trackIds");
32 | trackId = trackId.substring(2, trackId.length() - 2);
33 | String pid = cryptoRequest.json.getString("pid");
34 | String op = cryptoRequest.json.getString("op");
35 | String postData = "trackIds=[" + trackId + "," + trackId +"]&pid=" + pid + "&op=" + op;
36 | request.setUri("http://music.163.com/api/playlist/manipulate/tracks");
37 | request.setContent(postData.getBytes());
38 | }
39 |
40 | @Override
41 | public void hookResponse(Response response)
42 | {
43 | super.hookResponse(response);
44 | byte[] bytes = response.getContent();
45 | JSONObject jsonObject = JSONObject.parseObject(new String(bytes));
46 | jsonObject.put("code", 200);
47 | jsonObject.remove("message");
48 | if (jsonObject.getString("trackIds") == null)
49 | {
50 | jsonObject.put("trackIds", "[999999]");
51 | jsonObject.put("count", 999);
52 | jsonObject.put("cloudCount", 0);
53 | }
54 | bytes = JSONObject.toJSONString(jsonObject, SerializerFeature.WriteMapNullValue).getBytes();
55 | bytes = Crypto.aesEncrypt(bytes);
56 | response.setContent(bytes);
57 | }
58 | }
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/hooks/DownloadHook.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.hooks;
2 |
3 | import android.util.Log;
4 |
5 | import com.alibaba.fastjson.JSONArray;
6 | import com.alibaba.fastjson.JSONObject;
7 | import com.alibaba.fastjson.serializer.SerializerFeature;
8 |
9 | import org.ndroi.easy163.core.Cache;
10 | import org.ndroi.easy163.utils.Crypto;
11 | import org.ndroi.easy163.utils.Song;
12 | import org.ndroi.easy163.vpn.hookhttp.Request;
13 | import org.ndroi.easy163.vpn.hookhttp.Response;
14 |
15 | import java.io.IOException;
16 | import java.io.InputStream;
17 | import java.net.HttpURLConnection;
18 | import java.net.URL;
19 | import java.security.MessageDigest;
20 | import java.security.NoSuchAlgorithmException;
21 |
22 | /**
23 | * Created by andro on 2020/5/6.
24 | */
25 | public class DownloadHook extends BaseHook
26 | {
27 | @Override
28 | public boolean rule(Request request)
29 | {
30 | String method = request.getMethod();
31 | String host = request.getHeaderFields().get("Host");
32 | if (!method.equals("POST") || !host.endsWith("music.163.com"))
33 | {
34 | return false;
35 | }
36 | String path = getPath(request);
37 | return path.endsWith("/song/enhance/download/url");
38 | }
39 |
40 | @Override
41 | public void hookRequest(Request request)
42 | {
43 | super.hookRequest(request);
44 | Crypto.Request cryptoRequest = Crypto.decryptRequestBody(new String(request.getContent()));
45 | cryptoRequest.path = "/api/song/enhance/player/url";
46 | String id = cryptoRequest.json.getString("id");
47 | cryptoRequest.json.put("ids", "[\"" + id + "\"]");
48 | cryptoRequest.json.remove("id");
49 | byte[] bytes = Crypto.encryptRequestBody(cryptoRequest).getBytes();
50 | request.setUri("http://music.163.com/eapi/song/enhance/player/url");
51 | request.setContent(bytes);
52 | }
53 |
54 | private String preDownloadForMd5(String url)
55 | {
56 | String md5 = "";
57 | try
58 | {
59 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
60 | connection.setRequestMethod("GET");
61 | connection.connect();
62 | int responseCode = connection.getResponseCode();
63 | if (responseCode == HttpURLConnection.HTTP_OK)
64 | {
65 | MessageDigest messageDigest = MessageDigest.getInstance("md5");
66 | InputStream inputStream = connection.getInputStream();
67 | byte[] bytes = new byte[4096*2];
68 | while (true)
69 | {
70 | int readLen = inputStream.read(bytes);
71 | if (readLen == -1)
72 | {
73 | break;
74 | }
75 | messageDigest.update(bytes, 0, readLen);
76 | }
77 | for (byte b : messageDigest.digest())
78 | {
79 | String temp = Integer.toHexString(b & 0xff);
80 | if (temp.length() == 1)
81 | {
82 | temp = "0" + temp;
83 | }
84 | md5 += temp;
85 | }
86 | }
87 | } catch (IOException e)
88 | {
89 | e.printStackTrace();
90 | } catch (NoSuchAlgorithmException e)
91 | {
92 | e.printStackTrace();
93 | }
94 | return md5;
95 | }
96 |
97 | private void handleDownload(JSONObject jsonObject)
98 | {
99 | JSONObject songObject = null;
100 | Object object = jsonObject.get("data");
101 | if (object.getClass().equals(JSONArray.class))
102 | {
103 | songObject = ((JSONArray) object).getJSONObject(0);
104 | jsonObject.put("data", songObject);
105 | } else
106 | {
107 | songObject = (JSONObject) object;
108 | }
109 | if (songObject.getString("url") == null ||
110 | songObject.getIntValue("code") != 200 ||
111 | songObject.getJSONObject("freeTrialInfo") != null)
112 | {
113 | String id = songObject.getString("id");
114 | Song providerSong = (Song) Cache.providerSongs.get(id);
115 | if(providerSong == null)
116 | {
117 | Log.d("DownloadHook", "no provider found");
118 | return;
119 | }
120 | if (providerSong.md5.equals("unknown"))
121 | {
122 | providerSong.md5 = preDownloadForMd5(providerSong.url);
123 | }
124 | songObject.put("code", 200);
125 | songObject.put("url", providerSong.url);
126 | songObject.put("md5", providerSong.md5);
127 | songObject.put("br", providerSong.br);
128 | songObject.put("size", providerSong.size);
129 | songObject.put("freeTrialInfo", null);
130 | songObject.put("level", "standard");
131 | songObject.put("type", "mp3");
132 | songObject.put("encodeType", "mp3");
133 | }
134 | songObject.put("fee", 0);
135 | songObject.put("flag", 0);
136 | }
137 |
138 | @Override
139 | public void hookResponse(Response response)
140 | {
141 | super.hookResponse(response);
142 | byte[] bytes = Crypto.aesDecrypt(response.getContent());
143 | JSONObject jsonObject = JSONObject.parseObject(new String(bytes));
144 | handleDownload(jsonObject);
145 | bytes = JSONObject.toJSONString(jsonObject, SerializerFeature.WriteMapNullValue).getBytes();
146 | bytes = Crypto.aesEncrypt(bytes);
147 | response.setContent(bytes);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/hooks/PlaylistHook.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.hooks;
2 |
3 | import android.util.Log;
4 |
5 | import com.alibaba.fastjson.JSONObject;
6 | import com.alibaba.fastjson.serializer.SerializerFeature;
7 |
8 | import org.ndroi.easy163.core.Cache;
9 | import org.ndroi.easy163.hooks.utils.JsonUtil;
10 | import org.ndroi.easy163.utils.Crypto;
11 | import org.ndroi.easy163.utils.Keyword;
12 | import org.ndroi.easy163.vpn.hookhttp.Request;
13 | import org.ndroi.easy163.vpn.hookhttp.Response;
14 |
15 | import java.io.UnsupportedEncodingException;
16 | import java.util.Arrays;
17 | import java.util.List;
18 |
19 | /**
20 | * Created by andro on 2020/5/3.
21 | */
22 | public class PlaylistHook extends BaseHook
23 | {
24 | private List paths = Arrays.asList(
25 | "/playlist/detail",
26 | "/eapi/playlist/v4/detail",
27 | "/eapi/play-record/playlist/list",
28 | "/eapi/album/v3/detail",
29 | "/discovery/recommend/songs",
30 | "/eapi/album/privilege",
31 | "/eapi/playlist/privilege",
32 | "/eapi/batch",
33 | "/artist/privilege",
34 | "/eapi/artist/top/song",
35 | "/eapi/v3/song/detail",
36 | "/eapi/song/enhance/privilege",
37 | "/eapi/song/enhance/info/get",
38 | "/search/song/get",
39 | "/search/complex/get/",
40 | "/eapi/v1/artist/songs",
41 | "/eapi/v1/search/get"
42 | );
43 |
44 | @Override
45 | public boolean rule(Request request)
46 | {
47 | String method = request.getMethod();
48 | String host = request.getHeaderFields().get("Host");
49 | Log.d("check rule", host + "" + getPath(request));
50 | if (!method.equals("POST") || !host.endsWith("music.163.com"))
51 | {
52 | return false;
53 | }
54 | String path = getPath(request);
55 | for (String p : paths)
56 | {
57 | if (path.contains(p))
58 | {
59 | return true;
60 | }
61 | }
62 | return false;
63 | }
64 |
65 | @Override
66 | public void hookResponse(Response response)
67 | {
68 | super.hookResponse(response);
69 | byte[] bytes = Crypto.aesDecrypt(response.getContent());
70 | JSONObject jsonObject = JSONObject.parseObject(new String(bytes));
71 | cacheKeywords(jsonObject);
72 | modifyPrivileges(jsonObject);
73 | bytes = JSONObject.toJSONString(jsonObject, SerializerFeature.WriteMapNullValue).getBytes();
74 | bytes = Crypto.aesEncrypt(bytes);
75 | response.setContent(bytes);
76 | }
77 |
78 | private void cacheKeywords(JSONObject jsonObject)
79 | {
80 | JsonUtil.traverse(jsonObject, new JsonUtil.Rule()
81 | {
82 | @Override
83 | public void apply(JSONObject object)
84 | {
85 | if (object.containsKey("id") &&
86 | object.containsKey("name") &&
87 | object.containsKey("ar"))
88 | {
89 | String songId = object.getString("id");
90 | Keyword keyword = new Keyword();
91 | keyword.id = songId;
92 | keyword.applyRawSongName(object.getString("name"));
93 | for (Object singerObj : object.getJSONArray("ar"))
94 | {
95 | JSONObject singer = (JSONObject) singerObj;
96 | keyword.singers.add(singer.getString("name"));
97 | }
98 | Cache.neteaseKeywords.add(songId, keyword);
99 | }
100 | }
101 | });
102 | }
103 |
104 | private void modifyPrivileges(JSONObject jsonObject)
105 | {
106 | JsonUtil.traverse(jsonObject, new JsonUtil.Rule()
107 | {
108 | @Override
109 | public void apply(JSONObject object)
110 | {
111 | if(object.containsKey("fee"))
112 | {
113 | object.put("fee", 0);
114 | }
115 | if (object.containsKey("st") &&
116 | object.containsKey("subp") &&
117 | object.containsKey("pl") &&
118 | object.containsKey("dl"))
119 | {
120 | object.put("st", 0);
121 | object.put("subp", 1);
122 | if (object.getIntValue("pl") == 0)
123 | {
124 | object.put("pl", 320000);
125 | }
126 | if (object.getIntValue("dl") == 0)
127 | {
128 | object.put("dl", 320000);
129 | }
130 | }
131 | }
132 | });
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/hooks/SongPlayHook.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.hooks;
2 |
3 | import android.util.Log;
4 | import com.alibaba.fastjson.JSONArray;
5 | import com.alibaba.fastjson.JSONObject;
6 | import com.alibaba.fastjson.serializer.SerializerFeature;
7 |
8 | import org.ndroi.easy163.core.Cache;
9 | import org.ndroi.easy163.utils.ConcurrencyTask;
10 | import org.ndroi.easy163.utils.Crypto;
11 | import org.ndroi.easy163.utils.Song;
12 | import org.ndroi.easy163.vpn.hookhttp.Request;
13 | import org.ndroi.easy163.vpn.hookhttp.Response;
14 |
15 | /**
16 | * Created by andro on 2020/5/5.
17 | */
18 | public class SongPlayHook extends BaseHook
19 | {
20 | @Override
21 | public boolean rule(Request request)
22 | {
23 | String method = request.getMethod();
24 | String host = request.getHeaderFields().get("Host");
25 | if (!method.equals("POST") || !host.endsWith("music.163.com"))
26 | {
27 | return false;
28 | }
29 | String path = getPath(request);
30 | return path.contains("/song/enhance/player/url");
31 | }
32 |
33 | private void handleNoFreeSong(JSONObject jsonObject)
34 | {
35 | ConcurrencyTask concurrencyTask = new ConcurrencyTask();
36 | JSONArray songObjects = jsonObject.getJSONArray("data");
37 | for (Object obj : songObjects)
38 | {
39 | JSONObject songObject = (JSONObject) obj;
40 | if (songObject.getJSONObject("freeTrialInfo") != null || songObject.getIntValue("code") != 200)
41 | {
42 | concurrencyTask.addTask(new Thread()
43 | {
44 | @Override
45 | public void run()
46 | {
47 | super.run();
48 | String id = songObject.getString("id");
49 | Song providerSong = (Song) Cache.providerSongs.get(id);
50 | if (providerSong == null)
51 | {
52 | Log.d("SongPlayHook", "no provider found");
53 | return;
54 | }
55 | songObject.put("fee", 0);
56 | songObject.put("code", 200);
57 | songObject.put("url", providerSong.url);
58 | songObject.put("md5", providerSong.md5);
59 | songObject.put("br", providerSong.br);
60 | songObject.put("size", providerSong.size);
61 | songObject.put("freeTrialInfo", null);
62 | songObject.put("level", "standard");
63 | songObject.put("type", "mp3");
64 | songObject.put("encodeType", "mp3");
65 | }
66 | });
67 | }
68 | }
69 | concurrencyTask.waitAll();
70 | }
71 |
72 | @Override
73 | public void hookResponse(Response response)
74 | {
75 | super.hookResponse(response);
76 | byte[] bytes = Crypto.aesDecrypt(response.getContent());
77 | JSONObject jsonObject = JSONObject.parseObject(new String(bytes));
78 | handleNoFreeSong(jsonObject);
79 | bytes = JSONObject.toJSONString(jsonObject, SerializerFeature.WriteMapNullValue).getBytes();
80 | bytes = Crypto.aesEncrypt(bytes);
81 | response.setContent(bytes);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/hooks/utils/JsonUtil.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.hooks.utils;
2 |
3 | import com.alibaba.fastjson.JSONArray;
4 | import com.alibaba.fastjson.JSONObject;
5 |
6 | /**
7 | * Created by andro on 2020/5/8.
8 | */
9 | public class JsonUtil
10 | {
11 | public interface Rule
12 | {
13 | void apply(JSONObject object);
14 | }
15 |
16 | public static void traverse(Object object, Rule rule)
17 | {
18 | if (object == null || rule == null)
19 | {
20 | return;
21 | }
22 | if (object.getClass().equals(JSONObject.class))
23 | {
24 | JSONObject jsonObject = (JSONObject) object;
25 | rule.apply(jsonObject);
26 | for (Object subObject : jsonObject.values())
27 | {
28 | traverse(subObject, rule);
29 | }
30 | } else if (object.getClass().equals(JSONArray.class))
31 | {
32 | JSONArray jsonArray = (JSONArray) object;
33 | for (Object subObject : jsonArray)
34 | {
35 | traverse(subObject, rule);
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/KugouMusic.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers;
2 |
3 | import android.util.Log;
4 | import com.alibaba.fastjson.JSONArray;
5 | import com.alibaba.fastjson.JSONObject;
6 | import org.ndroi.easy163.core.Local;
7 | import org.ndroi.easy163.utils.Keyword;
8 | import org.ndroi.easy163.utils.ReadStream;
9 | import org.ndroi.easy163.utils.Song;
10 | import java.io.IOException;
11 | import java.net.HttpURLConnection;
12 | import java.net.URL;
13 | import java.security.MessageDigest;
14 | import java.security.NoSuchAlgorithmException;
15 | import java.util.Arrays;
16 |
17 | public class KugouMusic extends Provider
18 | {
19 | public KugouMusic(Keyword targetKeyword)
20 | {
21 | super("kugou", targetKeyword);
22 | }
23 |
24 | @Override
25 | public void collectCandidateKeywords()
26 | {
27 | String query = keyword2Query(targetKeyword);
28 | String url = "http://songsearch.kugou.com/song_search_v2?keyword=" + query + "&page=1";
29 | try
30 | {
31 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
32 | connection.setRequestMethod("GET");
33 | connection.connect();
34 | int responseCode = connection.getResponseCode();
35 | if (responseCode == HttpURLConnection.HTTP_OK)
36 | {
37 | byte[] content = ReadStream.read(connection.getInputStream());
38 | String str = new String(content);
39 | JSONObject jsonObject = JSONObject.parseObject(str);
40 | if (jsonObject != null && jsonObject.getIntValue("status") == 1)
41 | {
42 | try
43 | {
44 | JSONArray candidates = jsonObject.getJSONObject("data").getJSONArray("lists");
45 | for (Object obj : candidates)
46 | {
47 | JSONObject songJsonObject = (JSONObject) obj;
48 | Keyword candidateKeyword = new Keyword();
49 | candidateKeyword.songName = songJsonObject.getString("SongName");
50 | candidateKeyword.singers = Arrays.asList(songJsonObject.getString("SingerName").split("、"));
51 | songJsonObjects.add(songJsonObject);
52 | candidateKeywords.add(candidateKeyword);
53 | }
54 | }catch (Exception e)
55 | {
56 | e.printStackTrace();
57 | }
58 | }
59 | }
60 | } catch (IOException e)
61 | {
62 | e.printStackTrace();
63 | }
64 | }
65 |
66 | @Override
67 | public Song fetchSelectedSong()
68 | {
69 | if(selectedIndex == -1)
70 | {
71 | return null;
72 | }
73 | JSONObject songJsonObject = songJsonObjects.get(selectedIndex);
74 | Log.d("kugou", songJsonObject.toJSONString());
75 | String mId = songJsonObject.getString("HQFileHash");
76 | JSONObject jsonObject = new JSONObject();
77 | jsonObject.put("mid", mId);
78 | Song song = fetchSongByJson(jsonObject);
79 | if(song != null)
80 | {
81 | Local.put(targetKeyword.id, providerName, jsonObject);
82 | }
83 | return song;
84 | }
85 |
86 | @Override
87 | public Song fetchSongByJson(JSONObject jsonObject)
88 | {
89 | String mId = jsonObject.getString("mid");
90 | if (mId == null || mId.isEmpty())
91 | {
92 | return null;
93 | }
94 | Song song = null;
95 | String key = "";
96 | try
97 | {
98 | MessageDigest messageDigest = MessageDigest.getInstance("md5");
99 | for (byte b : messageDigest.digest((mId + "kgcloudv2").getBytes()))
100 | {
101 | String temp = Integer.toHexString(b & 0xff);
102 | if (temp.length() == 1)
103 | {
104 | temp = "0" + temp;
105 | }
106 | key += temp;
107 | }
108 | } catch (NoSuchAlgorithmException e)
109 | {
110 | e.printStackTrace();
111 | }
112 | String url = "http://trackercdn.kugou.com/i/v2/?key=" + key + "&hash=" + mId +
113 | "&br=hq&appid=1005&pid=2&cmd=25&behavior=download";
114 | try
115 | {
116 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
117 | connection.setRequestMethod("GET");
118 | connection.connect();
119 | int responseCode = connection.getResponseCode();
120 | if (responseCode == HttpURLConnection.HTTP_OK)
121 | {
122 | byte[] content = ReadStream.read(connection.getInputStream());
123 | String str = new String(content);
124 | Log.i("Kugou", str);
125 | JSONObject jo = JSONObject.parseObject(str);
126 | song = new Song();
127 | song.url = jo.getJSONArray("url").getString(0);
128 | song.br = jo.getIntValue("bitRate");
129 | song.size = jo.getIntValue("fileSize");
130 | song.md5 = mId.toLowerCase();
131 | }
132 | } catch (IOException e)
133 | {
134 | e.printStackTrace();
135 | }
136 | return song;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/KuwoMusic.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers;
2 |
3 | import android.util.Log;
4 |
5 | import com.alibaba.fastjson.JSONArray;
6 | import com.alibaba.fastjson.JSONObject;
7 |
8 | import org.ndroi.easy163.core.Local;
9 | import org.ndroi.easy163.utils.ReadStream;
10 | import org.ndroi.easy163.utils.Keyword;
11 | import org.ndroi.easy163.utils.Song;
12 | import java.io.IOException;
13 | import java.net.HttpURLConnection;
14 | import java.net.URL;
15 | import java.util.Arrays;
16 |
17 | public class KuwoMusic extends Provider
18 | {
19 | public KuwoMusic(Keyword targetKeyword)
20 | {
21 | super("kuwo", targetKeyword);
22 | }
23 |
24 | @Override
25 | public void collectCandidateKeywords()
26 | {
27 | String query = keyword2Query(targetKeyword);
28 | String token = "1234567890";
29 | String url = "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=" +
30 | query + "&pn=1&rn=30";
31 | try
32 | {
33 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
34 | connection.setRequestMethod("GET");
35 | connection.setRequestProperty("Referer", "http://kuwo.cn/search/list?key=" + query);
36 | connection.setRequestProperty("csrf", token);
37 | connection.setRequestProperty("Cookie", "kw_token=" + token);
38 | connection.connect();
39 | int responseCode = connection.getResponseCode();
40 | if (responseCode == HttpURLConnection.HTTP_OK)
41 | {
42 | byte[] content = ReadStream.read(connection.getInputStream());
43 | String str = new String(content);
44 | JSONObject jsonObject = JSONObject.parseObject(str);
45 | if (jsonObject.getIntValue("code") == 200)
46 | {
47 | JSONArray candidates = jsonObject.getJSONObject("data").getJSONArray("list");
48 | for (Object obj : candidates)
49 | {
50 | JSONObject songJsonObject = (JSONObject) obj;
51 | Keyword candidateKeyword = new Keyword();
52 | candidateKeyword.songName = songJsonObject.getString("name");
53 | candidateKeyword.singers = Arrays.asList(songJsonObject.getString("artist").split("&"));
54 | songJsonObjects.add(songJsonObject);
55 | candidateKeywords.add(candidateKeyword);
56 | }
57 | }
58 | }
59 | } catch (IOException e)
60 | {
61 | e.printStackTrace();
62 | }
63 | }
64 |
65 | @Override
66 | public Song fetchSelectedSong()
67 | {
68 | if(selectedIndex == -1)
69 | {
70 | return null;
71 | }
72 | JSONObject songJsonObject = songJsonObjects.get(selectedIndex);
73 | String mId = songJsonObject.getString("musicrid");
74 | JSONObject jsonObject = new JSONObject();
75 | jsonObject.put("mid", mId);
76 | Song song = fetchSongByJson(jsonObject);
77 | if(song != null)
78 | {
79 | Local.put(targetKeyword.id, providerName, jsonObject);
80 | }
81 | return song;
82 | }
83 |
84 | @Override
85 | public Song fetchSongByJson(JSONObject jsonObject)
86 | {
87 | String mId = jsonObject.getString("mid");
88 | if(mId == null)
89 | {
90 | return null;
91 | }
92 | Song song = null;
93 | String url = "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=" + mId;
94 | try
95 | {
96 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
97 | connection.setRequestMethod("GET");
98 | connection.connect();
99 | int responseCode = connection.getResponseCode();
100 | if (responseCode == HttpURLConnection.HTTP_OK)
101 | {
102 | byte[] content = ReadStream.read(connection.getInputStream());
103 | String songUrl = new String(content);
104 | Log.d("Kuwo", songUrl);
105 | if (songUrl.startsWith("http"))
106 | {
107 | song = generateSong(songUrl);
108 | }
109 | }
110 | } catch (IOException e)
111 | {
112 | e.printStackTrace();
113 | }
114 | return song;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/MiguMusic.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers;
2 |
3 | import android.util.Log;
4 |
5 | import com.alibaba.fastjson.JSONArray;
6 | import com.alibaba.fastjson.JSONObject;
7 |
8 | import org.ndroi.easy163.core.Local;
9 | import org.ndroi.easy163.providers.utils.MiguCrypto;
10 | import org.ndroi.easy163.utils.ReadStream;
11 | import org.ndroi.easy163.utils.ConcurrencyTask;
12 | import org.ndroi.easy163.utils.Keyword;
13 | import org.ndroi.easy163.utils.Song;
14 | import java.io.IOException;
15 | import java.net.HttpURLConnection;
16 | import java.net.URL;
17 | import java.util.Arrays;
18 | import java.util.HashMap;
19 | import java.util.Map;
20 |
21 | public class MiguMusic extends Provider
22 | {
23 | public MiguMusic(Keyword targetKeyword)
24 | {
25 | super("migu", targetKeyword);
26 | }
27 |
28 | private void setHttpHeader(HttpURLConnection connection)
29 | {
30 | connection.setRequestProperty("origin", "https://m.music.migu.cn/");
31 | connection.setRequestProperty("referer", "https://m.music.migu.cn/");
32 | connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36");
33 | }
34 |
35 | @Override
36 | public void collectCandidateKeywords()
37 | {
38 | String query = keyword2Query(targetKeyword);
39 | String url = "https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
40 | query + "&type=2&rows=20&pgc=1";
41 | try
42 | {
43 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
44 | connection.setRequestMethod("GET");
45 | setHttpHeader(connection);
46 | connection.connect();
47 | int responseCode = connection.getResponseCode();
48 | if (responseCode == HttpURLConnection.HTTP_OK)
49 | {
50 | byte[] content = ReadStream.read(connection.getInputStream());
51 | String str = new String(content);
52 | JSONObject jsonObject = JSONObject.parseObject(str);
53 | if (jsonObject == null)
54 | {
55 | return;
56 | }
57 | JSONArray candidates = jsonObject.getJSONArray("musics");
58 | if (candidates == null)
59 | {
60 | return;
61 | }
62 | for (Object infoObj : candidates)
63 | {
64 | JSONObject songJSONObject = (JSONObject) infoObj;
65 | if (songJSONObject.getString("mp3") == null) continue;
66 | String songName = songJSONObject.getString("songName");
67 | Keyword candidateKeyword = new Keyword();
68 | candidateKeyword.songName = songName;
69 | candidateKeyword.singers = Arrays.asList(songJSONObject.getString("singerName").split(", "));
70 | songJsonObjects.add(songJSONObject);
71 | candidateKeywords.add(candidateKeyword);
72 | }
73 | }
74 | } catch (IOException e)
75 | {
76 | e.printStackTrace();
77 | }
78 | }
79 |
80 | private String requestSongUrl(String mId)
81 | {
82 | String url = "https://m.music.migu.cn/migu/remoting/cms_detail_tag?cpid=";
83 | String req = url + mId;
84 | try
85 | {
86 | HttpURLConnection connection = (HttpURLConnection) new URL(req).openConnection();
87 | connection.setRequestMethod("GET");
88 | setHttpHeader(connection);
89 | connection.connect();
90 | int responseCode = connection.getResponseCode();
91 | if (responseCode == HttpURLConnection.HTTP_OK) {
92 | byte[] content = ReadStream.read(connection.getInputStream());
93 | String str = new String(content);
94 | JSONObject jsonObject = JSONObject.parseObject(str);
95 | if (jsonObject.containsKey("data"))
96 | {
97 | String songUrl = jsonObject.getJSONObject("data").getString("listenUrl");
98 | if(songUrl == null || songUrl.isEmpty())
99 | {
100 | return null;
101 | }
102 | return songUrl;
103 | }
104 | }
105 | return null;
106 | } catch (IOException e)
107 | {
108 | e.printStackTrace();
109 | return null;
110 | }
111 | /*
112 | String url = "https://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?dataType=2&";
113 | String param = "{\"copyrightId\":\"" + mId + "\",\"type\":" + type + "}";
114 | String req = url + MiguCrypto.Encrypt(param);
115 | try
116 | {
117 | HttpURLConnection connection = (HttpURLConnection) new URL(req).openConnection();
118 | connection.setRequestMethod("GET");
119 | setHttpHeader(connection);
120 | connection.connect();
121 | int responseCode = connection.getResponseCode();
122 | if (responseCode == HttpURLConnection.HTTP_OK)
123 | {
124 | byte[] content = ReadStream.read(connection.getInputStream());
125 | String str = new String(content);
126 | JSONObject jsonObject = JSONObject.parseObject(str);
127 | String code = jsonObject.getString("returnCode");
128 | if (code.equals("000000"))
129 | {
130 | String songUrl = jsonObject.getJSONObject("data").getString("playUrl");
131 | if(songUrl == null || songUrl.isEmpty())
132 | {
133 | return;
134 | }
135 | if (!songUrl.startsWith("http:"))
136 | {
137 | songUrl = "http:" + songUrl;
138 | }
139 | synchronized(results)
140 | {
141 | results.put(type, songUrl);
142 | }
143 | }
144 | }
145 | } catch (IOException e)
146 | {
147 | e.printStackTrace();
148 | }
149 | */
150 |
151 | }
152 |
153 | @Override
154 | public Song fetchSelectedSong()
155 | {
156 | if(selectedIndex == -1)
157 | {
158 | return null;
159 | }
160 | JSONObject songJsonObject = songJsonObjects.get(selectedIndex);
161 | String mId = songJsonObject.getString("copyrightId");
162 | JSONObject jsonObject = new JSONObject();
163 | jsonObject.put("mid", mId);
164 | Song song = fetchSongByJson(jsonObject);
165 | if(song != null)
166 | {
167 | Local.put(targetKeyword.id, providerName, jsonObject);
168 | }
169 | return song;
170 | }
171 |
172 | @Override
173 | public Song fetchSongByJson(JSONObject jsonObject)
174 | {
175 | String mId = jsonObject.getString("mid");
176 | String url = requestSongUrl(mId);
177 | if (url != null)
178 | {
179 | return generateSong(url);
180 | }
181 | return null;
182 | /*
183 | ConcurrencyTask concurrencyTask = new ConcurrencyTask();
184 | Map typeSongUrls = new HashMap<>();
185 | for (String type : new String[]{"1", "2"})
186 | {
187 | concurrencyTask.addTask(new Thread(){
188 | @Override
189 | public void run()
190 | {
191 | super.run();
192 | requestSongUrl(mId, type, typeSongUrls);
193 | }
194 | });
195 | }
196 | concurrencyTask.waitAll();
197 | if(typeSongUrls.containsKey("2"))
198 | {
199 | return generateSong(typeSongUrls.get("2"));
200 | }
201 | if(typeSongUrls.containsKey("1"))
202 | {
203 | return generateSong(typeSongUrls.get("1"));
204 | }
205 | return null;
206 | */
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/Provider.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers;
2 |
3 | import android.util.Log;
4 | import com.alibaba.fastjson.JSONObject;
5 | import org.ndroi.easy163.providers.utils.BitRate;
6 | import org.ndroi.easy163.providers.utils.KeywordMatch;
7 | import org.ndroi.easy163.utils.ReadStream;
8 | import org.ndroi.easy163.utils.Keyword;
9 | import org.ndroi.easy163.utils.Song;
10 | import java.io.IOException;
11 | import java.io.UnsupportedEncodingException;
12 | import java.net.HttpURLConnection;
13 | import java.net.URL;
14 | import java.net.URLEncoder;
15 | import java.util.ArrayList;
16 | import java.util.Arrays;
17 | import java.util.List;
18 |
19 | public abstract class Provider
20 | {
21 | protected String providerName;
22 | protected Keyword targetKeyword;
23 | protected int selectedIndex = -1;
24 | protected List candidateKeywords = new ArrayList<>();
25 | protected List songJsonObjects = new ArrayList<>();
26 |
27 | public Provider(String providerName, Keyword targetKeyword)
28 | {
29 | this.providerName = providerName;
30 | this.targetKeyword = targetKeyword;
31 | }
32 |
33 | public String getProviderName()
34 | {
35 | return providerName;
36 | };
37 |
38 | public Keyword getSelectedKeyword()
39 | {
40 | if(selectedIndex == -1)
41 | {
42 | return null;
43 | }
44 | return candidateKeywords.get(selectedIndex);
45 | };
46 |
47 | @Override
48 | public String toString()
49 | {
50 | return getClass().getSimpleName();
51 | }
52 |
53 | static protected String keyword2Query(Keyword keyword)
54 | {
55 | String songName = keyword.songName;
56 | if(songName.length() > 20)
57 | {
58 | songName = songName.substring(0, 20);
59 | Log.d("keyword2Query", "too long songName string, truncated");
60 | }
61 | String singers = "";
62 | for (String singer : keyword.singers)
63 | {
64 | String tmp = singers + singer + " ";
65 | if(tmp.length() > 15)
66 | {
67 | Log.d("keyword2Query", "too long singers string, truncated");
68 | break;
69 | }
70 | singers = tmp;
71 | }
72 | String queryStr = songName + " " + singers;
73 | try
74 | {
75 | queryStr = URLEncoder.encode(queryStr, "UTF-8");
76 | } catch (UnsupportedEncodingException e)
77 | {
78 | e.printStackTrace();
79 | }
80 | return queryStr;
81 | }
82 |
83 | static private int calculateScore(Keyword candidateKeyword, Keyword targetKeyword, int index)
84 | {
85 | if(!KeywordMatch.match(candidateKeyword, targetKeyword))
86 | {
87 | return -(50 + 3*index);
88 | }
89 | int score = 5 - 3*index;
90 | String targetName = targetKeyword.songName.toLowerCase();
91 | String candidateSongName = candidateKeyword.songName.toLowerCase();
92 | int candidateLen = candidateSongName.length();
93 | int targetLen = targetName.length();
94 | score -= Math.abs(candidateLen - targetLen);
95 | String leftName = candidateSongName.replace(targetName, "");
96 | List words = Arrays.asList(
97 | "live", "dj", "remix", "cover", "instrumental", "伴奏", "翻唱", "翻自"
98 | );
99 | for (String word : words)
100 | {
101 | if(KeywordMatch.match(word, leftName))
102 | {
103 | if(KeywordMatch.match(word, targetKeyword.extra))
104 | {
105 | score = 7;
106 | }else
107 | {
108 | score -= 2;
109 | }
110 | }
111 | }
112 | score -= Math.abs(targetKeyword.singers.size() - candidateKeyword.singers.size());
113 | for (String targetSinger : targetKeyword.singers)
114 | {
115 | for (String candidateSinger : candidateKeyword.singers)
116 | {
117 | if (KeywordMatch.match(targetSinger, candidateSinger))
118 | {
119 | score += 3;
120 | score -= Math.abs(targetSinger.length() - candidateSinger.length());
121 | }
122 | }
123 | }
124 | return score;
125 | }
126 |
127 | static public Provider selectCandidateKeywords(List providers)
128 | {
129 | Provider bestProvider = null;
130 | int maxScore = -999;
131 | int selectIndex = -1;
132 | for (Provider provider : providers)
133 | {
134 | for (int i = 0; i < provider.candidateKeywords.size(); i++)
135 | {
136 | Keyword candidateKeyword = provider.candidateKeywords.get(i);
137 | int score = calculateScore(candidateKeyword, provider.targetKeyword, i);
138 | Log.d("calculateScore", provider.providerName + "|" + candidateKeyword.toString() + '|' + provider.targetKeyword.toString() + "|" + score);
139 | if(score > maxScore)
140 | {
141 | maxScore = score;
142 | selectIndex = i;
143 | bestProvider = provider;
144 | }
145 | }
146 | }
147 | if(bestProvider != null)
148 | {
149 | bestProvider.selectedIndex = selectIndex;
150 | }
151 | return bestProvider;
152 | }
153 |
154 | static protected Song generateSong(String url)
155 | {
156 | Song song = null;
157 | try
158 | {
159 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
160 | connection.setRequestMethod("GET");
161 | connection.setRequestProperty("range", "bytes=0-8191");
162 | connection.connect();
163 | int responseCode = connection.getResponseCode();
164 | if (responseCode == HttpURLConnection.HTTP_OK ||
165 | responseCode == HttpURLConnection.HTTP_PARTIAL)
166 | {
167 | song = new Song();
168 | song.url = url;
169 | String content_range = connection.getHeaderField("Content-Range");
170 | if (content_range != null)
171 | {
172 | int p = content_range.indexOf('/');
173 | song.size = Integer.parseInt(content_range.substring(p + 1));
174 | } else
175 | {
176 | song.size = connection.getContentLength();
177 | }
178 | String qqMusicMd5 = connection.getHeaderField("Server-Md5");
179 | if (qqMusicMd5 != null)
180 | {
181 | song.md5 = qqMusicMd5;
182 | }
183 | byte[] mp3Data = ReadStream.read(connection.getInputStream());
184 | song.br = BitRate.detect(mp3Data);
185 | }
186 | } catch (IOException e)
187 | {
188 | e.printStackTrace();
189 | }
190 | return song;
191 | }
192 |
193 | abstract public void collectCandidateKeywords();
194 | abstract public Song fetchSelectedSong();
195 | abstract public Song fetchSongByJson(JSONObject jsonObject);
196 |
197 | public static List getProviders(Keyword targetKeyword)
198 | {
199 | List providers = Arrays.asList(
200 | new KuwoMusic(targetKeyword),
201 | new MiguMusic(targetKeyword)
202 | // new QQMusic(targetKeyword)
203 | // new KugouMusic(targetKeyword)
204 | );
205 | return providers;
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/QQMusic.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers;
2 |
3 | import android.util.Log;
4 |
5 | import com.alibaba.fastjson.JSONArray;
6 | import com.alibaba.fastjson.JSONObject;
7 | import org.ndroi.easy163.core.Local;
8 | import org.ndroi.easy163.utils.ReadStream;
9 | import org.ndroi.easy163.utils.Keyword;
10 | import org.ndroi.easy163.utils.Song;
11 | import java.io.IOException;
12 | import java.net.HttpURLConnection;
13 | import java.net.URL;
14 |
15 | public class QQMusic extends Provider
16 | {
17 | public QQMusic(Keyword targetKeyword)
18 | {
19 | super("qq", targetKeyword);
20 | }
21 |
22 | @Override
23 | public void collectCandidateKeywords()
24 | {
25 | String query = keyword2Query(targetKeyword);
26 | String url = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp?" +
27 | "ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&" +
28 | "searchid=46343560494538174&t=0&aggr=1&cr=1&catZhida=1&lossless=0&" +
29 | "flag_qc=0&p=1&n=10&w=" + query + "&" +
30 | "g_tk_new_20200303=5381&g_tk=5381&loginUin=0&hostUin=0&" +
31 | "format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0";
32 | try
33 | {
34 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
35 | connection.setRequestMethod("GET");
36 | connection.connect();
37 | int responseCode = connection.getResponseCode();
38 | if (responseCode == HttpURLConnection.HTTP_OK)
39 | {
40 | byte[] content = ReadStream.read(connection.getInputStream());
41 | String str = new String(content);
42 | Log.i("QQ", str);
43 | JSONObject jsonObject = JSONObject.parseObject(str);
44 | if (jsonObject.getIntValue("code") == 0)
45 | {
46 | JSONArray candidates = jsonObject.getJSONObject("data")
47 | .getJSONObject("song")
48 | .getJSONArray("list");
49 | for (Object infoObj : candidates)
50 | {
51 | JSONObject songJsonObject = (JSONObject) infoObj;
52 | int pay = songJsonObject.getJSONObject("pay").getIntValue("pay_play");
53 | if (pay != 0)
54 | {
55 | continue;
56 | }
57 | int fnote = songJsonObject.getIntValue("fnote");
58 | if (fnote == 4002)
59 | {
60 | continue;
61 | }
62 | JSONObject files = songJsonObject.getJSONObject("file");
63 | if(files.getIntValue("size_128") == 0 && files.getIntValue("size_320") == 0)
64 | {
65 | continue;
66 | }
67 | String songName = songJsonObject.getString("title");
68 | Keyword candidateKeyword = new Keyword();
69 | candidateKeyword.songName = songName;
70 | JSONArray singersObj = songJsonObject.getJSONArray("singer");
71 | for (Object singerObj : singersObj)
72 | {
73 | String singer = ((JSONObject) singerObj).getString("name");
74 | candidateKeyword.singers.add(singer);
75 | }
76 | songJsonObjects.add(songJsonObject);
77 | candidateKeywords.add(candidateKeyword);
78 | }
79 | }
80 | }
81 | } catch (IOException e)
82 | {
83 | e.printStackTrace();
84 | }
85 | }
86 |
87 | @Override
88 | public Song fetchSelectedSong()
89 | {
90 | if(selectedIndex == -1)
91 | {
92 | return null;
93 | }
94 | JSONObject songJsonObject = songJsonObjects.get(selectedIndex);
95 | String mId = songJsonObject.getString("mid");
96 | String mediaMId = songJsonObject.getJSONObject("file").getString("media_mid");
97 | JSONObject jsonObject = new JSONObject();
98 | jsonObject.put("mid", mId);
99 | jsonObject.put("media_mid", mediaMId);
100 | Song song = fetchSongByJson(jsonObject);
101 | if(song != null)
102 | {
103 | Local.put(targetKeyword.id, providerName, jsonObject);
104 | }
105 | return song;
106 | }
107 |
108 | @Override
109 | public Song fetchSongByJson(JSONObject jsonObject)
110 | {
111 | String mId = jsonObject.getString("mid");
112 | String mediaMId = jsonObject.getString("media_mid");
113 | if(mId == null || mediaMId == null)
114 | {
115 | return null;
116 | }
117 | String filename = "M500" + mediaMId + ".mp3";
118 | String url = "https://u.y.qq.com/cgi-bin/musicu.fcg?data=" +
119 | "{\"req_0\":{\"module\":\"vkey.GetVkeyServer\"," +
120 | "\"method\":\"CgiGetVkey\",\"param\":{\"guid\":\"7332953645\"," +
121 | "\"loginflag\":1,\"filename\":[\"" +
122 | filename +
123 | "\"],\"songmid\":[\"" +
124 | mId +
125 | "\"],\"songtype\":[0],\"uin\":\"0\",\"platform\":\"20\"}}}";
126 | Song song = null;
127 | try
128 | {
129 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
130 | connection.setRequestMethod("GET");
131 | connection.connect();
132 | int responseCode = connection.getResponseCode();
133 | if (responseCode == HttpURLConnection.HTTP_OK)
134 | {
135 | byte[] content = ReadStream.read(connection.getInputStream());
136 | String str = new String(content);
137 | JSONObject jo = JSONObject.parseObject(str);
138 | if (jo.getIntValue("code") == 0)
139 | {
140 | String vkey = jo.getJSONObject("req_0")
141 | .getJSONObject("data")
142 | .getJSONArray("midurlinfo")
143 | .getJSONObject(0)
144 | .getString("vkey");
145 | if (!vkey.isEmpty())
146 | {
147 | String songUrl = "http://dl.stream.qqmusic.qq.com/" + filename +
148 | "?vkey=" + vkey + "&uin=0&fromtag=8&guid=7332953645";
149 | song = generateSong(songUrl);
150 | }
151 | }
152 | }
153 | } catch (IOException e)
154 | {
155 | e.printStackTrace();
156 | }
157 | return song;
158 | }
159 | }
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/utils/BitRate.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers.utils;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | /**
7 | * Created by andro on 2020/5/10.
8 | */
9 | public class BitRate
10 | {
11 | private static Map> table = new HashMap<>();
12 |
13 | static
14 | {
15 | int[] arr_3_3 = {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1};
16 | int[] arr_3_2 = {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1};
17 | int[] arr_3_1 = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1};
18 | int[] arr_2_3 = {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1};
19 | int[] arr_2_2 = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1};
20 | Map map_3 = new HashMap<>();
21 | map_3.put(3, arr_3_3);
22 | map_3.put(2, arr_3_2);
23 | map_3.put(1, arr_3_1);
24 | table.put(3, map_3);
25 | Map map_2 = new HashMap<>();
26 | map_2.put(3, arr_2_3);
27 | map_2.put(2, arr_2_2);
28 | map_2.put(1, arr_2_2);
29 | table.put(2, map_2);
30 | table.put(0, map_2);
31 | }
32 |
33 | public static int detect(byte[] bytes)
34 | {
35 | int ptr = 0;
36 | if (bytes[0] == 'f' && bytes[1] == 'L' && bytes[2] == 'a' && bytes[3] == 'C')
37 | {
38 | return 999;
39 | }
40 | if (bytes[0] == 'I' && bytes[1] == 'D' && bytes[2] == '3')
41 | {
42 | ptr = 6;
43 | int size = 0;
44 | for (int i = 0; i < 4; i++)
45 | {
46 | size += (bytes[ptr + i] & 0x7f) << (7 * (3 - i));
47 | }
48 | ptr = 10 + size;
49 | }
50 | int version = ((bytes[ptr + 1] & 0xff) >> 3) & 0x3;
51 | int layer = ((bytes[ptr + 1] & 0xff) >> 1) & 0x3;
52 | int bitrate = (bytes[ptr + 2] & 0xff) >> 4;
53 | int result = table.get(version).get(layer)[bitrate];
54 | return result;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/utils/KeywordMatch.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers.utils;
2 |
3 | import org.ndroi.easy163.utils.Keyword;
4 |
5 | /**
6 | * Created by andro on 2020/5/7.
7 | */
8 | public class KeywordMatch
9 | {
10 | private static String matchStrPreProcess(String str)
11 | {
12 | str = str.toLowerCase().trim();
13 | str = str.replace(',', ',').replace('?', '?');
14 | str = str.replace(", ", ",");
15 | return str;
16 | }
17 |
18 | public static boolean match(String a, String b)
19 | {
20 | if(a == null || b == null || a.isEmpty() || b.isEmpty())
21 | {
22 | return false;
23 | }
24 | a = matchStrPreProcess(a);
25 | b = matchStrPreProcess(b);
26 | boolean isContain = a.contains(b) || b.contains(a);
27 | if(isContain)
28 | {
29 | return true;
30 | }
31 | if(a.contains(" "))
32 | {
33 | a = a.split(" ")[0];
34 | }
35 | if(b.contains(" "))
36 | {
37 | b = b.split(" ")[0];
38 | }
39 | return a.contains(b) || b.contains(a);
40 | }
41 |
42 | public static boolean match(Keyword a, Keyword b)
43 | {
44 | boolean nameMatch = match(a.songName, b.songName);
45 | if (!nameMatch)
46 | {
47 | return false;
48 | }
49 | for (String aSinger : a.singers)
50 | {
51 | for (String bSinger : b.singers)
52 | {
53 | if (match(aSinger, bSinger))
54 | {
55 | return true;
56 | }
57 | }
58 | }
59 | return false;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/providers/utils/MiguCrypto.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.providers.utils;
2 |
3 | import android.util.Base64;
4 | import java.io.UnsupportedEncodingException;
5 | import java.net.URLEncoder;
6 | import java.security.InvalidAlgorithmParameterException;
7 | import java.security.InvalidKeyException;
8 | import java.security.MessageDigest;
9 | import java.security.NoSuchAlgorithmException;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import javax.crypto.BadPaddingException;
13 | import javax.crypto.Cipher;
14 | import javax.crypto.IllegalBlockSizeException;
15 | import javax.crypto.NoSuchPaddingException;
16 | import javax.crypto.spec.IvParameterSpec;
17 | import javax.crypto.spec.SecretKeySpec;
18 |
19 | /**
20 | * Created by andro on 2020/5/6.
21 | */
22 | public class MiguCrypto
23 | {
24 | private static String password = "00000000000000000000000000000000";
25 | private static String salt = "00000000";
26 | private static String skeyB64 = "OMYm0ulbQZgEd21abq1wQI7CnLeAY5CT4RPRLBmAzSUdBWgPHq3n" +
27 | "KTYBNJe9EJMMs2l2aOKPHQCl05QDDfO4wJpzwwL4IFag5u%2FAWY81MZ6SJJpD1gUEw6fVqENIQowg" +
28 | "0bSjZwkY61kY0EIvDNsEZ9TbqFCiy25RXb%2BaLWgcRGE%3D";
29 | private static Cipher aesCipher = null;
30 |
31 | private static void initAes()
32 | {
33 | int keySize = 256 / 8;
34 | int ivSize = 16;
35 | int repeat = (keySize + ivSize) / 16;
36 | List byteList = new ArrayList<>();
37 | byte[] ps = (password + salt).getBytes();
38 | try
39 | {
40 | MessageDigest messageDigest = MessageDigest.getInstance("md5");
41 | for (int i = 0; i < repeat; i++)
42 | {
43 | if (byteList.isEmpty())
44 | {
45 | messageDigest.update(ps);
46 | byteList.add(messageDigest.digest());
47 | } else
48 | {
49 | byte[] last = byteList.get(byteList.size() - 1);
50 | byte[] buffer = new byte[last.length + ps.length];
51 | System.arraycopy(last, 0, buffer, 0, last.length);
52 | System.arraycopy(ps, 0, buffer, last.length, ps.length);
53 | messageDigest.update(buffer);
54 | byteList.add(messageDigest.digest());
55 | }
56 | }
57 | } catch (NoSuchAlgorithmException e)
58 | {
59 | e.printStackTrace();
60 | }
61 | byte[] skey = new byte[16 * 2];
62 | System.arraycopy(byteList.get(0), 0, skey, 0, 16);
63 | System.arraycopy(byteList.get(1), 0, skey, 16, 16);
64 | byte[] sIv = byteList.get(2);
65 | SecretKeySpec keySpec = new SecretKeySpec(skey, "AES");
66 | try
67 | {
68 | aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
69 | aesCipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(sIv));
70 | } catch (InvalidKeyException e)
71 | {
72 | e.printStackTrace();
73 | } catch (InvalidAlgorithmParameterException e)
74 | {
75 | e.printStackTrace();
76 | } catch (NoSuchPaddingException e)
77 | {
78 | e.printStackTrace();
79 | } catch (NoSuchAlgorithmException e)
80 | {
81 | e.printStackTrace();
82 | }
83 | }
84 |
85 | public static String Encrypt(String text)
86 | {
87 | if (aesCipher == null)
88 | {
89 | initAes();
90 | }
91 | String result = null;
92 | try
93 | {
94 | byte[] header = ("Salted__" + salt).getBytes();
95 | byte[] aseEnc = aesCipher.doFinal(text.getBytes());
96 | byte[] data = new byte[header.length + aseEnc.length];
97 | System.arraycopy(header, 0, data, 0, header.length);
98 | System.arraycopy(aseEnc, 0, data, header.length, aseEnc.length);
99 | String dataB64 = Base64.encodeToString(data, Base64.NO_WRAP);
100 | result = "data=" + URLEncoder.encode(dataB64, "UTF-8") + "&secKey=" + skeyB64;
101 | } catch (IllegalBlockSizeException e)
102 | {
103 | e.printStackTrace();
104 | } catch (BadPaddingException e)
105 | {
106 | e.printStackTrace();
107 | } catch (UnsupportedEncodingException e)
108 | {
109 | e.printStackTrace();
110 | }
111 | return result;
112 | }
113 | }
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/ui/EasyTileService.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.ui;
2 |
3 | import android.content.ComponentName;
4 | import android.content.Intent;
5 | import android.net.VpnService;
6 | import android.os.Build;
7 | import android.os.IBinder;
8 | import android.service.quicksettings.Tile;
9 | import android.service.quicksettings.TileService;
10 | import android.util.Log;
11 | import org.ndroi.easy163.vpn.LocalVPNService;
12 |
13 | import androidx.annotation.RequiresApi;
14 | import androidx.core.content.ContextCompat;
15 | import androidx.localbroadcastmanager.content.LocalBroadcastManager;
16 |
17 | @RequiresApi(Build.VERSION_CODES.N)
18 | public class EasyTileService extends TileService
19 | {
20 | @Override
21 | public void onStartListening()
22 | {
23 | super.onStartListening();
24 | Log.d("EasyTileService", "onStartListening");
25 | Tile tile = getQsTile();
26 | if (tile == null)
27 | {
28 | return;
29 | }
30 | if(LocalVPNService.getIsRunning())
31 | {
32 | tile.setState(Tile.STATE_ACTIVE);
33 | tile.updateTile();
34 | }else
35 | {
36 | tile.setState(Tile.STATE_INACTIVE);
37 | tile.updateTile();
38 | }
39 | }
40 |
41 | @Override
42 | public IBinder onBind(Intent intent)
43 | {
44 | TileService.requestListeningState(this, new ComponentName(this, EasyTileService.class));
45 | return super.onBind(intent);
46 | }
47 |
48 | @Override
49 | public void onClick()
50 | {
51 | super.onClick();
52 | Tile tile = getQsTile();
53 | if (tile == null)
54 | {
55 | return;
56 | }
57 | switch (tile.getState())
58 | {
59 | case Tile.STATE_ACTIVE:
60 | {
61 | stopVPN();
62 | tile.setState(Tile.STATE_INACTIVE);
63 | tile.updateTile();
64 | break;
65 | }
66 | case Tile.STATE_INACTIVE:
67 | {
68 | startVPN();
69 | tile.setState(Tile.STATE_ACTIVE);
70 | tile.updateTile();
71 | break;
72 | }
73 | default:break;
74 | }
75 | }
76 |
77 | private void startVPN()
78 | {
79 | if (VpnService.prepare(this) == null)
80 | {
81 | Intent intent = new Intent(this, LocalVPNService.class);
82 | ContextCompat.startForegroundService(this, intent);
83 | } else
84 | {
85 | Intent intent = new Intent(this, MainActivity.class);
86 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
87 | startActivityAndCollapse(intent);
88 | }
89 | }
90 |
91 | private void stopVPN()
92 | {
93 | Intent intent = new Intent("control");
94 | intent.putExtra("cmd", "stop");
95 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
96 | Log.d("stopVPN", "try to stopVPN");
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/ui/MainActivity.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.ui;
2 |
3 | import android.app.ActivityManager;
4 | import android.content.BroadcastReceiver;
5 | import android.content.Context;
6 | import android.content.DialogInterface;
7 | import android.content.Intent;
8 | import android.content.IntentFilter;
9 | import android.net.Uri;
10 | import android.net.VpnService;
11 | import android.os.Bundle;
12 | import com.google.android.material.navigation.NavigationView;
13 |
14 | import androidx.activity.result.ActivityResult;
15 | import androidx.activity.result.ActivityResultCallback;
16 | import androidx.activity.result.ActivityResultLauncher;
17 | import androidx.activity.result.contract.ActivityResultContract;
18 | import androidx.activity.result.contract.ActivityResultContracts;
19 | import androidx.core.content.ContextCompat;
20 | import androidx.localbroadcastmanager.content.LocalBroadcastManager;
21 | import androidx.core.view.GravityCompat;
22 | import androidx.drawerlayout.widget.DrawerLayout;
23 | import androidx.appcompat.app.ActionBarDrawerToggle;
24 | import androidx.appcompat.app.AppCompatActivity;
25 | import androidx.appcompat.widget.Toolbar;
26 |
27 | import android.os.StrictMode;
28 | import android.util.Log;
29 | import android.view.MenuItem;
30 | import android.widget.CompoundButton;
31 | import android.widget.Toast;
32 | import android.widget.ToggleButton;
33 | import org.ndroi.easy163.BuildConfig;
34 | import org.ndroi.easy163.R;
35 | import org.ndroi.easy163.core.Cache;
36 | import org.ndroi.easy163.core.Local;
37 | import org.ndroi.easy163.utils.EasyLog;
38 | import org.ndroi.easy163.vpn.LocalVPNService;
39 | import static androidx.appcompat.app.AlertDialog.Builder;
40 |
41 | public class MainActivity extends AppCompatActivity
42 | implements NavigationView.OnNavigationItemSelectedListener, ToggleButton.OnCheckedChangeListener
43 | {
44 | private ToggleButton toggleButton = null;
45 | private static boolean isBroadcastReceived = false; // workaround for multi-receive
46 | public static void resetBroadcastReceivedState()
47 | {
48 | isBroadcastReceived = false;
49 | }
50 | private ActivityResultLauncher intentActivityResultLauncher;
51 |
52 | private BroadcastReceiver serviceReceiver = new BroadcastReceiver()
53 | {
54 | @Override
55 | public void onReceive(Context context, Intent intent)
56 | {
57 | if (isBroadcastReceived) return;
58 | isBroadcastReceived = true;
59 | boolean isServiceRunning = intent.getBooleanExtra("isRunning", false);
60 | Log.d("MainActivity", "BroadcastReceiver service isRunning: " + isServiceRunning);
61 | toggleButton.setChecked(isServiceRunning);
62 | if(isServiceRunning)
63 | {
64 | EasyLog.log("Easy163 VPN 正在运行");
65 | EasyLog.log("原作者: ndroi, 此版本由溯洄w4123修改");
66 | }else
67 | {
68 | EasyLog.log("Easy163 VPN 停止运行");
69 | }
70 | }
71 | };
72 |
73 | @Override
74 | protected void onCreate(Bundle savedInstanceState)
75 | {
76 | StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
77 | .detectLeakedClosableObjects()
78 | .penaltyLog()
79 | .build());
80 | super.onCreate(savedInstanceState);
81 | LocalBroadcastManager.getInstance(this).registerReceiver(serviceReceiver, new IntentFilter("service"));
82 | setContentView(R.layout.activity_main);
83 | Toolbar toolbar = findViewById(R.id.toolbar);
84 | setSupportActionBar(toolbar);
85 | DrawerLayout drawer = findViewById(R.id.drawer_layout);
86 | ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
87 | this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
88 | drawer.addDrawerListener(toggle);
89 | toggle.syncState();
90 | NavigationView navigationView = findViewById(R.id.nav_view);
91 | navigationView.setNavigationItemSelectedListener(this);
92 | toggleButton = findViewById(R.id.bt_start);
93 | toggleButton.setOnCheckedChangeListener(this);
94 | EasyLog.setTextView(findViewById(R.id.log));
95 | syncServiceState();
96 | intentActivityResultLauncher = registerForActivityResult(
97 | new ActivityResultContracts.StartActivityForResult(),
98 | result -> {
99 | if (result.getResultCode() == RESULT_OK)
100 | {
101 | Intent intent = new Intent(MainActivity.this, LocalVPNService.class);
102 | ContextCompat.startForegroundService(this, intent);
103 | }
104 | }
105 | );
106 | }
107 |
108 | @Override
109 | protected void onDestroy()
110 | {
111 | super.onDestroy();
112 | LocalBroadcastManager.getInstance(this).unregisterReceiver(serviceReceiver);
113 | }
114 |
115 | @Override
116 | public void onBackPressed()
117 | {
118 | DrawerLayout drawer = findViewById(R.id.drawer_layout);
119 | if (drawer.isDrawerOpen(GravityCompat.START))
120 | {
121 | drawer.closeDrawer(GravityCompat.START);
122 | } else
123 | {
124 | //super.onBackPressed();
125 | Intent intent = new Intent(Intent.ACTION_MAIN);
126 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
127 | intent.addCategory(Intent.CATEGORY_HOME);
128 | startActivity(intent);
129 | }
130 | }
131 |
132 | @Override
133 | public boolean onNavigationItemSelected(MenuItem item)
134 | {
135 | int id = item.getItemId();
136 | if (id == R.id.nav_github)
137 | {
138 | Uri uri = Uri.parse("https://github.com/ndroi/easy163");
139 | Intent intent = new Intent(Intent.ACTION_VIEW, uri);
140 | startActivity(intent);
141 | } else if (id == R.id.nav_usage)
142 | {
143 | Builder builder = new Builder(this);
144 | builder.setTitle("使用说明");
145 | builder.setMessage("开启本软件 VPN 服务后即可使用\n" +
146 | "如无法启动 VPN 尝试重启手机\n" +
147 | "出现异常问题尝试情况软件缓存\n" +
148 | "更多问题请查阅 Github");
149 | builder.setNegativeButton("取消", new DialogInterface.OnClickListener()
150 | {
151 | @Override
152 | public void onClick(DialogInterface dialog, int which)
153 | {
154 | dialog.dismiss();
155 | }
156 | });
157 | builder.show();
158 | } else if (id == R.id.nav_statement)
159 | {
160 | Builder builder = new Builder(this);
161 | builder.setTitle("免责声明");
162 | builder.setMessage("本软件为实验性项目\n" +
163 | "仅提供技术研究使用\n" +
164 | "本软件完全免费\n" +
165 | "作者不承担用户因软件造成的一切责任");
166 | builder.setNegativeButton("取消", new DialogInterface.OnClickListener()
167 | {
168 | @Override
169 | public void onClick(DialogInterface dialog, int which)
170 | {
171 | dialog.dismiss();
172 | }
173 | });
174 | builder.show();
175 | } else if (id == R.id.nav_clear_cache)
176 | {
177 | Cache.clear();
178 | Local.clear();
179 | Toast.makeText(this, "缓存已清除", Toast.LENGTH_SHORT).show();
180 | } else if (id == R.id.nav_about)
181 | {
182 | Builder builder = new Builder(this);
183 | builder.setTitle("关于");
184 | builder.setMessage("当前版本 " + BuildConfig.VERSION_NAME + "\n" +
185 | "版本更新关注 Github Release");
186 | builder.setNegativeButton("取消", new DialogInterface.OnClickListener()
187 | {
188 | @Override
189 | public void onClick(DialogInterface dialog, int which)
190 | {
191 | dialog.dismiss();
192 | }
193 | });
194 | builder.show();
195 | }
196 | DrawerLayout drawer = findViewById(R.id.drawer_layout);
197 | drawer.closeDrawer(GravityCompat.START);
198 | return true;
199 | }
200 |
201 | @Override
202 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
203 | {
204 | if (isChecked)
205 | {
206 | startVPN();
207 | } else
208 | {
209 | stopVPN();
210 | }
211 | }
212 |
213 | private void syncServiceState()
214 | {
215 | Intent intent = new Intent("control");
216 | intent.putExtra("cmd", "check");
217 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
218 | }
219 |
220 | private void startVPN()
221 | {
222 | Intent vpnIntent = VpnService.prepare(this);
223 | if (vpnIntent != null)
224 | intentActivityResultLauncher.launch(vpnIntent);
225 | else {
226 | Intent intent = new Intent(this, LocalVPNService.class);
227 | ContextCompat.startForegroundService(this, intent);
228 | }
229 | }
230 |
231 | private void stopVPN()
232 | {
233 | Intent intent = new Intent("control");
234 | intent.putExtra("cmd", "stop");
235 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
236 | Log.d("stopVPN", "try to stopVPN");
237 | }
238 |
239 | }
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/utils/ConcurrencyTask.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.utils;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | public class ConcurrencyTask
7 | {
8 | private List threads = new ArrayList<>();
9 |
10 | public void addTask(Thread thread)
11 | {
12 | threads.add(thread);
13 | thread.start();
14 | }
15 |
16 | public boolean isAllFinished()
17 | {
18 | for (Thread thread : threads)
19 | {
20 | if(thread.isAlive())
21 | {
22 | return false;
23 | }
24 | }
25 | return true;
26 | }
27 |
28 | public void waitAll()
29 | {
30 | try
31 | {
32 | for (Thread thread : threads)
33 | {
34 | thread.join();
35 | }
36 | } catch (InterruptedException e)
37 | {
38 | e.printStackTrace();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/utils/Crypto.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.utils;
2 |
3 | import com.alibaba.fastjson.JSONObject;
4 |
5 | import java.security.InvalidKeyException;
6 | import java.security.MessageDigest;
7 | import java.security.NoSuchAlgorithmException;
8 |
9 | import javax.crypto.BadPaddingException;
10 | import javax.crypto.Cipher;
11 | import javax.crypto.IllegalBlockSizeException;
12 | import javax.crypto.NoSuchPaddingException;
13 | import javax.crypto.spec.SecretKeySpec;
14 |
15 | /**
16 | * Created by andro on 2020/5/3.
17 | */
18 | public class Crypto
19 | {
20 | private static String aes_key = "e82ckenh8dichen8";
21 | private static SecretKeySpec key = new SecretKeySpec(aes_key.getBytes(), "AES");
22 | private static Cipher decryptCipher = null;
23 | private static Cipher encryptCipher = null;
24 |
25 | static
26 | {
27 | try
28 | {
29 | decryptCipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
30 | decryptCipher.init(Cipher.DECRYPT_MODE, key);
31 | encryptCipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
32 | encryptCipher.init(Cipher.ENCRYPT_MODE, key);
33 | } catch (NoSuchAlgorithmException e)
34 | {
35 | e.printStackTrace();
36 | } catch (NoSuchPaddingException e)
37 | {
38 | e.printStackTrace();
39 | } catch (InvalidKeyException e)
40 | {
41 | e.printStackTrace();
42 | }
43 | }
44 |
45 | public static byte[] aesDecrypt(byte[] bytes)
46 | {
47 | byte[] result = null;
48 | try
49 | {
50 | result = decryptCipher.doFinal(bytes);
51 | } catch (BadPaddingException e)
52 | {
53 | e.printStackTrace();
54 | } catch (IllegalBlockSizeException e)
55 | {
56 | e.printStackTrace();
57 | }
58 | return result;
59 | }
60 |
61 | public static byte[] aesEncrypt(byte[] bytes)
62 | {
63 | byte[] result = null;
64 | try
65 | {
66 | result = encryptCipher.doFinal(bytes);
67 | } catch (BadPaddingException e)
68 | {
69 | e.printStackTrace();
70 | } catch (IllegalBlockSizeException e)
71 | {
72 | e.printStackTrace();
73 | }
74 | return result;
75 | }
76 |
77 | private static byte[] hexStringToByteArray(String hexString)
78 | {
79 | int len = hexString.length();
80 | byte[] bytes = new byte[len / 2];
81 | for (int i = 0; i < len; i += 2)
82 | {
83 | bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) +
84 | Character.digit(hexString.charAt(i + 1), 16));
85 | }
86 | return bytes;
87 | }
88 |
89 | private static String ByteArrayToHexString(byte[] bytes)
90 | {
91 | String hexStr = "";
92 | for (int i = 0; i < bytes.length; i++)
93 | {
94 | String hex = Integer.toHexString(bytes[i] & 0xFF).toUpperCase();
95 | if (hex.length() == 1)
96 | {
97 | hex = "0" + hex;
98 | }
99 | hexStr += hex;
100 | }
101 | return hexStr;
102 | }
103 |
104 | public static class Request
105 | {
106 | public String path;
107 | public JSONObject json;
108 | }
109 |
110 | public static Request decryptRequestBody(String body)
111 | {
112 | Request request = new Request();
113 | byte[] encryptedBytes = hexStringToByteArray(body.substring(7));
114 | byte[] rawBytes = aesDecrypt(encryptedBytes);
115 | String text = new String(rawBytes);
116 | String[] parts = text.split("-36cd479b6b5-");
117 | request.path = parts[0];
118 | request.json = JSONObject.parseObject(parts[1]);
119 | return request;
120 | }
121 |
122 | public static String encryptRequestBody(Request request)
123 | {
124 | String jsonText = request.json.toString();
125 | String message = "nobody" + request.path + "use" + jsonText + "md5forencrypt";
126 | String digest = "";
127 | try
128 | {
129 | MessageDigest messageDigest = MessageDigest.getInstance("md5");
130 | messageDigest.update(message.getBytes());
131 | for (byte b : messageDigest.digest())
132 | {
133 | String temp = Integer.toHexString(b & 0xff);
134 | if (temp.length() == 1)
135 | {
136 | temp = "0" + temp;
137 | }
138 | digest += temp;
139 | }
140 | } catch (NoSuchAlgorithmException e)
141 | {
142 | e.printStackTrace();
143 | }
144 | String text = request.path + "-36cd479b6b5-" + jsonText + "-36cd479b6b5-" + digest;
145 | String body = ByteArrayToHexString(aesEncrypt(text.getBytes()));
146 | body = "params=" + body;
147 | return body;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/utils/EasyLog.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.utils;
2 |
3 | import android.text.method.ScrollingMovementMethod;
4 | import android.widget.TextView;
5 | import java.text.SimpleDateFormat;
6 | import java.util.Date;
7 | import java.util.Locale;
8 |
9 | public class EasyLog
10 | {
11 | private static Logger logger = null;
12 |
13 | public static void log(String info)
14 | {
15 | if(logger != null)
16 | {
17 | logger.log(info);
18 | }
19 | }
20 |
21 | public static void setTextView(TextView textView)
22 | {
23 | textView.setMovementMethod(ScrollingMovementMethod.getInstance());
24 | logger = new Logger(textView);
25 | }
26 |
27 | private static class Logger
28 | {
29 | private TextView textView;
30 | public Logger(TextView textView)
31 | {
32 | this.textView = textView;
33 | }
34 |
35 | private String genTime()
36 | {
37 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("[HH:mm:ss]", Locale.US);
38 | String time = simpleDateFormat.format(new Date());
39 | return time;
40 | }
41 |
42 | private void log(String info)
43 | {
44 | StringBuilder stringBuilder = new StringBuilder();
45 | stringBuilder.append(genTime() + " " + info + "\n");
46 | textView.post(() -> textView.append(stringBuilder));
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/utils/Keyword.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.utils;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | /**
7 | * Created by andro on 2020/5/5.
8 | */
9 | public class Keyword
10 | {
11 | public String id;
12 | public String songName;
13 | public List singers = new ArrayList<>();
14 | public String extra = null; // extra songName info in (xxx)
15 |
16 | public void applyRawSongName(String rawSongName)
17 | {
18 | int p = rawSongName.indexOf('(');
19 | if(p == -1)
20 | {
21 | p = rawSongName.indexOf('(');
22 | }
23 | if (p != -1)
24 | {
25 | songName = rawSongName.substring(0, p).trim();
26 | int q = rawSongName.indexOf(')', p);
27 | if(q == -1)
28 | {
29 | q = rawSongName.indexOf(')', p);
30 | }
31 | if(q != -1)
32 | {
33 | extra = rawSongName.substring(p + 1, q);
34 | }
35 | }else
36 | {
37 | songName = rawSongName.trim();
38 | extra = null;
39 | }
40 | }
41 |
42 | @Override
43 | public String toString()
44 | {
45 | String str = songName + "-";
46 | for (String singer : singers)
47 | {
48 | str += singer + '/';
49 | }
50 | str = str.substring(0, str.length() - 1);
51 | return str;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/utils/ReadStream.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.utils;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 |
7 | /**
8 | * Created by andro on 2020/5/5.
9 | */
10 | public class ReadStream
11 | {
12 | public static byte[] read(InputStream is)
13 | {
14 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
15 | byte[] buffer = new byte[4096];
16 | try
17 | {
18 | while (true)
19 | {
20 | int len = is.read(buffer);
21 | if (len == -1) break;
22 | outputStream.write(buffer, 0, len);
23 | }
24 | outputStream.close();
25 | } catch (IOException e)
26 | {
27 | e.printStackTrace();
28 | }
29 | return outputStream.toByteArray();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/utils/Song.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.utils;
2 |
3 | /**
4 | * Created by andro on 2020/5/5.
5 | */
6 | /* Song::url is enough for playing normally in PC,
7 | * but Android client play need a correct size.
8 | * for Android client download, md5 is must.
9 | * */
10 | public class Song
11 | {
12 | public String url = "unknown";
13 | public int size = 10 * 1000 * 1000;
14 | public int br = 192000;
15 | public String md5 = "unknown";
16 |
17 | @Override
18 | public String toString()
19 | {
20 | String info = "url: " + url + "\nsize: " + size + "\nbr: " + br + "\nmd5: " + md5 + "\n";
21 | return info;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/LocalVPNService.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn;
2 |
3 | import android.app.Notification;
4 | import android.app.NotificationChannel;
5 | import android.app.NotificationManager;
6 | import android.app.PendingIntent;
7 | import android.content.BroadcastReceiver;
8 | import android.content.ComponentName;
9 | import android.content.Context;
10 | import android.content.Intent;
11 | import android.content.IntentFilter;
12 | import android.content.pm.PackageManager;
13 | import android.net.VpnService;
14 | import android.os.Build;
15 | import android.os.ParcelFileDescriptor;
16 |
17 | import androidx.core.app.NotificationCompat;
18 | import androidx.localbroadcastmanager.content.LocalBroadcastManager;
19 | import android.service.quicksettings.TileService;
20 | import android.util.Log;
21 | import org.ndroi.easy163.R;
22 | import org.ndroi.easy163.core.Cache;
23 | import org.ndroi.easy163.core.Local;
24 | import org.ndroi.easy163.core.Server;
25 | import org.ndroi.easy163.ui.EasyTileService;
26 | import org.ndroi.easy163.ui.MainActivity;
27 | import org.ndroi.easy163.utils.EasyLog;
28 | import org.ndroi.easy163.vpn.bio.BioTcpHandler;
29 | import org.ndroi.easy163.vpn.bio.BioUdpHandler;
30 | import org.ndroi.easy163.vpn.tcpip.Packet;
31 | import org.ndroi.easy163.vpn.util.ByteBufferPool;
32 | import java.io.Closeable;
33 | import java.io.FileDescriptor;
34 | import java.io.FileInputStream;
35 | import java.io.FileOutputStream;
36 | import java.io.IOException;
37 | import java.nio.ByteBuffer;
38 | import java.nio.channels.FileChannel;
39 | import java.util.concurrent.ArrayBlockingQueue;
40 | import java.util.concurrent.BlockingQueue;
41 | import java.util.concurrent.ExecutorService;
42 | import java.util.concurrent.Executors;
43 |
44 | public class LocalVPNService extends VpnService
45 | {
46 | private static final String TAG = LocalVPNService.class.getSimpleName();
47 | private static final String VPN_ADDRESS = "10.0.0.2"; // Only IPv4 support for now
48 | private static final String VPN_ROUTE = "0.0.0.0"; // Intercept everything
49 | private ParcelFileDescriptor vpnInterface = null;
50 | private BlockingQueue deviceToNetworkUDPQueue;
51 | private BlockingQueue deviceToNetworkTCPQueue;
52 | private BlockingQueue networkToDeviceQueue;
53 | private ExecutorService executorService;
54 | private static Boolean isRunning = false;
55 | private static Context context = null;
56 |
57 | public static Context getContext()
58 | {
59 | return context;
60 | }
61 |
62 | public static Boolean getIsRunning()
63 | {
64 | return isRunning;
65 | }
66 |
67 | private BroadcastReceiver stopReceiver = new BroadcastReceiver()
68 | {
69 | @Override
70 | public void onReceive(Context context, Intent intent)
71 | {
72 | String cmd = intent.getStringExtra("cmd");
73 | if(cmd.equals("stop"))
74 | {
75 | executorService.shutdownNow();
76 | cleanup();
77 | LocalVPNService.this.stopSelf();
78 | }else if(cmd.equals("check"))
79 | {
80 | Log.i(TAG, "checkServiceState received");
81 | sendState();
82 | }
83 | }
84 | };
85 |
86 | @Override
87 | public void onCreate()
88 | {
89 | super.onCreate();
90 | context = getApplicationContext();
91 | setupVPN();
92 | LocalBroadcastManager.getInstance(this).registerReceiver(stopReceiver, new IntentFilter("control"));
93 | registerReceiver(stopReceiver, new IntentFilter("control"));
94 | deviceToNetworkUDPQueue = new ArrayBlockingQueue(1000);
95 | deviceToNetworkTCPQueue = new ArrayBlockingQueue(1000);
96 | networkToDeviceQueue = new ArrayBlockingQueue<>(1000);
97 | executorService = Executors.newFixedThreadPool(3);
98 | executorService.submit(new BioUdpHandler(deviceToNetworkUDPQueue, networkToDeviceQueue, this));
99 | executorService.submit(new BioTcpHandler(deviceToNetworkTCPQueue, networkToDeviceQueue, this));
100 | executorService.submit(new VPNRunnable(vpnInterface.getFileDescriptor(),
101 | deviceToNetworkUDPQueue, deviceToNetworkTCPQueue, networkToDeviceQueue));
102 | startNotification();
103 | Server.getInstance().start();
104 | Cache.init();
105 | Local.load();
106 | isRunning = true;
107 | sendState();
108 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
109 | TileService.requestListeningState(this, new ComponentName(this, EasyTileService.class));
110 | }
111 | Log.i(TAG, "Easy163 VPN 启动");
112 | }
113 |
114 | private void startNotification()
115 | {
116 | String notificationId = "easy163";
117 | String notificationName = "easy163";
118 | NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
119 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
120 | {
121 | NotificationChannel channel = new NotificationChannel(notificationId, notificationName, NotificationManager.IMPORTANCE_HIGH);
122 | notificationManager.createNotificationChannel(channel);
123 | }
124 | Intent intent = new Intent(this, MainActivity.class);
125 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
126 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 100, intent, PendingIntent.FLAG_IMMUTABLE);
127 | Intent stopIntent = new Intent("control");
128 | stopIntent.putExtra("cmd", "stop");
129 | PendingIntent stopPendingIntent = PendingIntent.getBroadcast(this, 101, stopIntent, PendingIntent.FLAG_IMMUTABLE);
130 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationId)
131 | .setContentIntent(pendingIntent)
132 | .setSmallIcon(R.mipmap.icon)
133 | .setContentTitle("Easy163")
134 | .setContentText("正在运行...")
135 | .addAction(R.mipmap.icon, "停止", stopPendingIntent);
136 | Notification notification = builder.build();
137 | startForeground(1, notification);
138 | }
139 |
140 | private void setupVPN()
141 | {
142 | try
143 | {
144 | if (vpnInterface == null)
145 | {
146 | Builder builder = new Builder();
147 | builder.addAddress(VPN_ADDRESS, 32);
148 | builder.addRoute(VPN_ROUTE, 0);
149 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
150 | {
151 | try
152 | {
153 | builder.addAllowedApplication("com.netease.cloudmusic");
154 | }catch (PackageManager.NameNotFoundException e)
155 | {
156 | Log.d(TAG, "未检测到网易云音乐");
157 | }
158 | try
159 | {
160 | builder.addAllowedApplication("com.netease.cloudmusic.lite");
161 | }catch (PackageManager.NameNotFoundException e)
162 | {
163 | Log.d(TAG, "未检测到网易云音乐极速版");
164 | }
165 | }
166 | vpnInterface = builder.setSession(getString(R.string.app_name)).establish();
167 | }
168 | } catch (Exception e)
169 | {
170 | Log.e(TAG, "Easy163 VPN 启动失败");
171 | EasyLog.log("Easy163 VPN 启动失败");
172 | System.exit(0);
173 | }
174 | }
175 |
176 | @Override
177 | public int onStartCommand(Intent intent, int flags, int startId)
178 | {
179 | return START_STICKY;
180 | }
181 |
182 | @Override
183 | public void onDestroy()
184 | {
185 | super.onDestroy();
186 | executorService.shutdownNow();
187 | cleanup();
188 | isRunning = false;
189 | sendState();
190 | LocalBroadcastManager.getInstance(this).unregisterReceiver(stopReceiver);
191 | unregisterReceiver(stopReceiver);
192 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
193 | TileService.requestListeningState(this, new ComponentName(this, EasyTileService.class));
194 | }
195 | Log.i(TAG, "Stopped");
196 | }
197 |
198 | private void cleanup()
199 | {
200 | deviceToNetworkTCPQueue = null;
201 | deviceToNetworkUDPQueue = null;
202 | networkToDeviceQueue = null;
203 | closeResources(vpnInterface);
204 | }
205 |
206 | private void sendState()
207 | {
208 | MainActivity.resetBroadcastReceivedState();
209 | Intent replyIntent= new Intent("service");
210 | replyIntent.putExtra("isRunning", isRunning);
211 | LocalBroadcastManager.getInstance(this).sendBroadcast(replyIntent);
212 | Log.i(TAG, "sendState");
213 | }
214 |
215 | private static void closeResources(Closeable... resources)
216 | {
217 | for (Closeable resource : resources)
218 | {
219 | try
220 | {
221 | resource.close();
222 | } catch (IOException e)
223 | {
224 | // Ignore
225 | }
226 | }
227 | }
228 |
229 | private static class VPNRunnable implements Runnable
230 | {
231 | private static final String TAG = VPNRunnable.class.getSimpleName();
232 |
233 | private FileDescriptor vpnFileDescriptor;
234 |
235 | private BlockingQueue deviceToNetworkUDPQueue;
236 | private BlockingQueue deviceToNetworkTCPQueue;
237 | private BlockingQueue networkToDeviceQueue;
238 |
239 | public VPNRunnable(FileDescriptor vpnFileDescriptor,
240 | BlockingQueue deviceToNetworkUDPQueue,
241 | BlockingQueue deviceToNetworkTCPQueue,
242 | BlockingQueue networkToDeviceQueue)
243 | {
244 | this.vpnFileDescriptor = vpnFileDescriptor;
245 | this.deviceToNetworkUDPQueue = deviceToNetworkUDPQueue;
246 | this.deviceToNetworkTCPQueue = deviceToNetworkTCPQueue;
247 | this.networkToDeviceQueue = networkToDeviceQueue;
248 | }
249 |
250 | static class WriteVpnThread implements Runnable
251 | {
252 | FileChannel vpnOutput;
253 | private BlockingQueue networkToDeviceQueue;
254 |
255 | WriteVpnThread(FileChannel vpnOutput, BlockingQueue networkToDeviceQueue)
256 | {
257 | this.vpnOutput = vpnOutput;
258 | this.networkToDeviceQueue = networkToDeviceQueue;
259 | }
260 |
261 | @Override
262 | public void run()
263 | {
264 | while (!Thread.interrupted())
265 | {
266 | try
267 | {
268 | ByteBuffer bufferFromNetwork = networkToDeviceQueue.take();
269 | bufferFromNetwork.flip();
270 | while (bufferFromNetwork.hasRemaining())
271 | {
272 | int w = vpnOutput.write(bufferFromNetwork);
273 | }
274 | } catch (InterruptedException e)
275 | {
276 | break;
277 | }
278 | catch (Exception e)
279 | {
280 | Log.i(TAG, "WriteVpnThread fail", e);
281 | }
282 | }
283 | }
284 | }
285 |
286 | @Override
287 | public void run()
288 | {
289 | FileChannel vpnInput = new FileInputStream(vpnFileDescriptor).getChannel();
290 | FileChannel vpnOutput = new FileOutputStream(vpnFileDescriptor).getChannel();
291 | Thread t = new Thread(new WriteVpnThread(vpnOutput, networkToDeviceQueue));
292 | t.start();
293 | try
294 | {
295 | while (!Thread.interrupted())
296 | {
297 | ByteBuffer bufferToNetwork = ByteBufferPool.acquire();
298 | int readBytes = vpnInput.read(bufferToNetwork);
299 | if (readBytes > 0)
300 | {
301 | bufferToNetwork.flip();
302 | Packet packet = new Packet(bufferToNetwork);
303 | if (packet.isUDP())
304 | {
305 | deviceToNetworkUDPQueue.offer(packet);
306 | } else if (packet.isTCP())
307 | {
308 | deviceToNetworkTCPQueue.offer(packet);
309 | } else
310 | {
311 | //Log.w(TAG, String.format("Unknown packet protocol type %d", packet.ip4Header.protocolNum));
312 | }
313 | } else
314 | {
315 | Thread.sleep(50);
316 | }
317 | }
318 | } catch (InterruptedException ignored)
319 | {
320 |
321 | }catch (IOException e)
322 | {
323 | Log.w(TAG, e.toString(), e);
324 | } finally
325 | {
326 | t.interrupt();
327 | closeResources(vpnInput, vpnOutput);
328 | }
329 | }
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/bio/BioUdpHandler.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.bio;
2 |
3 | import android.net.VpnService;
4 | import android.util.Log;
5 |
6 | import org.ndroi.easy163.vpn.config.Config;
7 | import org.ndroi.easy163.vpn.tcpip.IpUtil;
8 | import org.ndroi.easy163.vpn.tcpip.Packet;
9 | import org.ndroi.easy163.vpn.util.ByteBufferPool;
10 |
11 | import java.io.IOException;
12 | import java.net.InetAddress;
13 | import java.net.InetSocketAddress;
14 | import java.nio.ByteBuffer;
15 | import java.nio.channels.DatagramChannel;
16 | import java.nio.channels.SelectionKey;
17 | import java.nio.channels.Selector;
18 | import java.util.HashMap;
19 | import java.util.Iterator;
20 | import java.util.Map;
21 | import java.util.Set;
22 | import java.util.concurrent.ArrayBlockingQueue;
23 | import java.util.concurrent.BlockingQueue;
24 | import java.util.concurrent.atomic.AtomicInteger;
25 |
26 | /**
27 | *
28 | */
29 | public class BioUdpHandler implements Runnable
30 | {
31 |
32 | BlockingQueue queue;
33 |
34 | BlockingQueue networkToDeviceQueue;
35 | VpnService vpnService;
36 |
37 | private Selector selector;
38 | private static final int HEADER_SIZE = Packet.IP4_HEADER_SIZE + Packet.UDP_HEADER_SIZE;
39 |
40 | private static class UdpDownWorker implements Runnable
41 | {
42 | BlockingQueue networkToDeviceQueue;
43 | BlockingQueue tunnelQueue;
44 | Selector selector;
45 |
46 | private static AtomicInteger ipId = new AtomicInteger();
47 |
48 | private void sendUdpPack(UdpTunnel tunnel, byte[] data) throws IOException
49 | {
50 | int dataLen = 0;
51 | if (data != null)
52 | {
53 | dataLen = data.length;
54 | }
55 | Packet packet = IpUtil.buildUdpPacket(tunnel.remote, tunnel.local, ipId.addAndGet(1));
56 |
57 | ByteBuffer byteBuffer = ByteBufferPool.acquire();
58 | //
59 | byteBuffer.position(HEADER_SIZE);
60 | if (data != null)
61 | {
62 | if (byteBuffer.remaining() < data.length)
63 | {
64 | System.currentTimeMillis();
65 | }
66 | byteBuffer.put(data);
67 | }
68 | packet.updateUDPBuffer(byteBuffer, dataLen);
69 | byteBuffer.position(HEADER_SIZE + dataLen);
70 | this.networkToDeviceQueue.offer(byteBuffer);
71 | }
72 |
73 |
74 | public UdpDownWorker(Selector selector, BlockingQueue networkToDeviceQueue, BlockingQueue tunnelQueue)
75 | {
76 | this.networkToDeviceQueue = networkToDeviceQueue;
77 | this.tunnelQueue = tunnelQueue;
78 | this.selector = selector;
79 | }
80 |
81 | @Override
82 | public void run()
83 | {
84 | try
85 | {
86 | while (!Thread.interrupted())
87 | {
88 | int readyChannels = selector.select();
89 | while (true)
90 | {
91 | UdpTunnel tunnel = tunnelQueue.poll();
92 | if (tunnel == null)
93 | {
94 | break;
95 | } else
96 | {
97 | try
98 | {
99 | SelectionKey key = tunnel.channel.register(selector, SelectionKey.OP_READ, tunnel);
100 | key.interestOps(SelectionKey.OP_READ);
101 | boolean isvalid = key.isValid();
102 | } catch (IOException e)
103 | {
104 | Log.d(TAG, "register fail", e);
105 | }
106 | }
107 | }
108 | if (readyChannels == 0)
109 | {
110 | selector.selectedKeys().clear();
111 | continue;
112 | }
113 | Set keys = selector.selectedKeys();
114 | Iterator keyIterator = keys.iterator();
115 | while (keyIterator.hasNext())
116 | {
117 | SelectionKey key = keyIterator.next();
118 | keyIterator.remove();
119 | if (key.isValid() && key.isReadable())
120 | {
121 | try
122 | {
123 | DatagramChannel inputChannel = (DatagramChannel) key.channel();
124 |
125 | ByteBuffer receiveBuffer = ByteBufferPool.acquire();
126 | int readBytes = inputChannel.read(receiveBuffer);
127 | receiveBuffer.flip();
128 | byte[] data = new byte[receiveBuffer.remaining()];
129 | receiveBuffer.get(data);
130 | sendUdpPack((UdpTunnel) key.attachment(), data);
131 | } catch (IOException e)
132 | {
133 | Log.e(TAG, "error", e);
134 | }
135 |
136 | }
137 | }
138 | }
139 | }
140 | catch (Exception e)
141 | {
142 | Log.e(TAG, "error", e);
143 | System.exit(0);
144 | } finally
145 | {
146 | Log.d(TAG, "BioUdpHandler quit");
147 | }
148 |
149 |
150 | }
151 | }
152 |
153 | public BioUdpHandler(BlockingQueue queue, BlockingQueue networkToDeviceQueue, VpnService vpnService)
154 | {
155 | this.queue = queue;
156 | this.networkToDeviceQueue = networkToDeviceQueue;
157 | this.vpnService = vpnService;
158 | }
159 |
160 | private static final String TAG = BioUdpHandler.class.getSimpleName();
161 |
162 |
163 | Map udpSockets = new HashMap<>();
164 |
165 |
166 | private static class UdpTunnel
167 | {
168 | InetSocketAddress local;
169 | InetSocketAddress remote;
170 | DatagramChannel channel;
171 |
172 | }
173 |
174 | @Override
175 | public void run()
176 | {
177 | Thread t = null;
178 | try
179 | {
180 | BlockingQueue tunnelQueue = new ArrayBlockingQueue<>(100);
181 | selector = Selector.open();
182 | t = new Thread(new UdpDownWorker(selector, networkToDeviceQueue, tunnelQueue));
183 | t.start();
184 |
185 |
186 | while (!Thread.interrupted())
187 | {
188 | Packet packet = queue.take();
189 |
190 | InetAddress destinationAddress = packet.ip4Header.destinationAddress;
191 | Packet.UDPHeader header = packet.udpHeader;
192 |
193 | //Log.d(TAG, String.format("get pack %d udp %d ", packet.packId, header.length));
194 |
195 | int destinationPort = header.destinationPort;
196 | int sourcePort = header.sourcePort;
197 | String ipAndPort = destinationAddress.getHostAddress() + ":" + destinationPort + ":" + sourcePort;
198 | if (!udpSockets.containsKey(ipAndPort))
199 | {
200 | DatagramChannel outputChannel = DatagramChannel.open();
201 | vpnService.protect(outputChannel.socket());
202 | outputChannel.socket().bind(null);
203 | outputChannel.connect(new InetSocketAddress(destinationAddress, destinationPort));
204 |
205 | outputChannel.configureBlocking(false);
206 |
207 | UdpTunnel tunnel = new UdpTunnel();
208 | tunnel.local = new InetSocketAddress(packet.ip4Header.sourceAddress, header.sourcePort);
209 | tunnel.remote = new InetSocketAddress(packet.ip4Header.destinationAddress, header.destinationPort);
210 | tunnel.channel = outputChannel;
211 | tunnelQueue.offer(tunnel);
212 |
213 | selector.wakeup();
214 |
215 | udpSockets.put(ipAndPort, outputChannel);
216 | }
217 |
218 | DatagramChannel outputChannel = udpSockets.get(ipAndPort);
219 | ByteBuffer buffer = packet.backingBuffer;
220 | try
221 | {
222 | while (packet.backingBuffer.hasRemaining())
223 | {
224 | int w = outputChannel.write(buffer);
225 | if (Config.logRW)
226 | {
227 | //Log.d(TAG, String.format("write udp pack %d len %d %s ", packet.packId, w, ipAndPort));
228 | }
229 |
230 | }
231 | } catch (IOException e)
232 | {
233 | Log.e(TAG, "udp write error", e);
234 | outputChannel.close();
235 | udpSockets.remove(ipAndPort);
236 | }
237 | }
238 | } catch (InterruptedException ignored)
239 | {
240 |
241 | }
242 | catch (Exception e)
243 | {
244 | Log.e(TAG, "error", e);
245 | //System.exit(0);
246 | }
247 | finally
248 | {
249 | if (t != null)
250 | {
251 | t.interrupt();
252 | }
253 |
254 | for (DatagramChannel channel: udpSockets.values()) {
255 | try {
256 | channel.close();
257 | } catch(Exception ignored) {}
258 | }
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/bio/BioUtil.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.bio;
2 |
3 | import org.ndroi.easy163.vpn.config.Config;
4 |
5 | import java.io.IOException;
6 | import java.nio.ByteBuffer;
7 | import java.nio.channels.SocketChannel;
8 |
9 | public class BioUtil
10 | {
11 |
12 | private static final String TAG = BioUtil.class.getSimpleName();
13 |
14 | public static int write(SocketChannel channel, ByteBuffer byteBuffer) throws IOException
15 | {
16 | int len = channel.write(byteBuffer);
17 | //Log.i(TAG, String.format("write %d %s ", len, channel.toString()));
18 | return len;
19 | }
20 |
21 | public static int read(SocketChannel channel, ByteBuffer byteBuffer) throws IOException
22 | {
23 | int len = channel.read(byteBuffer);
24 | if (Config.logRW)
25 | {
26 | //Log.d(TAG, String.format("read %d %s ", len, channel.toString()));
27 | }
28 | return len;
29 | }
30 |
31 | public static String byteToString(byte[] data, int off, int len)
32 | {
33 | len = Math.min(128, len);
34 | StringBuilder sb = new StringBuilder();
35 | for (int i = off; i < off + len; i++)
36 | {
37 | sb.append(String.format("%02x ", data[i]));
38 | }
39 | return sb.toString();
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/block/BlockHttp.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.block;
2 |
3 | import android.util.Log;
4 |
5 | import org.ndroi.easy163.vpn.hookhttp.Request;
6 |
7 | import java.util.HashSet;
8 | import java.util.Set;
9 |
10 | public class BlockHttp
11 | {
12 | private static BlockHttp instance = new BlockHttp();
13 |
14 | public static BlockHttp getInstance()
15 | {
16 | return instance;
17 | }
18 |
19 | private Set hosts = new HashSet<>();
20 |
21 | public void addHost(String host)
22 | {
23 | hosts.add(host);
24 | }
25 |
26 | public boolean check(Request request)
27 | {
28 | String host = request.getHeaderFields().get("Host");
29 | if(hosts.contains(host))
30 | {
31 | Log.d("BlockHttp", host);
32 | return true;
33 | }
34 | return false;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/block/BlockHttps.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.block;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /* not used yet
7 | * TODO: HTTP-DNS
8 | * */
9 | public class BlockHttps
10 | {
11 | private static BlockHttps instance = new BlockHttps();
12 |
13 | public static BlockHttps getInstance()
14 | {
15 | return instance;
16 | }
17 |
18 | private Set hosts = new HashSet<>();
19 |
20 | public void addHost(String host)
21 | {
22 | hosts.add(host);
23 | }
24 |
25 | public boolean check(String ip)
26 | {
27 | return false;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/config/Config.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.config;
2 |
3 | public class Config
4 | {
5 | //io日志
6 | public static boolean logRW = false;
7 | //ack日志
8 | public static boolean logAck = false;
9 | //配置dns
10 | public static String dns = "114.114.114.114";
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/hookhttp/Hook.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.hookhttp;
2 |
3 | abstract public class Hook
4 | {
5 | public abstract boolean rule(Request request);
6 |
7 | public void hookRequest(Request request)
8 | {
9 |
10 | }
11 |
12 | public void hookResponse(Response response)
13 | {
14 |
15 | }
16 |
17 | protected String getPath(Request request)
18 | {
19 | String path = request.getUri();
20 | int p = path.indexOf("?");
21 | if (p != -1)
22 | {
23 | path = path.substring(0, p);
24 | }
25 | return path;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/hookhttp/HookHttp.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.hookhttp;
2 |
3 | import org.ndroi.easy163.vpn.bio.BioTcpHandler;
4 | import org.ndroi.easy163.vpn.block.BlockHttp;
5 |
6 | import java.nio.ByteBuffer;
7 | import java.util.ArrayList;
8 | import java.util.HashMap;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | public class HookHttp
13 | {
14 | private static HookHttp instance = new HookHttp();
15 |
16 | public static HookHttp getInstance()
17 | {
18 | return instance;
19 | }
20 |
21 | class Session
22 | {
23 | public BioTcpHandler.TcpTunnel tcpTunnel;
24 | public Request request = new Request();
25 | public Response response = new Response();
26 | public Hook hook = null;
27 |
28 | public Session(BioTcpHandler.TcpTunnel tcpTunnel)
29 | {
30 | this.tcpTunnel = tcpTunnel;
31 | }
32 | }
33 |
34 | private List hooks = new ArrayList<>();
35 | private Map sessions = new HashMap<>();
36 |
37 | private Hook findHook(Request request)
38 | {
39 | for (Hook hook : hooks)
40 | {
41 | if (hook.rule(request))
42 | {
43 | return hook;
44 | }
45 | }
46 | return null;
47 | }
48 |
49 | public void addHook(Hook hook)
50 | {
51 | hooks.add(hook);
52 | }
53 |
54 | public ByteBuffer checkAndHookRequest(BioTcpHandler.TcpTunnel tcpTunnel, ByteBuffer byteBuffer)
55 | {
56 | Session session = sessions.get(tcpTunnel);
57 | if (session == null)
58 | {
59 | session = new Session(tcpTunnel);
60 | sessions.put(tcpTunnel, session);
61 | }
62 | byte[] bytes = new byte[byteBuffer.remaining()];
63 | byteBuffer.get(bytes);
64 | session.request.putBytes(bytes);
65 | if (session.request.finished())
66 | {
67 | if (BlockHttp.getInstance().check(session.request))
68 | {
69 | sessions.remove(session.tcpTunnel);
70 | return byteBuffer;
71 | }
72 | session.hook = findHook(session.request);
73 | if (session.hook != null)
74 | {
75 | session.hook.hookRequest(session.request);
76 | } else
77 | {
78 | sessions.remove(session.tcpTunnel);
79 | }
80 | byte[] requestBytes = session.request.dump();
81 | byteBuffer = ByteBuffer.wrap(requestBytes);
82 | }
83 | return byteBuffer;
84 | }
85 |
86 | public ByteBuffer checkAndHookResponse(BioTcpHandler.TcpTunnel tcpTunnel, ByteBuffer byteBuffer)
87 | {
88 | Session session = sessions.get(tcpTunnel);
89 | if (session == null)
90 | {
91 | return byteBuffer;
92 | }
93 | byte[] bytes = new byte[byteBuffer.remaining()];
94 | byteBuffer.get(bytes);
95 | session.response.putBytes(bytes);
96 | if (session.response.finished())
97 | {
98 | session.hook.hookResponse(session.response);
99 | byte[] responseBytes = session.response.dump();
100 | sessions.remove(session.tcpTunnel);
101 | byteBuffer = ByteBuffer.wrap(responseBytes);
102 | }
103 | return byteBuffer;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/hookhttp/Request.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.hookhttp;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.util.LinkedHashMap;
6 | import java.util.Map;
7 |
8 | /**
9 | * Created by andro on 2020/5/25.
10 | */
11 | public class Request
12 | {
13 | private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
14 | private Map headerFields = new LinkedHashMap<>();
15 | private int headerLen = 0;
16 | private String method;
17 | private String uri;
18 | private String version;
19 | private byte[] content = null;
20 |
21 | public Map getHeaderFields()
22 | {
23 | return headerFields;
24 | }
25 |
26 | public void putBytes(byte[] bytes)
27 | {
28 | putBytes(bytes, 0, bytes.length);
29 | }
30 |
31 | public void putBytes(byte[] bytes, int offset, int length)
32 | {
33 | if (finished()) return;
34 | byteArrayOutputStream.write(bytes, offset, length);
35 | if (!headerReceived())
36 | {
37 | tryDecode();
38 | }
39 | if (finished() && content != null)
40 | {
41 | byte[] data = byteArrayOutputStream.toByteArray();
42 | System.arraycopy(data, headerLen, content, 0, content.length);
43 | }
44 | }
45 |
46 | public boolean finished()
47 | {
48 | if (headerLen == 0)
49 | {
50 | return false;
51 | }
52 | int contentLen = (content == null ? 0 : content.length);
53 | return byteArrayOutputStream.size() >= headerLen + contentLen;
54 | }
55 |
56 | public byte[] getContent()
57 | {
58 | return content;
59 | }
60 |
61 | public void setContent(byte[] content)
62 | {
63 | headerFields.put("Content-Length", content.length + "");
64 | this.content = content;
65 | }
66 |
67 | private boolean headerReceived()
68 | {
69 | return headerLen != 0;
70 | }
71 |
72 | public String getMethod()
73 | {
74 | return method;
75 | }
76 |
77 | public void setMethod(String method)
78 | {
79 | this.method = method;
80 | }
81 |
82 | public void setUri(String uri)
83 | {
84 | this.uri = uri;
85 | }
86 |
87 | public void setVersion(String version)
88 | {
89 | this.version = version;
90 | }
91 |
92 | public String getUri()
93 | {
94 | return uri;
95 | }
96 |
97 | public String getVersion()
98 | {
99 | return version;
100 | }
101 |
102 | private void tryDecode()
103 | {
104 | int crlf = checkCRLF();
105 | if (crlf != -1)
106 | {
107 | headerLen = crlf + 4;
108 | decode();
109 | if (method.equals("POST"))
110 | {
111 | int contentLen = Integer.parseInt(headerFields.get("Content-Length"));
112 | content = new byte[contentLen];
113 | }
114 | }
115 | }
116 |
117 | private int checkCRLF()
118 | {
119 | byte[] bytes = byteArrayOutputStream.toByteArray();
120 | for (int i = 0; i < bytes.length - 3; i++)
121 | {
122 | if (bytes[i] == 13 && bytes[i + 1] == 10 &&
123 | bytes[i + 2] == 13 && bytes[i + 3] == 10)
124 | {
125 | return i;
126 | }
127 | }
128 | return -1;
129 | }
130 |
131 | private void decode()
132 | {
133 | byte[] bytes = byteArrayOutputStream.toByteArray();
134 | String headerStr = new String(bytes, 0, headerLen - 4);
135 | String[] lines = headerStr.split("\r\n");
136 | String requestLine = lines[0];
137 | int _p = requestLine.indexOf(' ');
138 | method = requestLine.substring(0, _p);
139 | requestLine = requestLine.substring(_p + 1);
140 | _p = requestLine.indexOf(' ');
141 | uri = requestLine.substring(0, _p);
142 | version = requestLine.substring(_p + 1);
143 | for (int i = 1; i < lines.length; i++)
144 | {
145 | String line = lines[i];
146 | _p = line.indexOf(':');
147 | String key = line.substring(0, _p).trim();
148 | String value = line.substring(_p + 1).trim();
149 | headerFields.put(key, value);
150 | }
151 | }
152 |
153 | private void encode()
154 | {
155 | StringBuffer stringBuffer = new StringBuffer();
156 | stringBuffer.append(method + " " + uri + " " + version + "\r\n");
157 | for (String key : headerFields.keySet())
158 | {
159 | String value = headerFields.get(key);
160 | stringBuffer.append(key + ": " + value + "\r\n");
161 | }
162 | stringBuffer.append("\r\n");
163 | try
164 | {
165 | byteArrayOutputStream.reset();
166 | byteArrayOutputStream.write(stringBuffer.toString().getBytes());
167 | } catch (IOException e)
168 | {
169 | e.printStackTrace();
170 | }
171 | }
172 |
173 | public byte[] dump()
174 | {
175 | encode();
176 | if (content != null)
177 | {
178 | try
179 | {
180 | byteArrayOutputStream.write(content);
181 | byteArrayOutputStream.close();
182 | } catch (IOException e)
183 | {
184 | e.printStackTrace();
185 | }
186 | }
187 | return byteArrayOutputStream.toByteArray();
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/hookhttp/Response.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.hookhttp;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.ByteArrayOutputStream;
5 | import java.io.IOException;
6 | import java.util.ArrayList;
7 | import java.util.LinkedHashMap;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.zip.GZIPInputStream;
11 |
12 | public class Response
13 | {
14 | private static class Chunks
15 | {
16 | private static class Chunk
17 | {
18 | private byte[] data;
19 |
20 | public Chunk(int size)
21 | {
22 | data = new byte[size];
23 | }
24 |
25 | public void setData(byte[] bytes)
26 | {
27 | setData(bytes, 0, bytes.length);
28 | }
29 |
30 | public void setData(byte[] bytes, int offset, int length)
31 | {
32 | System.arraycopy(bytes, offset, data, 0, length);
33 | }
34 |
35 | public byte[] getData()
36 | {
37 | return data;
38 | }
39 |
40 | public int getSize()
41 | {
42 | return data.length;
43 | }
44 | }
45 |
46 | private enum Status
47 | {
48 | RECV_LENGTH,
49 | RECV_CONTENT,
50 | RECV_OVER,
51 | }
52 |
53 | private ByteArrayOutputStream remainingStream = new ByteArrayOutputStream();
54 | private List chunks = new ArrayList<>();
55 | private Status status = Status.RECV_LENGTH;
56 |
57 | private Chunk getCurrent()
58 | {
59 | if (chunks.isEmpty())
60 | {
61 | return null;
62 | }
63 | return chunks.get(chunks.size() - 1);
64 | }
65 |
66 | private int checkCRLF(byte[] bytes)
67 | {
68 | for (int i = 0; i < bytes.length - 3; i++)
69 | {
70 | if (bytes[i] == 13 && bytes[i + 1] == 10)
71 | {
72 | return i;
73 | }
74 | }
75 | return -1;
76 | }
77 |
78 | private void addNew(int length)
79 | {
80 | chunks.add(new Chunk(length));
81 | }
82 |
83 | public void putBytes(byte[] bytes)
84 | {
85 | remainingStream.write(bytes, 0, bytes.length);
86 | while (true)
87 | {
88 | if (status == Status.RECV_LENGTH)
89 | {
90 | bytes = remainingStream.toByteArray();
91 | int crlf = checkCRLF(bytes);
92 | if (crlf != -1)
93 | {
94 | int length = Integer.parseInt(new String(bytes, 0, crlf), 16);
95 | addNew(length);
96 | remainingStream.reset();
97 | int offset = crlf + 2;
98 | remainingStream.write(bytes, offset, bytes.length - offset);
99 | status = Status.RECV_CONTENT;
100 | } else
101 | {
102 | break;
103 | }
104 | }
105 | if (status == Status.RECV_CONTENT)
106 | {
107 | Chunk curChunk = getCurrent();
108 | int chunkSize = curChunk.getSize();
109 | if (remainingStream.size() >= chunkSize + 2)
110 | {
111 | bytes = remainingStream.toByteArray();
112 | curChunk.setData(bytes, 0, chunkSize);
113 | remainingStream.reset();
114 | int offset = chunkSize + 2;
115 | remainingStream.write(bytes, offset, bytes.length - offset);
116 | if (chunkSize == 0)
117 | {
118 | status = Status.RECV_OVER;
119 | break;
120 | } else
121 | {
122 | status = Status.RECV_LENGTH;
123 | }
124 | } else
125 | {
126 | break;
127 | }
128 | }
129 | }
130 | }
131 |
132 | public boolean finished()
133 | {
134 | return status == Status.RECV_OVER;
135 | }
136 |
137 | public byte[] dump()
138 | {
139 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
140 | for (Chunk chunk : chunks)
141 | {
142 | byteArrayOutputStream.write(chunk.getData(), 0, chunk.getSize());
143 | }
144 | return byteArrayOutputStream.toByteArray();
145 | }
146 | }
147 |
148 | private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
149 | private Map headerFields = new LinkedHashMap<>();
150 | private int headerLen = 0;
151 | private String version;
152 | private String code;
153 | private String desc;
154 | private byte[] content = null;
155 | private Chunks chunks = null;
156 |
157 | private boolean headerReceived()
158 | {
159 | return headerLen != 0;
160 | }
161 |
162 | public boolean finished()
163 | {
164 | if (headerLen == 0)
165 | {
166 | return false;
167 | }
168 | if (chunks == null)
169 | {
170 | int contentLen = (content == null ? 0 : content.length);
171 | return byteArrayOutputStream.size() >= contentLen;
172 | }
173 | return chunks.finished();
174 | }
175 |
176 | public void putBytes(byte[] bytes)
177 | {
178 | putBytes(bytes, 0, bytes.length);
179 | }
180 |
181 | public Map getHeaderFields()
182 | {
183 | return headerFields;
184 | }
185 |
186 | public String getVersion()
187 | {
188 | return version;
189 | }
190 |
191 | public void setVersion(String version)
192 | {
193 | this.version = version;
194 | }
195 |
196 | public String getCode()
197 | {
198 | return code;
199 | }
200 |
201 | public void setCode(String code)
202 | {
203 | this.code = code;
204 | }
205 |
206 | public String getDesc()
207 | {
208 | return desc;
209 | }
210 |
211 | public void setDesc(String desc)
212 | {
213 | this.desc = desc;
214 | }
215 |
216 | public byte[] getContent()
217 | {
218 | return content;
219 | }
220 |
221 | public void setContent(byte[] content)
222 | {
223 | this.content = content;
224 | headerFields.put("Content-Length", "" + content.length);
225 | }
226 |
227 | public void putBytes(byte[] bytes, int offset, int length)
228 | {
229 | if (finished()) return;
230 | byteArrayOutputStream.write(bytes, offset, length);
231 | if (!headerReceived())
232 | {
233 | tryDecode();
234 | if (headerReceived())
235 | {
236 | bytes = byteArrayOutputStream.toByteArray();
237 | byteArrayOutputStream.reset();
238 | byteArrayOutputStream.write(bytes, headerLen, bytes.length - headerLen);
239 | bytes = byteArrayOutputStream.toByteArray();
240 | }
241 | }
242 | if (chunks != null && !chunks.finished())
243 | {
244 | chunks.putBytes(bytes);
245 | }
246 | if (finished())
247 | {
248 | if (content != null)
249 | {
250 | content = byteArrayOutputStream.toByteArray();
251 | }
252 | if (chunks != null)
253 | {
254 | content = chunks.dump();
255 | headerFields.remove("Transfer-Encoding");
256 | if (headerFields.containsKey("Content-Encoding"))
257 | {
258 | content = unzip(content);
259 | headerFields.remove("Content-Encoding");
260 | }
261 | headerFields.put("Content-Length", "" + content.length);
262 | }
263 | }
264 | }
265 |
266 | private byte[] unzip(byte[] bytes)
267 | {
268 | byte[] result = null;
269 | try
270 | {
271 | GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(bytes));
272 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
273 | byte[] buffer = new byte[1024];
274 | while (true)
275 | {
276 | int len = gzipInputStream.read(buffer);
277 | if (len == -1) break;
278 | byteArrayOutputStream.write(buffer, 0, len);
279 | }
280 | gzipInputStream.close();
281 | byteArrayOutputStream.close();
282 | result = byteArrayOutputStream.toByteArray();
283 | } catch (IOException e)
284 | {
285 | e.printStackTrace();
286 | }
287 | return result;
288 | }
289 |
290 |
291 | private void tryDecode()
292 | {
293 | int crlf = checkCRLF();
294 | if (crlf != -1)
295 | {
296 | headerLen = crlf + 4;
297 | decode();
298 | if (headerFields.containsKey("Content-Length"))
299 | {
300 | int contentLen = Integer.parseInt(headerFields.get("Content-Length"));
301 | content = new byte[contentLen];
302 | } else if (headerFields.containsKey("Transfer-Encoding"))
303 | {
304 | chunks = new Chunks();
305 | }
306 | }
307 | }
308 |
309 | private int checkCRLF()
310 | {
311 | byte[] bytes = byteArrayOutputStream.toByteArray();
312 | for (int i = 0; i < bytes.length - 3; i++)
313 | {
314 | if (bytes[i] == 13 && bytes[i + 1] == 10 &&
315 | bytes[i + 2] == 13 && bytes[i + 3] == 10)
316 | {
317 | return i;
318 | }
319 | }
320 | return -1;
321 | }
322 |
323 | private void decode()
324 | {
325 | byte[] bytes = byteArrayOutputStream.toByteArray();
326 | String headerStr = new String(bytes, 0, headerLen - 4);
327 | String[] lines = headerStr.split("\r\n");
328 | String requestLine = lines[0];
329 | int _p = requestLine.indexOf(' ');
330 | version = requestLine.substring(0, _p);
331 | requestLine = requestLine.substring(_p + 1);
332 | _p = requestLine.indexOf(' ');
333 | code = requestLine.substring(0, _p);
334 | desc = requestLine.substring(_p + 1);
335 | for (int i = 1; i < lines.length; i++)
336 | {
337 | String line = lines[i];
338 | _p = line.indexOf(':');
339 | String key = line.substring(0, _p).trim();
340 | String value = line.substring(_p + 1).trim();
341 | headerFields.put(key, value);
342 | }
343 | }
344 |
345 | private void encode()
346 | {
347 | StringBuffer stringBuffer = new StringBuffer();
348 | stringBuffer.append(version + " " + code + " " + desc + "\r\n");
349 | for (String key : headerFields.keySet())
350 | {
351 | String value = headerFields.get(key);
352 | stringBuffer.append(key + ": " + value + "\r\n");
353 | }
354 | stringBuffer.append("\r\n");
355 | try
356 | {
357 | byteArrayOutputStream.reset();
358 | byteArrayOutputStream.write(stringBuffer.toString().getBytes());
359 | } catch (IOException e)
360 | {
361 | e.printStackTrace();
362 | }
363 | }
364 |
365 | public byte[] dump()
366 | {
367 | encode();
368 | if (content != null)
369 | {
370 | try
371 | {
372 | byteArrayOutputStream.write(content);
373 | byteArrayOutputStream.close();
374 | } catch (IOException e)
375 | {
376 | e.printStackTrace();
377 | }
378 | }
379 | return byteArrayOutputStream.toByteArray();
380 | }
381 | }
382 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/tcpip/IpUtil.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.tcpip;
2 |
3 | import org.ndroi.easy163.vpn.util.ByteBufferPool;
4 |
5 | import java.net.InetSocketAddress;
6 | import java.nio.ByteBuffer;
7 |
8 | public class IpUtil
9 | {
10 | public static Packet buildUdpPacket(InetSocketAddress source, InetSocketAddress dest, int ipId)
11 | {
12 | Packet packet = new Packet();
13 | packet.isTCP = false;
14 | packet.isUDP = true;
15 | Packet.IP4Header ip4Header = new Packet.IP4Header();
16 | ip4Header.version = 4;
17 | ip4Header.IHL = 5;
18 | ip4Header.destinationAddress = dest.getAddress();
19 | ip4Header.headerChecksum = 0;
20 | ip4Header.headerLength = 20;
21 |
22 | //int ipId=0;
23 | int ipFlag = 0x40;
24 | int ipOff = 0;
25 |
26 | ip4Header.identificationAndFlagsAndFragmentOffset = ipId << 16 | ipFlag << 8 | ipOff;
27 |
28 | ip4Header.optionsAndPadding = 0;
29 | ip4Header.protocol = Packet.IP4Header.TransportProtocol.UDP;
30 | ip4Header.protocolNum = 17;
31 | ip4Header.sourceAddress = source.getAddress();
32 | ip4Header.totalLength = 60;
33 | ip4Header.typeOfService = 0;
34 | ip4Header.TTL = 64;
35 |
36 | Packet.UDPHeader udpHeader = new Packet.UDPHeader();
37 | udpHeader.sourcePort = source.getPort();
38 | udpHeader.destinationPort = dest.getPort();
39 | udpHeader.length = 0;
40 |
41 | ByteBuffer byteBuffer = ByteBufferPool.acquire();
42 | byteBuffer.flip();
43 |
44 | packet.ip4Header = ip4Header;
45 | packet.udpHeader = udpHeader;
46 | packet.backingBuffer = byteBuffer;
47 | return packet;
48 | }
49 |
50 | public static Packet buildTcpPacket(InetSocketAddress source, InetSocketAddress dest, byte flag, long ack, long seq, int ipId)
51 | {
52 | Packet packet = new Packet();
53 | packet.isTCP = true;
54 | packet.isUDP = false;
55 | Packet.IP4Header ip4Header = new Packet.IP4Header();
56 | ip4Header.version = 4;
57 | ip4Header.IHL = 5;
58 | ip4Header.destinationAddress = dest.getAddress();
59 | ip4Header.headerChecksum = 0;
60 | ip4Header.headerLength = 20;
61 |
62 | //int ipId=0;
63 | int ipFlag = 0x40;
64 | int ipOff = 0;
65 |
66 | ip4Header.identificationAndFlagsAndFragmentOffset = ipId << 16 | ipFlag << 8 | ipOff;
67 |
68 | ip4Header.optionsAndPadding = 0;
69 | ip4Header.protocol = Packet.IP4Header.TransportProtocol.TCP;
70 | ip4Header.protocolNum = 6;
71 | ip4Header.sourceAddress = source.getAddress();
72 | ip4Header.totalLength = 60;
73 | ip4Header.typeOfService = 0;
74 | ip4Header.TTL = 64;
75 |
76 | Packet.TCPHeader tcpHeader = new Packet.TCPHeader();
77 | tcpHeader.acknowledgementNumber = ack;
78 | tcpHeader.checksum = 0;
79 | tcpHeader.dataOffsetAndReserved = -96;
80 | tcpHeader.destinationPort = dest.getPort();
81 | tcpHeader.flags = flag;
82 | tcpHeader.headerLength = 40;
83 | tcpHeader.optionsAndPadding = null;
84 | tcpHeader.sequenceNumber = seq;
85 | tcpHeader.sourcePort = source.getPort();
86 | tcpHeader.urgentPointer = 0;
87 | tcpHeader.window = 65535;
88 |
89 | ByteBuffer byteBuffer = ByteBufferPool.acquire();
90 | byteBuffer.flip();
91 |
92 | packet.ip4Header = ip4Header;
93 | packet.tcpHeader = tcpHeader;
94 | packet.backingBuffer = byteBuffer;
95 | return packet;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/tcpip/Packet.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.tcpip;
2 |
3 |
4 | import java.net.InetAddress;
5 | import java.net.UnknownHostException;
6 | import java.nio.ByteBuffer;
7 | import java.util.concurrent.atomic.AtomicInteger;
8 |
9 | /**
10 | * Representation of an IP Packet
11 | */
12 | // TODO: Reduce public mutability
13 | public class Packet
14 | {
15 | public static final int IP4_HEADER_SIZE = 20;
16 | public static final int TCP_HEADER_SIZE = 20;
17 | public static final int UDP_HEADER_SIZE = 8;
18 |
19 |
20 | private static AtomicInteger globalPackId = new AtomicInteger();
21 | public int packId = globalPackId.addAndGet(1);
22 | public IP4Header ip4Header;
23 | public TCPHeader tcpHeader;
24 | public UDPHeader udpHeader;
25 | public ByteBuffer backingBuffer;
26 |
27 | public boolean isTCP;
28 | public boolean isUDP;
29 |
30 | public Packet()
31 | {
32 |
33 | }
34 |
35 | public Packet(ByteBuffer buffer) throws UnknownHostException
36 | {
37 | this.ip4Header = new IP4Header(buffer);
38 | if (this.ip4Header.protocol == IP4Header.TransportProtocol.TCP)
39 | {
40 | this.tcpHeader = new TCPHeader(buffer);
41 | this.isTCP = true;
42 | } else if (ip4Header.protocol == IP4Header.TransportProtocol.UDP)
43 | {
44 | this.udpHeader = new UDPHeader(buffer);
45 | this.isUDP = true;
46 | }
47 | this.backingBuffer = buffer;
48 | }
49 |
50 | @Override
51 | public String toString()
52 | {
53 | final StringBuilder sb = new StringBuilder("Packet{");
54 | sb.append("ip4Header=").append(ip4Header);
55 | if (isTCP) sb.append(", tcpHeader=").append(tcpHeader);
56 | else if (isUDP) sb.append(", udpHeader=").append(udpHeader);
57 | sb.append(", payloadSize=").append(backingBuffer.limit() - backingBuffer.position());
58 | sb.append('}');
59 | return sb.toString();
60 | }
61 |
62 | public boolean isTCP()
63 | {
64 | return isTCP;
65 | }
66 |
67 | public boolean isUDP()
68 | {
69 | return isUDP;
70 | }
71 |
72 |
73 | public void updateTCPBuffer(ByteBuffer buffer, byte flags, long sequenceNum, long ackNum, int payloadSize)
74 | {
75 | buffer.position(0);
76 | fillHeader(buffer);
77 | backingBuffer = buffer;
78 |
79 | tcpHeader.flags = flags;
80 | backingBuffer.put(IP4_HEADER_SIZE + 13, flags);
81 |
82 | tcpHeader.sequenceNumber = sequenceNum;
83 | backingBuffer.putInt(IP4_HEADER_SIZE + 4, (int) sequenceNum);
84 |
85 | tcpHeader.acknowledgementNumber = ackNum;
86 | backingBuffer.putInt(IP4_HEADER_SIZE + 8, (int) ackNum);
87 |
88 | // Reset header size, since we don't need options
89 | byte dataOffset = (byte) (TCP_HEADER_SIZE << 2);
90 | tcpHeader.dataOffsetAndReserved = dataOffset;
91 | backingBuffer.put(IP4_HEADER_SIZE + 12, dataOffset);
92 |
93 | updateTCPChecksum(payloadSize);
94 |
95 | int ip4TotalLength = IP4_HEADER_SIZE + TCP_HEADER_SIZE + payloadSize;
96 | backingBuffer.putShort(2, (short) ip4TotalLength);
97 | ip4Header.totalLength = ip4TotalLength;
98 |
99 | updateIP4Checksum();
100 | }
101 |
102 | public void updateUDPBuffer(ByteBuffer buffer, int payloadSize)
103 | {
104 | buffer.position(0);
105 | fillHeader(buffer);
106 | backingBuffer = buffer;
107 |
108 | int udpTotalLength = UDP_HEADER_SIZE + payloadSize;
109 | backingBuffer.putShort(IP4_HEADER_SIZE + 4, (short) udpTotalLength);
110 | udpHeader.length = udpTotalLength;
111 |
112 | // Disable UDP checksum validation
113 | backingBuffer.putShort(IP4_HEADER_SIZE + 6, (short) 0);
114 | udpHeader.checksum = 0;
115 |
116 | int ip4TotalLength = IP4_HEADER_SIZE + udpTotalLength;
117 | backingBuffer.putShort(2, (short) ip4TotalLength);
118 | ip4Header.totalLength = ip4TotalLength;
119 |
120 | updateIP4Checksum();
121 | }
122 |
123 | private void updateIP4Checksum()
124 | {
125 | ByteBuffer buffer = backingBuffer.duplicate();
126 | buffer.position(0);
127 |
128 | // Clear previous checksum
129 | buffer.putShort(10, (short) 0);
130 |
131 | int ipLength = ip4Header.headerLength;
132 | int sum = 0;
133 | while (ipLength > 0)
134 | {
135 | sum += BitUtils.getUnsignedShort(buffer.getShort());
136 | ipLength -= 2;
137 | }
138 | while (sum >> 16 > 0)
139 | sum = (sum & 0xFFFF) + (sum >> 16);
140 |
141 | sum = ~sum;
142 | ip4Header.headerChecksum = sum;
143 | backingBuffer.putShort(10, (short) sum);
144 | }
145 |
146 | private void updateTCPChecksum(int payloadSize)
147 | {
148 | int sum = 0;
149 | int tcpLength = TCP_HEADER_SIZE + payloadSize;
150 |
151 | // Calculate pseudo-header checksum
152 | ByteBuffer buffer = ByteBuffer.wrap(ip4Header.sourceAddress.getAddress());
153 | sum = BitUtils.getUnsignedShort(buffer.getShort()) + BitUtils.getUnsignedShort(buffer.getShort());
154 |
155 | buffer = ByteBuffer.wrap(ip4Header.destinationAddress.getAddress());
156 | sum += BitUtils.getUnsignedShort(buffer.getShort()) + BitUtils.getUnsignedShort(buffer.getShort());
157 |
158 | sum += IP4Header.TransportProtocol.TCP.getNumber() + tcpLength;
159 |
160 | buffer = backingBuffer.duplicate();
161 | // Clear previous checksum
162 | buffer.putShort(IP4_HEADER_SIZE + 16, (short) 0);
163 |
164 | // Calculate TCP segment checksum
165 | buffer.position(IP4_HEADER_SIZE);
166 | while (tcpLength > 1)
167 | {
168 | sum += BitUtils.getUnsignedShort(buffer.getShort());
169 | tcpLength -= 2;
170 | }
171 | if (tcpLength > 0)
172 | sum += BitUtils.getUnsignedByte(buffer.get()) << 8;
173 |
174 | while (sum >> 16 > 0)
175 | sum = (sum & 0xFFFF) + (sum >> 16);
176 |
177 | sum = ~sum;
178 | tcpHeader.checksum = sum;
179 | backingBuffer.putShort(IP4_HEADER_SIZE + 16, (short) sum);
180 | }
181 |
182 | private void fillHeader(ByteBuffer buffer)
183 | {
184 | ip4Header.fillHeader(buffer);
185 | if (isUDP)
186 | udpHeader.fillHeader(buffer);
187 | else if (isTCP)
188 | tcpHeader.fillHeader(buffer);
189 | }
190 |
191 | public static class IP4Header
192 | {
193 | public byte version;
194 | public byte IHL;
195 | public int headerLength;
196 | public short typeOfService;
197 | public int totalLength;
198 |
199 | public int identificationAndFlagsAndFragmentOffset;
200 |
201 | public short TTL;
202 | public short protocolNum;
203 | public TransportProtocol protocol;
204 | public int headerChecksum;
205 |
206 | public InetAddress sourceAddress;
207 | public InetAddress destinationAddress;
208 |
209 | public int optionsAndPadding;
210 |
211 | public enum TransportProtocol
212 | {
213 | TCP(6),
214 | UDP(17),
215 | Other(0xFF);
216 |
217 | private int protocolNumber;
218 |
219 | TransportProtocol(int protocolNumber)
220 | {
221 | this.protocolNumber = protocolNumber;
222 | }
223 |
224 | private static TransportProtocol numberToEnum(int protocolNumber)
225 | {
226 | if (protocolNumber == 6)
227 | return TCP;
228 | else if (protocolNumber == 17)
229 | return UDP;
230 | else
231 | return Other;
232 | }
233 |
234 | public int getNumber()
235 | {
236 | return this.protocolNumber;
237 | }
238 | }
239 |
240 | public IP4Header()
241 | {
242 |
243 | }
244 |
245 | private IP4Header(ByteBuffer buffer) throws UnknownHostException
246 | {
247 | byte versionAndIHL = buffer.get();
248 | this.version = (byte) (versionAndIHL >> 4);
249 | this.IHL = (byte) (versionAndIHL & 0x0F);
250 | this.headerLength = this.IHL << 2;
251 |
252 | this.typeOfService = BitUtils.getUnsignedByte(buffer.get());
253 | this.totalLength = BitUtils.getUnsignedShort(buffer.getShort());
254 |
255 | this.identificationAndFlagsAndFragmentOffset = buffer.getInt();
256 |
257 | this.TTL = BitUtils.getUnsignedByte(buffer.get());
258 | this.protocolNum = BitUtils.getUnsignedByte(buffer.get());
259 | this.protocol = TransportProtocol.numberToEnum(protocolNum);
260 | this.headerChecksum = BitUtils.getUnsignedShort(buffer.getShort());
261 |
262 | byte[] addressBytes = new byte[4];
263 | buffer.get(addressBytes, 0, 4);
264 | this.sourceAddress = InetAddress.getByAddress(addressBytes);
265 |
266 | buffer.get(addressBytes, 0, 4);
267 | this.destinationAddress = InetAddress.getByAddress(addressBytes);
268 |
269 | //this.optionsAndPadding = buffer.getInt();
270 | }
271 |
272 | public void fillHeader(ByteBuffer buffer)
273 | {
274 | buffer.put((byte) (this.version << 4 | this.IHL));
275 | buffer.put((byte) this.typeOfService);
276 | buffer.putShort((short) this.totalLength);
277 |
278 | buffer.putInt(this.identificationAndFlagsAndFragmentOffset);
279 |
280 | buffer.put((byte) this.TTL);
281 | buffer.put((byte) this.protocol.getNumber());
282 | buffer.putShort((short) this.headerChecksum);
283 |
284 | buffer.put(this.sourceAddress.getAddress());
285 | buffer.put(this.destinationAddress.getAddress());
286 | }
287 |
288 | @Override
289 | public String toString()
290 | {
291 | final StringBuilder sb = new StringBuilder("IP4Header{");
292 | sb.append("version=").append(version);
293 | sb.append(", IHL=").append(IHL);
294 | sb.append(", typeOfService=").append(typeOfService);
295 | sb.append(", totalLength=").append(totalLength);
296 | sb.append(", identificationAndFlagsAndFragmentOffset=").append(identificationAndFlagsAndFragmentOffset);
297 | sb.append(", TTL=").append(TTL);
298 | sb.append(", protocol=").append(protocolNum).append(":").append(protocol);
299 | sb.append(", headerChecksum=").append(headerChecksum);
300 | sb.append(", sourceAddress=").append(sourceAddress.getHostAddress());
301 | sb.append(", destinationAddress=").append(destinationAddress.getHostAddress());
302 | sb.append('}');
303 | return sb.toString();
304 | }
305 | }
306 |
307 | public static class TCPHeader
308 | {
309 | public static final int FIN = 0x01;
310 | public static final int SYN = 0x02;
311 | public static final int RST = 0x04;
312 | public static final int PSH = 0x08;
313 | public static final int ACK = 0x10;
314 | public static final int URG = 0x20;
315 |
316 | public int sourcePort;
317 | public int destinationPort;
318 |
319 | public long sequenceNumber;
320 | public long acknowledgementNumber;
321 |
322 | public byte dataOffsetAndReserved;
323 | public int headerLength;
324 | public byte flags;
325 | public int window;
326 |
327 | public int checksum;
328 | public int urgentPointer;
329 |
330 | public byte[] optionsAndPadding;
331 |
332 | public TCPHeader(ByteBuffer buffer)
333 | {
334 | this.sourcePort = BitUtils.getUnsignedShort(buffer.getShort());
335 | this.destinationPort = BitUtils.getUnsignedShort(buffer.getShort());
336 |
337 | this.sequenceNumber = BitUtils.getUnsignedInt(buffer.getInt());
338 | this.acknowledgementNumber = BitUtils.getUnsignedInt(buffer.getInt());
339 |
340 | this.dataOffsetAndReserved = buffer.get();
341 | this.headerLength = (this.dataOffsetAndReserved & 0xF0) >> 2;
342 | this.flags = buffer.get();
343 | this.window = BitUtils.getUnsignedShort(buffer.getShort());
344 |
345 | this.checksum = BitUtils.getUnsignedShort(buffer.getShort());
346 | this.urgentPointer = BitUtils.getUnsignedShort(buffer.getShort());
347 |
348 | int optionsLength = this.headerLength - TCP_HEADER_SIZE;
349 | if (optionsLength > 0)
350 | {
351 | optionsAndPadding = new byte[optionsLength];
352 | buffer.get(optionsAndPadding, 0, optionsLength);
353 | }
354 | }
355 |
356 | public TCPHeader()
357 | {
358 |
359 | }
360 |
361 | public boolean isFIN()
362 | {
363 | return (flags & FIN) == FIN;
364 | }
365 |
366 | public boolean isSYN()
367 | {
368 | return (flags & SYN) == SYN;
369 | }
370 |
371 |
372 | public boolean isRST()
373 | {
374 | return (flags & RST) == RST;
375 | }
376 |
377 | public boolean isPSH()
378 | {
379 | return (flags & PSH) == PSH;
380 | }
381 |
382 | public boolean isACK()
383 | {
384 | return (flags & ACK) == ACK;
385 | }
386 |
387 | public boolean isURG()
388 | {
389 | return (flags & URG) == URG;
390 | }
391 |
392 | private void fillHeader(ByteBuffer buffer)
393 | {
394 | buffer.putShort((short) sourcePort);
395 | buffer.putShort((short) destinationPort);
396 |
397 | buffer.putInt((int) sequenceNumber);
398 | buffer.putInt((int) acknowledgementNumber);
399 |
400 | buffer.put(dataOffsetAndReserved);
401 | buffer.put(flags);
402 | buffer.putShort((short) window);
403 |
404 | buffer.putShort((short) checksum);
405 | buffer.putShort((short) urgentPointer);
406 | }
407 |
408 | public static String flagToString(byte flags)
409 | {
410 | final StringBuilder sb = new StringBuilder("");
411 | if ((flags & FIN) == FIN) sb.append("FIN ");
412 | if ((flags & SYN) == SYN) sb.append("SYN ");
413 | if ((flags & RST) == RST) sb.append("RST ");
414 | if ((flags & PSH) == PSH) sb.append("PSH ");
415 | if ((flags & ACK) == ACK) sb.append("ACK ");
416 | if ((flags & URG) == URG) sb.append("URG ");
417 | return sb.toString();
418 | }
419 |
420 | public String printSimple()
421 | {
422 | final StringBuilder sb = new StringBuilder("");
423 | if (isFIN()) sb.append("FIN ");
424 | if (isSYN()) sb.append("SYN ");
425 | if (isRST()) sb.append("RST ");
426 | if (isPSH()) sb.append("PSH ");
427 | if (isACK()) sb.append("ACK ");
428 | if (isURG()) sb.append("URG ");
429 | sb.append("seq " + sequenceNumber + " ");
430 | sb.append("ack " + acknowledgementNumber + " ");
431 | return sb.toString();
432 | }
433 |
434 | @Override
435 | public String toString()
436 | {
437 | final StringBuilder sb = new StringBuilder("TCPHeader{");
438 | sb.append("sourcePort=").append(sourcePort);
439 | sb.append(", destinationPort=").append(destinationPort);
440 | sb.append(", sequenceNumber=").append(sequenceNumber);
441 | sb.append(", acknowledgementNumber=").append(acknowledgementNumber);
442 | sb.append(", headerLength=").append(headerLength);
443 | sb.append(", window=").append(window);
444 | sb.append(", checksum=").append(checksum);
445 | sb.append(", flags=");
446 | if (isFIN()) sb.append(" FIN");
447 | if (isSYN()) sb.append(" SYN");
448 | if (isRST()) sb.append(" RST");
449 | if (isPSH()) sb.append(" PSH");
450 | if (isACK()) sb.append(" ACK");
451 | if (isURG()) sb.append(" URG");
452 | sb.append('}');
453 | return sb.toString();
454 | }
455 | }
456 |
457 | public static class UDPHeader
458 | {
459 | public int sourcePort;
460 | public int destinationPort;
461 |
462 | public int length;
463 | public int checksum;
464 |
465 |
466 | public UDPHeader()
467 | {
468 |
469 | }
470 |
471 | private UDPHeader(ByteBuffer buffer)
472 | {
473 | this.sourcePort = BitUtils.getUnsignedShort(buffer.getShort());
474 | this.destinationPort = BitUtils.getUnsignedShort(buffer.getShort());
475 |
476 | this.length = BitUtils.getUnsignedShort(buffer.getShort());
477 | this.checksum = BitUtils.getUnsignedShort(buffer.getShort());
478 | }
479 |
480 | private void fillHeader(ByteBuffer buffer)
481 | {
482 | buffer.putShort((short) this.sourcePort);
483 | buffer.putShort((short) this.destinationPort);
484 |
485 | buffer.putShort((short) this.length);
486 | buffer.putShort((short) this.checksum);
487 | }
488 |
489 | @Override
490 | public String toString()
491 | {
492 | final StringBuilder sb = new StringBuilder("UDPHeader{");
493 | sb.append("sourcePort=").append(sourcePort);
494 | sb.append(", destinationPort=").append(destinationPort);
495 | sb.append(", length=").append(length);
496 | sb.append(", checksum=").append(checksum);
497 | sb.append('}');
498 | return sb.toString();
499 | }
500 | }
501 |
502 | private static class BitUtils
503 | {
504 | private static short getUnsignedByte(byte value)
505 | {
506 | return (short) (value & 0xFF);
507 | }
508 |
509 | private static int getUnsignedShort(short value)
510 | {
511 | return value & 0xFFFF;
512 | }
513 |
514 | private static long getUnsignedInt(int value)
515 | {
516 | return value & 0xFFFFFFFFL;
517 | }
518 | }
519 | }
520 |
521 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/tcpip/TCBStatus.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.tcpip;
2 |
3 | public enum TCBStatus
4 | {
5 | SYN_SENT,
6 | SYN_RECEIVED,
7 | ESTABLISHED,
8 | CLOSE_WAIT,
9 | LAST_ACK,
10 | //new
11 | CLOSED,
12 | }
13 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/util/ByteBufferPool.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.util;
2 |
3 | import java.nio.ByteBuffer;
4 |
5 | public class ByteBufferPool
6 | {
7 | public static final int BUFFER_SIZE = 16384; // XXX: Is this ideal?
8 |
9 | public static ByteBuffer acquire()
10 | {
11 | //return ByteBuffer.allocate(BUFFER_SIZE);
12 | return ByteBuffer.allocateDirect(BUFFER_SIZE);
13 | }
14 | }
15 |
16 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/java/org/ndroi/easy163/vpn/util/ProxyException.java:
--------------------------------------------------------------------------------
1 | package org.ndroi.easy163.vpn.util;
2 |
3 | /**
4 | * @author Administrator
5 | */
6 | public class ProxyException extends RuntimeException
7 | {
8 | public ProxyException(String msg)
9 | {
10 | super(msg);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/drawable/side_nav_bar.xml:
--------------------------------------------------------------------------------
1 |
3 |
9 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/layout/app_bar_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
16 |
17 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/layout/nav_header_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
22 |
23 |
30 |
31 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/menu/activity_main_drawer.xml:
--------------------------------------------------------------------------------
1 |
2 |
25 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/mipmap-xxhdpi/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w4123/easy163/2e7ffb027e9c4a01bc4edd5e8bdcbaaa3d96a29b/Easy163/app/src/main/res/mipmap-xxhdpi/github.png
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/mipmap-xxhdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w4123/easy163/2e7ffb027e9c4a01bc4edd5e8bdcbaaa3d96a29b/Easy163/app/src/main/res/mipmap-xxhdpi/icon.png
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/mipmap-xxhdpi/icon_tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w4123/easy163/2e7ffb027e9c4a01bc4edd5e8bdcbaaa3d96a29b/Easy163/app/src/main/res/mipmap-xxhdpi/icon_tile.png
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #D2361C
4 | #AF2B15
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 8dp
6 | 176dp
7 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Easy163
3 | Open navigation drawer
4 | Close navigation drawer
5 | easy163
6 | github.com/ndroi
7 | Navigation header
8 | 服务关闭中,点击开启
9 | 服务开启中,点击关闭
10 | 运行日志
11 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Easy163/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Easy163/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:7.1.3'
11 |
12 |
13 | // NOTE: Do not place your application dependencies here; they belong
14 | // in the individual module build.gradle files
15 | }
16 | }
17 |
18 | allprojects {
19 | repositories {
20 | google()
21 | mavenCentral()
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
29 | allprojects {
30 | gradle.projectsEvaluated {
31 | tasks.withType(JavaCompile) {
32 | options.compilerArgs << "-Xlint:deprecation" << "-Xlint:unchecked"
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/Easy163/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Easy163/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/w4123/easy163/2e7ffb027e9c4a01bc4edd5e8bdcbaaa3d96a29b/Easy163/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/Easy163/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 15 22:10:41 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
7 |
--------------------------------------------------------------------------------
/Easy163/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/Easy163/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/Easy163/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 ndroi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # easy163 安卓版网易云助手
2 |
3 | **一键可用,无须 ROOT**
4 |
5 | **播放 VIP 和下架歌曲**
6 |
7 | **任意歌曲下载到本地**
8 |
9 | **下架歌曲收藏**
10 |
11 | 工作原理:
12 | 在本地搭建 VPN 服务,拦截修改重定向网易云 APP 的 HTTP 请求,从其他音乐平台获取资源
13 |
14 | 特性:
15 |
16 | - 支持 VIP 和下架歌曲播放
17 | - 支持任意歌曲下载
18 | - 支持收藏(不支持 Like)
19 | - 高精度歌曲匹配算法
20 |
21 |
22 | 已知问题:
23 |
24 | - Easy163 开启时无法登录网易云音乐,请先关闭时登录,登录完毕后再打开
25 | - 用户自身个人主页不可用,暂时无解
26 | - 下载时若长时间显示正在计算文件长度然后失败,点击重试即可
27 |
28 |
29 | 使用方式:
30 | 开启本软件的 VPN 服务即可使用
31 | 如无法使用请重启网易云
32 | 开启本软件后如遇到设备网络异常请关闭本软件
33 |
34 | 说明:
35 | 开启本软件后网易云 APP 所有的 HTTP 请求皆由本软件代理,如质疑其安全性欢迎审阅源码并自行编译 APK
36 | 本软件为实验性项目,使用完全免费,仅提供技术研究使用,作者不承担用户使用造成的一切责任
37 |
38 |
39 | 推荐:
40 | 网易云官方推出的 **网易云极速版** [https://mip.onlinedown.net/soft/1225624.htm]
41 | 官网推出后不久下架,此版本网易云功能简单稳定,与 easy163 兼容良好
42 |
43 | **项目需要频繁维护,希望大家支持**
44 |
45 | **欢迎点赞项目,提交问题**
46 |
47 | 感谢 [https://github.com/nondanee/UnblockNeteaseMusic] 提供 NodeJS 版网易云助手
48 | 本项目参考其大量业务逻辑,包括 APP 图标
49 |
50 | 感谢 [https://github.com/mightofcode/android-vpnservice] 实现了轻量易用的 Android VPN 代理程序
51 |
--------------------------------------------------------------------------------