├── .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 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | -------------------------------------------------------------------------------- /Easy163/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 37 | 57 | 58 | 59 | 60 | 61 | 62 | 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 | 5 | 6 | 7 | 10 | 13 | 16 | 19 | 22 | 23 | 24 | 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 |