From 3716840df55cf0292dfc6a02fcd0b686962bd46e Mon Sep 17 00:00:00 2001 From: WrBug Date: Sat, 16 Feb 2019 10:34:09 +0800 Subject: [PATCH 01/72] =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../developerhelper/DeveloperApplication.kt | 40 +++++++++++++++++++ .../activity/hierachy/AppInfoPagerAdapter.kt | 1 + .../ui/activity/hierachy/HierarchyActivity.kt | 6 +++ .../ui/activity/main/MainActivity.kt | 13 +++--- .../ui/widget/hierarchyView/HierarchyView.kt | 1 + .../LayoutInfoViewPagerAdapter.kt | 1 - .../layoutinfoview/ViewTreeGraphAdapter.kt | 8 +--- .../layoutinfoview/infopage/InfoAdapter.kt | 5 +-- .../layoutinfoview/infopage/ItemInfo.kt | 1 + .../developerhelper/util/EnforceUtils.kt | 2 +- app/src/main/res/values/strings.xml | 6 +++ 11 files changed, 65 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt b/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt index b76ab66..541b394 100644 --- a/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt +++ b/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt @@ -1,5 +1,7 @@ package com.wrbug.developerhelper +import android.app.Activity +import android.os.Bundle import android.os.Handler import android.os.Looper import com.elvishew.xlog.LogConfiguration @@ -13,6 +15,7 @@ import java.io.File import java.io.FileOutputStream import kotlin.concurrent.thread import com.wrbug.developerhelper.commonwidget.flexibletoast.FlexibleToast +import com.wrbug.developerhelper.ui.activity.main.MainActivity class DeveloperApplication : BaseApp() { @@ -42,6 +45,43 @@ class DeveloperApplication : BaseApp() { DefaultsFactory.createPrinter() ) releaseAssetsFile() + registerLifecycle() + } + + private fun registerLifecycle() { + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + private var count = 0 + override fun onActivityPaused(activity: Activity?) { + + } + + override fun onActivityResumed(activity: Activity?) { + } + + override fun onActivityStarted(activity: Activity?) { + count++ + } + + override fun onActivityDestroyed(activity: Activity?) { + } + + override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { + } + + override fun onActivityStopped(activity: Activity?) { + count-- + activity?.let { + if (count == 0 && activity is MainActivity) { + activity.finish() + } + } + + } + + override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { + } + + }) } private fun registerModule() { diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt index a118cb8..529efab 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoPagerAdapter.kt @@ -73,6 +73,7 @@ class AppInfoPagerAdapter( listener?.showHierachyView() dialog.dismissAllowingStateLoss() }) + item.textColor = context.resources.getColor(R.color.colorPrimaryDark) itemInfos.add(item) itemInfos.add(ItemInfo("VersionCode", it.packageInfo.versionCode)) itemInfos.add(ItemInfo("VersionName", it.packageInfo.versionName)) diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt index eed2cf4..c4fc0d8 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt @@ -133,6 +133,12 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan hierarchyDetailView.visibility = View.GONE return } + if (showHierachyView) { + showHierachyView = false + hierarchyView.visibility = View.GONE + showAppInfoDialog() + return + } super.onBackPressed() } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt index 41f1901..057d524 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt @@ -50,7 +50,6 @@ class MainActivity : BaseVMActivity() { setupActionBar(R.id.toolbar) { } - ShellManager.openAccessibilityService() initListener() val filter = IntentFilter(ReceiverConstant.ACTION_ACCESSIBILITY_SERVICE_STATUS_CHANGED) @@ -149,7 +148,7 @@ class MainActivity : BaseVMActivity() { ClipboardUtils.saveClipboardText(this, "627962572") showSnack(R.string.copy_success) } - .setNeutralButton("检查更新") { _, _ -> + .setNeutralButton(getString(R.string.check_update)) { _, _ -> checkUpdate(true) } .create().show() @@ -160,12 +159,12 @@ class MainActivity : BaseVMActivity() { private fun checkUpdate(showSnack: Boolean = false) { if (showSnack) { - showSnack("正在检查新版本...") + showSnack(getString(R.string.checking_update)) } UpdateUtils.checkUpdate(object : Callback { override fun onSuccess(data: VersionInfo) { if (BuildConfig.VERSION_NAME == data.versionName) { - showSnack("暂无新版本") + showSnack(getString(R.string.no_new_version)) return } showUpdateDialog(data) @@ -173,16 +172,16 @@ class MainActivity : BaseVMActivity() { override fun onFailed(msg: String) { if (showSnack) { - showSnack("检查失败...") + showSnack(getString(R.string.check_update_failed)) } } }) } private fun showUpdateDialog(data: VersionInfo) = AlertDialog.Builder(this) - .setTitle("发现新版本") + .setTitle(getString(R.string.find_new_version)) .setMessage("版本号:${data.versionName}\n更新时间:${data.updateDate}\n大小:${data.size}\n版本说明:\n${data.feature}") - .setPositiveButton("下载") { _, _ -> + .setPositiveButton(getString(R.string.download)) { _, _ -> val intent = Intent(Intent.ACTION_VIEW) val uri = Uri.parse(data.downloadUrl) intent.data = uri diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt index fd4c119..28679e8 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt @@ -36,6 +36,7 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr mHierarchyNodes.addAll(it) } nodeMap.putAll(DeveloperHelperAccessibilityService.nodeMap) + visibility=View.VISIBLE invalidate() } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt index 3c6c383..c3461ef 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt @@ -56,7 +56,6 @@ class LayoutInfoViewPagerAdapter( resetViewTreeTab(node) onNodeChangedListener?.onChanged(node.node, node.parent?.node) } - }) val configuration = BuchheimWalkerConfiguration.Builder() .setSiblingSeparation(100) diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt index bafec3c..8a3e153 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/ViewTreeGraphAdapter.kt @@ -18,6 +18,7 @@ class ViewTreeGraphAdapter(@NonNull val context: Context, @LayoutRes val layoutR val node = data as ViewTreeGraphNode widgetTv.text = node.node.widget cardView.setOnClickListener { + widgetTv.text = node.node.widget listener?.onClick(node, position) } when { @@ -27,13 +28,6 @@ class ViewTreeGraphAdapter(@NonNull val context: Context, @LayoutRes val layoutR cardView.setCardBackgroundColor(context.resources.getColor(R.color.colorPrimary)) if (node.shortName) { widgetTv.text = toShortName(node.node.widget) - cardView.setOnClickListener { - widgetTv.text = node.node.widget - node.shortName = false - cardView.setOnClickListener{ - listener?.onClick(node, position) - } - } } else { widgetTv.text = node.node.widget } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt index 4a66f6f..f442ae2 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/infopage/InfoAdapter.kt @@ -39,6 +39,7 @@ class InfoAdapter(val context: Context) : RecyclerView.Adapter脱壳应用管理 移除该项 请先开启Xposed设置 + 检查更新 + 正在检查新版本... + 暂无新版本 + 检查失败... + 发现新版本 + 下载 From 3a53aa30cd2550b2ce4b395e812df582ccb203f2 Mon Sep 17 00:00:00 2001 From: WrBug Date: Wed, 20 Feb 2019 10:25:51 +0800 Subject: [PATCH 02/72] =?UTF-8?q?netbare=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 9 +--- .../developerhelper/DeveloperApplication.kt | 12 ++--- basewidgetimport/.gitignore | 1 + basewidgetimport/build.gradle | 45 +++++++++++++++++++ basewidgetimport/proguard-rules.pro | 21 +++++++++ .../ExampleInstrumentedTest.java | 26 +++++++++++ basewidgetimport/src/main/AndroidManifest.xml | 2 + .../basewidgetimport/BaseWidget.kt | 14 ++++++ .../src/main/res/values/strings.xml | 3 ++ .../basewidgetimport/ExampleUnitTest.java | 17 +++++++ build.gradle | 1 + .../ApplicationLifeCycleCallback.kt | 10 +++++ settings.gradle | 2 +- 13 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 basewidgetimport/.gitignore create mode 100644 basewidgetimport/build.gradle create mode 100644 basewidgetimport/proguard-rules.pro create mode 100644 basewidgetimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java create mode 100644 basewidgetimport/src/main/AndroidManifest.xml create mode 100644 basewidgetimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseWidget.kt create mode 100644 basewidgetimport/src/main/res/values/strings.xml create mode 100644 basewidgetimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java create mode 100644 commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ApplicationLifeCycleCallback.kt diff --git a/app/build.gradle b/app/build.gradle index 5495239..10ad169 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,15 +48,10 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' implementation 'com.google.code.gson:gson:2.8.5' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'com.google.android.material:material:1.1.0-alpha02' implementation 'com.github.yhaolpz:FloatWindow:1.0.9' implementation project(':basecommon') kapt "com.android.databinding:compiler:3.1.4" @@ -69,11 +64,9 @@ dependencies { implementation 'com.evrencoskun.library:tableview:0.8.8' implementation 'gdut.bsx:share2:0.9.3' kapt 'com.google.dagger:dagger-compiler:2.16' - implementation project(':commonutil') - implementation project(':mmkv') implementation project(':xposedmodule') - implementation project(':commonwidget') implementation 'de.blox:graphview:0.5.0' + implementation project(':basewidgetimport') } kapt { generateStubs = true diff --git a/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt b/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt index 541b394..7c01f71 100644 --- a/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt +++ b/app/src/main/java/com/wrbug/developerhelper/DeveloperApplication.kt @@ -1,6 +1,7 @@ package com.wrbug.developerhelper import android.app.Activity +import android.content.Context import android.os.Bundle import android.os.Handler import android.os.Looper @@ -9,6 +10,7 @@ import com.elvishew.xlog.LogLevel import com.elvishew.xlog.XLog import com.elvishew.xlog.internal.DefaultsFactory import com.wrbug.developerhelper.basecommon.BaseApp +import com.wrbug.developerhelper.basewidgetimport.BaseWidget import com.wrbug.developerhelper.commonutil.CommonUtils import com.wrbug.developerhelper.mmkv.manager.MMKVManager import java.io.File @@ -36,9 +38,12 @@ class DeveloperApplication : BaseApp() { } } + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + } override fun onCreate() { super.onCreate() - registerModule() + BaseWidget.init(this) instance = this XLog.init( LogConfiguration.Builder().logLevel(LogLevel.ALL).tag("developerHelper.print-->").build(), @@ -84,10 +89,7 @@ class DeveloperApplication : BaseApp() { }) } - private fun registerModule() { - MMKVManager.register(this) - CommonUtils.register(this) - } + private fun releaseAssetsFile() { thread { diff --git a/basewidgetimport/.gitignore b/basewidgetimport/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/basewidgetimport/.gitignore @@ -0,0 +1 @@ +/build diff --git a/basewidgetimport/build.gradle b/basewidgetimport/build.gradle new file mode 100644 index 0000000..71a8f8a --- /dev/null +++ b/basewidgetimport/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 28 + + + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + api fileTree(include: ['*.jar'], dir: 'libs') + api 'androidx.appcompat:appcompat:1.0.2' + api 'androidx.constraintlayout:constraintlayout:1.1.3' + api 'androidx.legacy:legacy-support-v4:1.0.0' + api 'androidx.recyclerview:recyclerview:1.0.0' + api 'com.google.android.material:material:1.1.0-alpha02' + api project(':commonutil') + api project(':commonwidget') + api project(':mmkv') + api project(':netbare-core') + api project(':netbare-injector') + api project(':basecommon') + api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/basewidgetimport/proguard-rules.pro b/basewidgetimport/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/basewidgetimport/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/basewidgetimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java b/basewidgetimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java new file mode 100644 index 0000000..38b9eb4 --- /dev/null +++ b/basewidgetimport/src/androidTest/java/com/wrbug/developerhelper/basewidgetimport/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.wrbug.developerhelper.basewidgetimport; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.wrbug.developerhelper.basewidgetimport.test", appContext.getPackageName()); + } +} diff --git a/basewidgetimport/src/main/AndroidManifest.xml b/basewidgetimport/src/main/AndroidManifest.xml new file mode 100644 index 0000000..27a1cb8 --- /dev/null +++ b/basewidgetimport/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/basewidgetimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseWidget.kt b/basewidgetimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseWidget.kt new file mode 100644 index 0000000..cbab057 --- /dev/null +++ b/basewidgetimport/src/main/java/com/wrbug/developerhelper/basewidgetimport/BaseWidget.kt @@ -0,0 +1,14 @@ +package com.wrbug.developerhelper.basewidgetimport + +import android.app.Application +import com.github.megatronking.netbare.NetBare +import com.wrbug.developerhelper.commonutil.CommonUtils +import com.wrbug.developerhelper.mmkv.manager.MMKVManager + +object BaseWidget { + fun init(application: Application) { + MMKVManager.register(application) + CommonUtils.register(application) + NetBare.get().attachApplication(application, BuildConfig.DEBUG) + } +} \ No newline at end of file diff --git a/basewidgetimport/src/main/res/values/strings.xml b/basewidgetimport/src/main/res/values/strings.xml new file mode 100644 index 0000000..2e0519d --- /dev/null +++ b/basewidgetimport/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + basewidgetimport + diff --git a/basewidgetimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java b/basewidgetimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java new file mode 100644 index 0000000..32c485c --- /dev/null +++ b/basewidgetimport/src/test/java/com/wrbug/developerhelper/basewidgetimport/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.wrbug.developerhelper.basewidgetimport; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index d875d87..46e05c0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.21' ext.kotlin_version = '1.3.11' ext.gradle_version = '3.2.0' repositories { diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ApplicationLifeCycleCallback.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ApplicationLifeCycleCallback.kt new file mode 100644 index 0000000..ad4562c --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ApplicationLifeCycleCallback.kt @@ -0,0 +1,10 @@ +package com.wrbug.developerhelper.commonutil + +import android.app.Application +import android.content.Context + +interface ApplicationLifeCycleCallback { + fun attachBaseContext(context: Context) + + fun onCreate(application: Application) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index cf8020c..a09a273 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':basecommon', ':commonutil', ':mmkv', ':xposedmodule', ':commonwidget' +include ':app', ':basecommon', ':commonutil', ':mmkv', ':xposedmodule', ':commonwidget', ':netbare-core', ':netbare-injector', ':basewidgetimport', ':netbare-sample' From 2848fe5e58efabd4afde9c54ef7d6b15d0c382c4 Mon Sep 17 00:00:00 2001 From: WrBug Date: Wed, 20 Feb 2019 10:27:37 +0800 Subject: [PATCH 03/72] =?UTF-8?q?netbare=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netbare-core/.gitignore | 1 + netbare-core/build.gradle | 30 + netbare-core/src/main/AndroidManifest.xml | 14 + .../github/megatronking/netbare/NetBare.java | 193 +++++ .../megatronking/netbare/NetBareConfig.java | 307 +++++++ .../megatronking/netbare/NetBareListener.java | 38 + .../megatronking/netbare/NetBareLog.java | 169 ++++ .../megatronking/netbare/NetBareService.java | 121 +++ .../megatronking/netbare/NetBareThread.java | 235 ++++++ .../megatronking/netbare/NetBareUtils.java | 153 ++++ .../netbare/NetBareVirtualGateway.java | 334 ++++++++ .../megatronking/netbare/NetBareXLog.java | 174 ++++ .../gateway/DefaultVirtualGateway.java | 70 ++ .../gateway/DefaultVirtualGatewayFactory.java | 64 ++ .../netbare/gateway/Interceptor.java | 80 ++ .../netbare/gateway/InterceptorChain.java | 99 +++ .../netbare/gateway/InterceptorFactory.java | 36 + .../megatronking/netbare/gateway/Request.java | 48 ++ .../netbare/gateway/RequestChain.java | 58 ++ .../netbare/gateway/Response.java | 48 ++ .../netbare/gateway/ResponseChain.java | 58 ++ .../netbare/gateway/SessionTunnelFlow.java | 101 +++ .../netbare/gateway/SpecVirtualGateway.java | 102 +++ .../netbare/gateway/TunnelFlow.java | 37 + .../netbare/gateway/VirtualGateway.java | 92 +++ .../gateway/VirtualGatewayFactory.java | 37 + .../http/ContainerHttpInterceptor.java | 196 +++++ .../netbare/http/Http2SniffInterceptor.java | 110 +++ .../http/HttpHeaderParseInterceptor.java | 186 +++++ .../http/HttpHeaderSeparateInterceptor.java | 137 ++++ .../http/HttpHeaderSniffInterceptor.java | 174 ++++ .../megatronking/netbare/http/HttpId.java | 42 + .../netbare/http/HttpIndexInterceptor.java | 86 ++ .../netbare/http/HttpInterceptor.java | 111 +++ .../netbare/http/HttpInterceptorFactory.java | 39 + .../netbare/http/HttpInterceptorsFactory.java | 39 + .../megatronking/netbare/http/HttpMethod.java | 101 +++ .../http/HttpMultiplexInterceptor.java | 89 +++ .../netbare/http/HttpPendingInterceptor.java | 123 +++ .../netbare/http/HttpProtocol.java | 115 +++ .../netbare/http/HttpRequest.java | 206 +++++ .../netbare/http/HttpRequestChain.java | 65 ++ .../netbare/http/HttpResponse.java | 278 +++++++ .../netbare/http/HttpResponseChain.java | 65 ++ .../netbare/http/HttpSession.java | 48 ++ .../netbare/http/HttpSessionFactory.java | 46 ++ .../netbare/http/HttpSniffInterceptor.java | 111 +++ .../netbare/http/HttpVirtualGateway.java | 135 ++++ .../http/HttpVirtualGatewayFactory.java | 71 ++ .../netbare/http/HttpZygoteRequest.java | 79 ++ .../netbare/http/HttpZygoteResponse.java | 80 ++ .../netbare/http/SSLCodecInterceptor.java | 256 ++++++ .../netbare/http/SSLHttpRequestCodec.java | 134 ++++ .../netbare/http/SSLHttpResponseCodec.java | 230 ++++++ .../netbare/http/SSLRefluxCallback.java | 33 + .../netbare/http/SSLRefluxInterceptor.java | 58 ++ .../netbare/http2/DecodeCallback.java | 35 + .../netbare/http2/EncodeCallback.java | 31 + .../megatronking/netbare/http2/ErrorCode.java | 87 ++ .../megatronking/netbare/http2/FrameType.java | 120 +++ .../megatronking/netbare/http2/Hpack.java | 753 ++++++++++++++++++ .../megatronking/netbare/http2/Http2.java | 77 ++ .../netbare/http2/Http2DecodeInterceptor.java | 481 +++++++++++ .../netbare/http2/Http2EncodeInterceptor.java | 245 ++++++ .../netbare/http2/Http2Settings.java | 165 ++++ .../netbare/http2/Http2Stream.java | 33 + .../netbare/http2/Http2Updater.java | 30 + .../megatronking/netbare/http2/Huffman.java | 236 ++++++ .../megatronking/netbare/ip/Header.java | 80 ++ .../megatronking/netbare/ip/IcmpHeader.java | 91 +++ .../megatronking/netbare/ip/IpAddress.java | 83 ++ .../megatronking/netbare/ip/IpHeader.java | 130 +++ .../megatronking/netbare/ip/Protocol.java | 68 ++ .../megatronking/netbare/ip/TcpHeader.java | 157 ++++ .../megatronking/netbare/ip/UdpHeader.java | 145 ++++ .../netbare/net/DumpCallback.java | 33 + .../github/megatronking/netbare/net/Net.java | 64 ++ .../megatronking/netbare/net/NetDumper.java | 149 ++++ .../megatronking/netbare/net/Session.java | 94 +++ .../netbare/net/SessionProvider.java | 94 +++ .../megatronking/netbare/net/UidDumper.java | 184 +++++ .../megatronking/netbare/net/UidProvider.java | 21 + .../netbare/proxy/BaseProxyServer.java | 73 ++ .../proxy/IcmpProxyServerForwarder.java | 51 ++ .../netbare/proxy/ProxyServer.java | 67 ++ .../netbare/proxy/ProxyServerForwarder.java | 47 ++ .../netbare/proxy/TcpProxyServer.java | 216 +++++ .../proxy/TcpProxyServerForwarder.java | 135 ++++ .../netbare/proxy/UdpProxyServer.java | 180 +++++ .../proxy/UdpProxyServerForwarder.java | 92 +++ .../netbare/ssl/CertificateGenerator.java | 253 ++++++ .../ssl/CertificateInstallActivity.java | 82 ++ .../github/megatronking/netbare/ssl/JKS.java | 215 +++++ .../megatronking/netbare/ssl/SSLCodec.java | 476 +++++++++++ .../netbare/ssl/SSLEngineFactory.java | 212 +++++ .../netbare/ssl/SSLRequestCodec.java | 75 ++ .../netbare/ssl/SSLResponseCodec.java | 114 +++ .../megatronking/netbare/ssl/SSLUtils.java | 251 ++++++ .../tunnel/ConnectionShutdownException.java | 38 + .../netbare/tunnel/NioCallback.java | 61 ++ .../netbare/tunnel/NioTunnel.java | 195 +++++ .../netbare/tunnel/TcpProxyTunnel.java | 75 ++ .../netbare/tunnel/TcpRemoteTunnel.java | 84 ++ .../netbare/tunnel/TcpTunnel.java | 80 ++ .../netbare/tunnel/TcpVATunnel.java | 163 ++++ .../megatronking/netbare/tunnel/Tunnel.java | 37 + .../netbare/tunnel/UdpRemoteTunnel.java | 76 ++ .../netbare/tunnel/UdpTunnel.java | 68 ++ .../netbare/tunnel/UdpVATunnel.java | 197 +++++ .../netbare/tunnel/VirtualGatewayTunnel.java | 47 ++ .../netbare/ws/WebSocketCallback.java | 66 ++ .../netbare/ws/WebSocketProtocol.java | 145 ++++ .../netbare/ws/WebSocketReader.java | 313 ++++++++ netbare-core/src/main/res/values/styles.xml | 21 + netbare-injector/.gitignore | 1 + netbare-injector/build.gradle | 25 + netbare-injector/src/main/AndroidManifest.xml | 4 + .../megatronking/netbare/http/Cookie.java | 581 ++++++++++++++ .../megatronking/netbare/http/HttpBody.java | 27 + .../netbare/http/HttpHeaderPart.java | 204 +++++ .../netbare/http/HttpInjectInterceptor.java | 136 ++++ .../netbare/http/HttpRawBody.java | 45 ++ .../netbare/http/HttpRequestHeaderPart.java | 233 ++++++ .../http/HttpRequestInjectorCallback.java | 59 ++ .../netbare/http/HttpResponseHeaderPart.java | 239 ++++++ .../http/HttpResponseInjectorCallback.java | 58 ++ .../netbare/injector/BlockedHttpInjector.java | 63 ++ .../netbare/injector/HttpInjector.java | 115 +++ .../netbare/injector/InjectorCallback.java | 38 + .../netbare/injector/SimpleHttpInjector.java | 78 ++ .../netbare/io/ByteBufferInputStream.java | 39 + .../netbare/io/HttpBodyInputStream.java | 39 + .../netbare/stream/BufferStream.java | 47 ++ .../netbare/stream/ByteStream.java | 62 ++ .../megatronking/netbare/stream/Stream.java | 38 + .../netbare/stream/StringStream.java | 39 + .../netbare/stream/TinyFileStream.java | 78 ++ .../utils/CaseInsensitiveLinkedMap.java | 362 +++++++++ netbare-sample/build.gradle | 40 + netbare-sample/proguard-rules.pro | 21 + .../netbare/ExampleInstrumentedTest.java | 26 + netbare-sample/src/main/AndroidManifest.xml | 30 + .../github/megatronking/netbare/sample/App.kt | 28 + .../megatronking/netbare/sample/AppService.kt | 54 ++ .../netbare/sample/BaiduLogoInjector.kt | 55 ++ .../netbare/sample/HttpUrlPrintInterceptor.kt | 43 + .../netbare/sample/MainActivity.kt | 107 +++ .../netbare/sample/PresellLocationInjector.kt | 115 +++ .../netbare/sample/TestHttpIntercepter.kt | 32 + .../netbare/sample/WechatLocationInjector.kt | 101 +++ .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 ++++ .../src/main/res/layout/activity_main.xml | 21 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../src/main/res/raw/baidu_inject_logo.png | Bin 0 -> 10764 bytes netbare-sample/src/main/res/values/colors.xml | 6 + .../src/main/res/values/strings.xml | 7 + netbare-sample/src/main/res/values/styles.xml | 11 + .../megatronking/netbare/ExampleUnitTest.java | 17 + 170 files changed, 17571 insertions(+) create mode 100644 netbare-core/.gitignore create mode 100644 netbare-core/build.gradle create mode 100644 netbare-core/src/main/AndroidManifest.xml create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java create mode 100755 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java create mode 100644 netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java create mode 100644 netbare-core/src/main/res/values/styles.xml create mode 100644 netbare-injector/.gitignore create mode 100644 netbare-injector/build.gradle create mode 100644 netbare-injector/src/main/AndroidManifest.xml create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java create mode 100644 netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java create mode 100644 netbare-sample/build.gradle create mode 100644 netbare-sample/proguard-rules.pro create mode 100644 netbare-sample/src/androidTest/java/com/github/megatronking/netbare/ExampleInstrumentedTest.java create mode 100644 netbare-sample/src/main/AndroidManifest.xml create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt create mode 100644 netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt create mode 100644 netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 netbare-sample/src/main/res/drawable/ic_launcher_background.xml create mode 100644 netbare-sample/src/main/res/layout/activity_main.xml create mode 100644 netbare-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 netbare-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 netbare-sample/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 netbare-sample/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 netbare-sample/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 netbare-sample/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 netbare-sample/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 netbare-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 netbare-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 netbare-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 netbare-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 netbare-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 netbare-sample/src/main/res/raw/baidu_inject_logo.png create mode 100644 netbare-sample/src/main/res/values/colors.xml create mode 100644 netbare-sample/src/main/res/values/strings.xml create mode 100644 netbare-sample/src/main/res/values/styles.xml create mode 100644 netbare-sample/src/test/java/com/github/megatronking/netbare/ExampleUnitTest.java diff --git a/netbare-core/.gitignore b/netbare-core/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/netbare-core/.gitignore @@ -0,0 +1 @@ +/build diff --git a/netbare-core/build.gradle b/netbare-core/build.gradle new file mode 100644 index 0000000..1784f6e --- /dev/null +++ b/netbare-core/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'com.android.support:appcompat-v7:28.0.0' + + implementation 'org.bouncycastle:bcpkix-jdk15on:1.56' + implementation 'org.bouncycastle:bcprov-jdk15on:1.56' + implementation 'com.google.guava:guava:19.0' +} diff --git a/netbare-core/src/main/AndroidManifest.xml b/netbare-core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b4a5f13 --- /dev/null +++ b/netbare-core/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java new file mode 100644 index 0000000..e1b51cd --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBare.java @@ -0,0 +1,193 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.app.Activity; +import android.app.Application; +import android.content.Intent; +import android.net.VpnService; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; + +import androidx.core.content.ContextCompat; +import com.github.megatronking.netbare.gateway.DefaultVirtualGatewayFactory; +import com.github.megatronking.netbare.gateway.VirtualGatewayFactory; +import com.github.megatronking.netbare.ssl.JKS; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * NetBare is a single instance, we can use this class to config and manage the NetBare service. + * The NetBare service is an implement class of {@link VpnService}, before starting this service, + * should call {@link #prepare()} to check the vpn state. + * + * Start and stop the NetBare service: + *
+ * 
+ *     NetBare.get().start(config);
+ *     NetBare.get().stop();
+ * 
+ * 
+ * + * @author Megatron King + * @since 2018-10-07 09:28 + */ +public final class NetBare { + + private static class Holder { + + private static final NetBare INSTANCE = new NetBare(); + + } + + private final Set mListeners; + private final Handler mMainThreadHandler; + + private Application mApp; + private NetBareConfig mNetBareConfig; + + private boolean mAlive; + + public static NetBare get() { + return Holder.INSTANCE; + } + + private NetBare() { + mListeners = new LinkedHashSet<>(); + mMainThreadHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Attach an application instance to NetBare. We recommend you to call this method in your + * {@link Application} class. + * + * @param application The application instance. + * @param debug Should print logs in console. + * @return The single instance of NetBare. + */ + public NetBare attachApplication(@NonNull Application application, boolean debug) { + JKS.init(application); + mApp = application; + NetBareLog.setDebug(debug); + return this; + } + + /** + * Prepare to establish a VPN connection. This method returns {@code null} if the VPN + * application is already prepared or if the user has previously consented to the VPN + * application. Otherwise, it returns an {@link Intent} to a system activity. The application + * should launch the activity using {@link Activity#startActivityForResult} to get itself + * prepared. + * + * @return The intent to call using {@link Activity#startActivityForResult}. + */ + public Intent prepare() { + return VpnService.prepare(mApp); + } + + /** + * Start the NetBare service with your specific configuration. If the service is started, + * {@link NetBareListener#onServiceStarted()} will be invoked. + * + * @param config The configuration for NetBare service. + */ + public void start(@NonNull NetBareConfig config) { + if (config.mtu <= 0) { + throw new RuntimeException("Must set mtu in NetBareConfig"); + } + if (config.address == null) { + throw new RuntimeException("Must set address in NetBareConfig"); + } + mNetBareConfig = config; + Intent intent = new Intent(NetBareService.ACTION_START); + intent.setPackage(mApp.getPackageName()); + ContextCompat.startForegroundService(mApp, intent); + } + + /** + * Stop the NetBare service. If the service is started, + * {@link NetBareListener#onServiceStopped()} will be invoked. + */ + public void stop() { + Intent intent = new Intent(NetBareService.ACTION_STOP); + intent.setPackage(mApp.getPackageName()); + mApp.startService(intent); + } + + /** + * Whether the NetBare service is alive or not. + * + * @return True if the service is alive, false otherwise. + */ + public boolean isActive() { + return mAlive; + } + + /** + * Register a callback to be invoked when the service state changes. + * + * @param listener The callback to register. + */ + public void registerNetBareListener(NetBareListener listener) { + mListeners.add(listener); + } + + /** + * Remove a previously installed service state callback. + * + * @param listener The callback to remove. + */ + public void unregisterNetBareListener(NetBareListener listener) { + mListeners.remove(listener); + } + + /* package */ NetBareConfig getConfig() { + return mNetBareConfig; + } + + /* package */ VirtualGatewayFactory getGatewayFactory() { + // Make sure the virtual gateway not be null. + return mNetBareConfig.gatewayFactory == null ? DefaultVirtualGatewayFactory.create() : + mNetBareConfig.gatewayFactory; + } + + /* package */ void notifyServiceStarted() { + mAlive = true; + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + for (NetBareListener listener : mListeners) { + listener.onServiceStarted(); + } + } + }); + } + + /* package */ void notifyServiceStopped() { + mAlive = false; + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + for (NetBareListener listener : mListeners) { + listener.onServiceStopped(); + } + } + }); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java new file mode 100644 index 0000000..c690a03 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareConfig.java @@ -0,0 +1,307 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.app.PendingIntent; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.VirtualGatewayFactory; +import com.github.megatronking.netbare.http.HttpInterceptorFactory; +import com.github.megatronking.netbare.http.HttpVirtualGatewayFactory; +import com.github.megatronking.netbare.ip.IpAddress; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.net.UidProvider; +import com.github.megatronking.netbare.ssl.JKS; + +import java.net.InetAddress; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The configuration class for NetBare. Use {@link Builder} to construct an instance. + * + * @author Megatron King + * @since 2018-10-07 11:20 + */ +public final class NetBareConfig { + + String session; + PendingIntent configureIntent; + int mtu; + IpAddress address; + Set routes; + Set dnsServers; + Set allowedApplications; + Set disallowedApplications; + Set allowedHosts; + Set disallowedHosts; + VirtualGatewayFactory gatewayFactory; + UidProvider uidProvider; + boolean dumpUid; + boolean excludeSelf; + + private NetBareConfig() { + } + + /** + * Create a new builder based on the current. + * + * @return A new builder instance. + */ + public Builder newBuilder() { + Builder builder = new Builder(); + builder.mConfig = this; + return builder; + } + + /** + * Create a default config using {@link HttpVirtualGatewayFactory} for HTTP protocol. + * + * @param jks JSK instance, not null. + * @param interceptors A collection of {@link HttpInterceptorFactory}. + * @return A NetBare config instance. + */ + public static NetBareConfig defaultHttpConfig(@NonNull JKS jks, + List interceptors) { + return defaultConfig().newBuilder() + .setVirtualGatewayFactory(new HttpVirtualGatewayFactory(jks, interceptors)) + .build(); + } + + /** + * Create a default config. + * + * @return A NetBare config instance. + */ + public static NetBareConfig defaultConfig() { + return new Builder() + .dumpUid(false) + .setMtu(4096) + .setAddress(new IpAddress("10.1.10.1", 32)) + .setSession("NetBare") + .addRoute(new IpAddress("0.0.0.0", 0)) + .build(); + } + + /** + * Helper class to createServerEngine a VPN Service. + */ + public static class Builder { + + private NetBareConfig mConfig; + + public Builder() { + this.mConfig = new NetBareConfig(); + this.mConfig.routes = new HashSet<>(); + this.mConfig.dnsServers = new HashSet<>(); + this.mConfig.allowedApplications = new HashSet<>(); + this.mConfig.disallowedApplications = new HashSet<>(); + this.mConfig.allowedHosts = new HashSet<>(); + this.mConfig.disallowedHosts = new HashSet<>(); + } + + /** + * Set the name of this session. It will be displayed in system-managed dialogs and + * notifications. This is recommended not required. + * + * @param session Session name. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder setSession(@NonNull String session) { + mConfig.session = session; + return this; + } + + /** + * Set the {@link PendingIntent} to an activity for users to configure the VPN connection. + * If it is not set, the button to configure will not be shown in system-managed dialogs. + * + * @param intent An Activity intent. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder setConfigureIntent(@NonNull PendingIntent intent) { + mConfig.configureIntent = intent; + return this; + } + + /** + * Set the maximum transmission unit (MTU) of the VPN interface. If it is not set, the + * default value in the operating system will be used. + * + * @param mtu Maximum transmission unit (MTU). + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder setMtu(int mtu) { + mConfig.mtu = mtu; + return this; + } + + /** + * Convenience method to add a network address to the VPN interface using a numeric address + * string. See {@link InetAddress} for the definitions of numeric address formats. + * + * Adding an address implicitly allows traffic from that address family (i.e., IPv4 or IPv6) + * to be routed over the VPN. + * + * @param address IPv4 or IPv6 address. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder setAddress(@NonNull IpAddress address) { + mConfig.address = address; + return this; + } + + /** + * Add a network route to the VPN interface. Both IPv4 and IPv6 routes are supported. + * + * Adding a route implicitly allows traffic from that address family (i.e., IPv4 or IPv6) + * to be routed over the VPN. + * + * @param address IPv4 or IPv6 address. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder addRoute(@NonNull IpAddress address) { + mConfig.routes.add(address); + return this; + } + + /** + * Add a DNS server to the VPN connection. Both IPv4 and IPv6 addresses are supported. + * If none is set, the DNS servers of the default network will be used. + * + * Adding a server implicitly allows traffic from that address family (i.e., IPv4 or IPv6) + * to be routed over the VPN. + * + * @param address IPv4 or IPv6 address. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder addDnsServer(@NonNull String address) { + mConfig.dnsServers.add(address); + return this; + } + + /** + * Adds an application that's allowed to access the VPN connection. + * + * If this method is called at least once, only applications added through this method (and + * no others) are allowed access. Else (if this method is never called), all applications + * are allowed by default. If some applications are added, other, un-added applications + * will use networking as if the VPN wasn't running. + * + * @param packageName The full name (e.g.: "com.google.apps.contacts") of an application. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder addAllowedApplication(@NonNull String packageName) { + mConfig.allowedApplications.add(packageName); + return this; + } + + /** + * Adds an application that's denied access to the VPN connection. + * + * By default, all applications are allowed access, except for those denied through this + * method. Denied applications will use networking as if the VPN wasn't running. + * + * @param packageName The full name (e.g.: "com.google.apps.contacts") of an application. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder addDisallowedApplication(@NonNull String packageName) { + mConfig.disallowedApplications.add(packageName); + return this; + } + + /** + * Adds an ip host or a domain host that's allowed to capture. + * + * @param host An ip host or a domain host, not support the domain host. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder addAllowedHost(@NonNull String host) { + mConfig.allowedHosts.add(host); + return this; + } + + /** + * Adds an ip host or a domain host that's denied access to capture. + * + * @param host An ip host or a domain host, not support the domain host. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder addDisallowedHost(@NonNull String host) { + mConfig.disallowedHosts.add(host); + return this; + } + + /** + * Set the factory of gateway, the gateway will handle some intercepted actions before the + * server and client received the final data. + * + * @param gatewayFactory A factory of gateway. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder setVirtualGatewayFactory(VirtualGatewayFactory gatewayFactory) { + mConfig.gatewayFactory = gatewayFactory; + return this; + } + + /** + * Dump the uid of the session, you can get the value from {@link Session#uid}. This config + * will cost much battery. + * + * @param dumpUid Should dump session's uid from /proc/net/ + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder dumpUid(boolean dumpUid) { + mConfig.dumpUid = dumpUid; + return this; + } + + /** + * Exclude all net packets of the app self, this config is associated with {@link #dumpUid}. + * If the config of dumpUid is false, the excludeSelf will be forced to false too. + * + * @param excludeSelf Should exclude all net packets of the app self. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder excludeSelf(boolean excludeSelf) { + mConfig.excludeSelf = excludeSelf; + return this; + } + + /** + * Sets an uid provider. + * + * @param provider This interface provides a known uid for a session. + * @return this {@link Builder} object to facilitate chaining method calls. + */ + public Builder setUidProvider(UidProvider provider) { + mConfig.uidProvider = provider; + return this; + } + + /** + * Create the instance of {@link NetBareConfig}. + * + * @return The instance of {@link NetBareConfig}. + */ + public NetBareConfig build() { + return mConfig; + } + + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java new file mode 100644 index 0000000..0c547d0 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareListener.java @@ -0,0 +1,38 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +/** + * Interface definition for a callback to be invoked when the NetBare service state changes. + * + * @author Megatron King + * @since 2018-10-11 19:44 + */ +public interface NetBareListener { + + /** + * Callback method to be invoked when the NetBare service is started. It usual is called after + * {@link NetBare#start(NetBareConfig)}. + */ + void onServiceStarted(); + + /** + * Callback method to be invoked when the NetBare service is stopped. It usual is called after + * {@link NetBare#stop()} or another VPN service is established. + */ + void onServiceStopped(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java new file mode 100644 index 0000000..d9aff21 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareLog.java @@ -0,0 +1,169 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.util.Log; + +/** + * A static log util using in NetBare, and the tag is 'NetBare'; + * + * @author Megatron King + * @since 2018-10-08 23:12 + */ +public final class NetBareLog { + + private static final String TAG = "NetBare"; + + private static boolean sDebug; + + private NetBareLog() { + } + + /* package */ static void setDebug(boolean debug) { + sDebug = debug; + } + + /** + * Print a verbose level log in console. + * + * @param msg The message you would like logged. + */ + public static void v(String msg) { + if (!sDebug || msg == null) { + return; + } + Log.v(TAG, msg); + } + + /** + * Print a verbose level log in console. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public static void v(String msg, Object... args) { + v(format(msg, args)); + } + + /** + * Print a debug level log in console. + * + * @param msg The message you would like logged. + */ + public static void d(String msg) { + if (!sDebug || msg == null) { + return; + } + Log.d(TAG, msg); + } + + /** + * Print a debug level log in console. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public static void d(String msg, Object... args) { + d(format(msg, args)); + } + + /** + * Print a info level log in console. + * + * @param msg The message you would like logged. + */ + public static void i(String msg) { + if (!sDebug || msg == null) { + return; + } + Log.i(TAG, msg); + } + + /** + * Print a info level log in console. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public static void i(String msg, Object... args) { + i(format(msg, args)); + } + + /** + * Print a error level log in console. + * + * @param msg The message you would like logged. + */ + public static void e(String msg) { + if (!sDebug || msg == null) { + return; + } + Log.e(TAG, msg); + } + + /** + * Print a error level log in console. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public static void e(String msg, Object... args) { + e(format(msg, args)); + } + + /** + * Print a warning level log in console. + * + * @param msg The message you would like logged. + */ + public static void w(String msg) { + if (!sDebug || msg == null) { + return; + } + Log.w(TAG, msg); + } + + /** + * Print a warning level log in console. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public static void w(String msg, Object... args) { + w(format(msg, args)); + } + + /** + * Print a fatal level log in console. + * + * @param throwable The error you would like logged. + */ + public static void wtf(Throwable throwable) { + if (!sDebug || throwable == null) { + return; + } + Log.wtf(TAG, throwable); + } + + private static String format(String format, Object... objs) { + if (objs == null || objs.length == 0) { + return format; + } else { + return String.format(format, objs); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java new file mode 100644 index 0000000..a4edb1a --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareService.java @@ -0,0 +1,121 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Intent; +import android.net.VpnService; +import androidx.annotation.NonNull; + +/** + * Base class for NetBare services. + *

+ * NetBare service is an implement of {@link VpnService}, it establishes a vpn connection to + * route incoming and outgoing net packets. The NetBare service are forced to display a notification + * due to intercepting packets raises huge security concerns. + *

+ *

+ * The NetBare service is managed by {@link NetBare}, and you can use {@link NetBareListener} to + * observe the state. + *

+ * + * @author Megatron King + * @since 2018-10-08 21:09 + */ +public abstract class NetBareService extends VpnService { + + /** + * Start capturing target app's net packets. + */ + public static final String ACTION_START = + "com.github.megatronking.netbare.action.Start"; + + /** + * Stop capturing target app's net packets. + */ + public static final String ACTION_STOP = + "com.github.megatronking.netbare.action.Stop"; + + /** + * The identifier for this notification as per + * {@link NotificationManager#notify(int, Notification)}; must not be 0. + * + * @return The identifier + */ + protected abstract int notificationId(); + + /** + * A {@link Notification} object describing what to show the user. Must not be null. + * + * @return The Notification to be displayed. + */ + @NonNull + protected abstract Notification createNotification(); + + private NetBareThread mNetBareThread; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { + return START_NOT_STICKY; + } + String action = intent.getAction(); + if (ACTION_START.equals(action)) { + startNetBare(); + startForeground(notificationId(), createNotification()); + } else if (ACTION_STOP.equals(action)) { + stopNetBare(); + stopForeground(true); + stopSelf(); + } else { + stopSelf(); + } + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopNetBare(); + stopForeground(true); + } + + private void startNetBare() { + // Terminate previous service. + stopNetBare(); + + NetBareConfig config = NetBare.get().getConfig(); + if (config == null) { + throw new IllegalArgumentException("Must start NetBareService with a " + + "NetBareConfig"); + } + + NetBareLog.i("Start NetBare service!"); + mNetBareThread = new NetBareThread(this, config); + mNetBareThread.start(); + } + + private void stopNetBare() { + if (mNetBareThread == null) { + return; + } + NetBareLog.i("Stop NetBare service!"); + mNetBareThread.interrupt(); + mNetBareThread = null; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java new file mode 100644 index 0000000..cfb9a1a --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareThread.java @@ -0,0 +1,235 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.content.pm.PackageManager; +import android.net.VpnService; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; + +import com.github.megatronking.netbare.ip.IpAddress; +import com.github.megatronking.netbare.ip.IpHeader; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.net.UidDumper; +import com.github.megatronking.netbare.proxy.IcmpProxyServerForwarder; +import com.github.megatronking.netbare.proxy.ProxyServerForwarder; +import com.github.megatronking.netbare.proxy.TcpProxyServerForwarder; +import com.github.megatronking.netbare.proxy.UdpProxyServerForwarder; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A work thread running NetBare core logic. NetBase established the VPN connection is this thread + * and read packets from the VPN file descriptor and transfer them to local proxy servers. Every + * IP protocol runs an independent local proxy server to receive the packets. + * + * @author Megatron King + * @since 2018-10-08 19:38 + */ +/* package */ final class NetBareThread extends Thread { + + private static final int TRANSPORT_WAIT_TIME = 5; + + private final NetBareConfig mConfig; + private final VpnService mVpnService; + + private boolean mRunning; + + /* package */ NetBareThread(VpnService vpnService, NetBareConfig config) { + super("NetBare"); + this.mVpnService = vpnService; + this.mConfig = config; + } + + @Override + public void start() { + mRunning = true; + super.start(); + } + + @Override + public void interrupt() { + mRunning = false; + super.interrupt(); + } + + @Override + public void run() { + super.run(); + if (!mRunning) { + return; + } + + // Notify NetBareListener that the service is started now. + NetBare.get().notifyServiceStarted(); + + PacketsTransfer packetsTransfer = null; + try { + packetsTransfer = new PacketsTransfer(mVpnService, mConfig); + } catch (IOException e) { + NetBareLog.wtf(e); + } + if (packetsTransfer != null) { + // Establish VPN, it runs a while loop unless failed. + establishVpn(packetsTransfer); + } + + // Notify NetBareListener that the service is stopped now. + NetBare.get().notifyServiceStopped(); + + } + + private void establishVpn(PacketsTransfer packetsTransfer) { + VpnService.Builder builder = mVpnService.new Builder(); + builder.setMtu(mConfig.mtu); + builder.addAddress(mConfig.address.address, mConfig.address.prefixLength); + if (mConfig.session != null) { + builder.setSession(mConfig.session); + } + if (mConfig.configureIntent != null) { + builder.setConfigureIntent(mConfig.configureIntent); + } + for (IpAddress ip : mConfig.routes) { + builder.addRoute(ip.address, ip.prefixLength); + } + for (String address : mConfig.dnsServers) { + builder.addDnsServer(address); + } + try { + for (String packageName : mConfig.allowedApplications) { + builder.addAllowedApplication(packageName); + } + for (String packageName : mConfig.disallowedApplications) { + builder.addDisallowedApplication(packageName); + } + // Add self to allowed list. + if (!mConfig.allowedApplications.isEmpty()) { + builder.addAllowedApplication(mVpnService.getPackageName()); + } + } catch (PackageManager.NameNotFoundException e) { + NetBareLog.wtf(e); + } + ParcelFileDescriptor vpnDescriptor = builder.establish(); + if (vpnDescriptor == null) { + return; + } + + // Open io with the VPN descriptor. + FileDescriptor descriptor = vpnDescriptor.getFileDescriptor(); + if (descriptor == null) { + return; + } + InputStream input = new FileInputStream(descriptor); + OutputStream output = new FileOutputStream(descriptor); + int mtu = mConfig.mtu; + + packetsTransfer.start(); + + try { + // Read packets from input io and forward them to proxy servers. + while (mRunning) { + packetsTransfer.transfer(input, output, mtu); + } + } catch (IOException e) { + NetBareLog.wtf(e); + } + + packetsTransfer.stop(); + + NetBareUtils.closeQuietly(vpnDescriptor); + NetBareUtils.closeQuietly(input); + NetBareUtils.closeQuietly(output); + } + + private static class PacketsTransfer { + + private final UidDumper mUidDumper; + private final Map mForwarderRegistry; + + private PacketsTransfer(VpnService service, NetBareConfig config) throws IOException { + int mtu = config.mtu; + String localIp = config.address.address; + this.mUidDumper = config.dumpUid ? new UidDumper(localIp, config.uidProvider) : null; + // Register all supported protocols here. + this.mForwarderRegistry = new LinkedHashMap<>(3); + // TCP + this.mForwarderRegistry.put(Protocol.TCP, new TcpProxyServerForwarder(service, localIp, mtu, + mUidDumper)); + // UDP + this.mForwarderRegistry.put(Protocol.UDP, new UdpProxyServerForwarder(service, mtu, + mUidDumper)); + // ICMP + this.mForwarderRegistry.put(Protocol.ICMP, new IcmpProxyServerForwarder()); + } + + private void start() { + if (mUidDumper != null) { + mUidDumper.startDump(); + } + for (ProxyServerForwarder forwarder : mForwarderRegistry.values()) { + forwarder.prepare(); + } + } + + private void stop() { + for (ProxyServerForwarder forwarder : mForwarderRegistry.values()) { + forwarder.release(); + } + mForwarderRegistry.clear(); + if (mUidDumper != null) { + mUidDumper.stopDump(); + } + } + + private void transfer(InputStream input, OutputStream output, int mtu) throws IOException { + // The thread would be blocked if there is no outgoing packets from input stream. + byte[] packet = new byte[mtu]; + int readLength = input.read(packet); + if (readLength < 0) { + throw new IOException("Read -1 from vpn FileDescriptor."); + } + if (readLength == 0) { + SystemClock.sleep(TRANSPORT_WAIT_TIME); + return; + } + transfer(packet, readLength, output); + } + + private void transfer(byte[] packet, int len, OutputStream output) { + if (len < IpHeader.MIN_HEADER_LENGTH) { + NetBareLog.w("Ip header length < " + IpHeader.MIN_HEADER_LENGTH); + return; + } + IpHeader ipHeader = new IpHeader(packet, 0); + Protocol protocol = Protocol.parse(ipHeader.getProtocol()); + ProxyServerForwarder forwarder = mForwarderRegistry.get(protocol); + if (forwarder != null) { + forwarder.forward(packet, len, output); + } else { + NetBareLog.w("Unknown ip protocol: " + ipHeader.getProtocol()); + } + } + + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java new file mode 100644 index 0000000..e322239 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareUtils.java @@ -0,0 +1,153 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.text.TextUtils; + +import java.io.Closeable; +import java.io.IOException; + +/** + * A collection of assorted utility classes. + * + * @author Megatron King + * @since 2018-10-08 22:52 + */ +public final class NetBareUtils { + + /** + * Http line end (CRLF) symbol. + */ + public static final String LINE_END = "\r\n"; + + /** + * Http line end (CRLF) regex. + */ + public static final String LINE_END_REGEX = "\\r\\n"; + + /** + * A byte array of http line end (CRLF). + */ + public static final byte[] LINE_END_BYTES = LINE_END.getBytes(); + + /** + * Http double line end (CRLF), it separate the headers and body. + */ + public static final String PART_END = "\r\n\r\n"; + + /** + * A byte array of double http line end (CRLF). + */ + public static final byte[] PART_END_BYTES = "\r\n\r\n".getBytes(); + + /** + * Convert a int ip value to ipv4 string. + * + * @param ip The ip address. + * @return A ipv4 string value, format is N.N.N.N + */ + public static String convertIp(int ip) { + return String.format("%s.%s.%s.%s", (ip >> 24) & 0x00FF, + (ip >> 16) & 0x00FF, (ip >> 8) & 0x00FF, (ip & 0x00FF)); + } + + /** + * Convert a string ip value to int. + * + * @param ip The ip address. + * @return A int ip value. + */ + public static int convertIp(String ip) { + String[] arrayStrings = ip.split("\\."); + return (Integer.parseInt(arrayStrings[0]) << 24) + | (Integer.parseInt(arrayStrings[1]) << 16) + | (Integer.parseInt(arrayStrings[2]) << 8) + | (Integer.parseInt(arrayStrings[3])); + } + + /** + * Convert a short ip value to int. + * + * @param port The port. + * @return A int port value. + */ + public static int convertPort(short port) { + return port & 0xFFFF; + } + + /** + * Closes a closeable object or release resource. + * + * @param closeable A closeable object like io stream. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + NetBareLog.wtf(e); + } + } + } + + /** + * Parse a string to a integer value. If the string is not a integer value, this will return the + * default value. + * + * @param string The string value. + * @param defaultValue The default integer value. + * @return The integer value. + */ + public static int parseInt(String string, int defaultValue) { + int result = defaultValue; + + if (TextUtils.isEmpty(string)) { + return result; + } + + try { + result = Integer.parseInt(string); + } catch (Exception e) { + // parse error + } + return result; + } + + /** + * Parse a string to a integer value with a radix. If the string is not a integer value, this + * will return the default value. + * + * @param string The string value. + * @param radix The radix to be used. + * @param defaultValue The default integer value. + * @return The integer value. + */ + public static int parseInt(String string, int radix, int defaultValue) { + int result = defaultValue; + + if (TextUtils.isEmpty(string)) { + return result; + } + + try { + result = Integer.parseInt(string, radix); + } catch (Exception e) { + // parse error + } + return result; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java new file mode 100644 index 0000000..50e613f --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareVirtualGateway.java @@ -0,0 +1,334 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import android.os.Process; + +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.net.Session; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +/** + * The main virtual gateway used in proxy servers, it wraps the actual virtual gateway. We use this + * class to do some internal verifications. + * + * @author Megatron King + * @since 2018-11-17 23:10 + */ +public final class NetBareVirtualGateway extends VirtualGateway { + + /** + * Policy is indeterminate, we should resolve the policy before process data. + */ + private static final int POLICY_INDETERMINATE = 0; + + /** + * This policy allows data flow to configured virtual gateway. + */ + private static final int POLICY_ALLOWED = 1; + + /** + * This policy doesn't allow data flow to configured virtual gateway. + */ + private static final int POLICY_DISALLOWED = 2; + + private final VirtualGateway mGateway; + private final Session mSession; + private final NetBareXLog mLog; + + private int mPolicy; + + private boolean mRequestFinished; + private boolean mResponseFinished; + + public NetBareVirtualGateway(Session session, Request request, Response response) { + super(session, request, response); + mGateway = NetBare.get().getGatewayFactory().create(session, request, response); + mSession = session; + mLog = new NetBareXLog(session); + + NetBareConfig config = NetBare.get().getConfig(); + if (config == null || (config.excludeSelf && session.uid == Process.myUid())) { + // Exclude the app itself. + mLog.w("Exclude an app-self connection!"); + mPolicy = POLICY_DISALLOWED; + } else { + mPolicy = POLICY_INDETERMINATE; + } + } + + @Override + public void sendRequest(ByteBuffer buffer) throws IOException { + if (mRequestFinished) { + mLog.w("Drop a buffer due to request has finished."); + return; + } + resolvePolicyIfNecessary(buffer); + if (mPolicy == POLICY_ALLOWED) { + mGateway.sendRequest(buffer); + } else if (mPolicy == POLICY_DISALLOWED) { + super.sendRequest(buffer); + } + } + + @Override + public void sendResponse(ByteBuffer buffer) throws IOException { + if (mResponseFinished) { + mLog.w("Drop a buffer due to response has finished."); + return; + } + resolvePolicyIfNecessary(buffer); + if (mPolicy == POLICY_ALLOWED) { + mGateway.sendResponse(buffer); + } else if (mPolicy == POLICY_DISALLOWED) { + super.sendResponse(buffer); + } + } + + @Override + public void sendRequestFinished() { + if (mRequestFinished) { + return; + } + mLog.i("Gateway request finished!"); + mRequestFinished = true; + if (mPolicy == POLICY_ALLOWED) { + mGateway.sendRequestFinished(); + } else if (mPolicy == POLICY_DISALLOWED) { + super.sendRequestFinished(); + } + } + + @Override + public void sendResponseFinished() { + if (mResponseFinished) { + return; + } + mLog.i("Gateway response finished!"); + mResponseFinished = true; + if (mPolicy == POLICY_ALLOWED) { + mGateway.sendResponseFinished(); + } else if (mPolicy == POLICY_DISALLOWED) { + super.sendResponseFinished(); + } + } + + private void resolvePolicyIfNecessary(ByteBuffer buffer) { + if (mPolicy != POLICY_INDETERMINATE) { + // Resolved. + return; + } + if (!buffer.hasRemaining()) { + // Invalid buffer remaining, do nothing. + return; + } + if (mSession.protocol != Protocol.TCP) { + mPolicy = POLICY_ALLOWED; + return; + } + + // Now we verify the TCP protocol host + String domain; + if (isHttp(buffer)) { + domain = parseHttpHost(buffer.array(), buffer.position(), buffer.remaining()); + } else { + domain = parseHttpsHost(buffer.array(), buffer.position(), buffer.remaining()); + } + if (domain == null) { + // Maybe not http protocol. + mPolicy = POLICY_ALLOWED; + return; + } else { + mSession.host = domain; + } + NetBareConfig config = NetBare.get().getConfig(); + Set allowedHost = new HashSet<>(config.allowedHosts); + Set disallowedHost = new HashSet<>(config.disallowedHosts); + + boolean isAllowedHostEmpty = allowedHost.isEmpty(); + boolean isDisallowedHostEmpty = disallowedHost.isEmpty(); + + if (isAllowedHostEmpty && isDisallowedHostEmpty) { + // No white and black list, it means allow everything. + mPolicy = POLICY_ALLOWED; + return; + } + + if (!isDisallowedHostEmpty) { + // Check domain hosts. + for (String host : disallowedHost) { + if (host.equals(domain)) { + // Denied host. + mPolicy = POLICY_DISALLOWED; + return; + } + } + // Check ip hosts. + for (String host : disallowedHost) { + if (host.equals(NetBareUtils.convertIp(mSession.remoteIp))) { + // Denied host. + mPolicy = POLICY_DISALLOWED; + return; + } + } + } + + if (!isAllowedHostEmpty) { + for (String host : allowedHost) { + if (host.equals(domain)) { + mPolicy = POLICY_ALLOWED; + return; + } + } + for (String host : allowedHost) { + if (host.equals(NetBareUtils.convertIp(mSession.remoteIp))) { + mPolicy = POLICY_ALLOWED; + return; + } + } + mPolicy = POLICY_DISALLOWED; + } else { + mPolicy = POLICY_ALLOWED; + } + } + + private boolean isHttp(ByteBuffer buffer) { + switch (buffer.get(buffer.position())) { + // HTTP methods. + case 'G': + case 'H': + case 'P': + case 'D': + case 'O': + case 'T': + case 'C': + return true; + default: + // Unknown first byte data. + break; + } + return false; + } + + private String parseHttpHost(byte[] buffer, int offset, int size) { + String header = new String(buffer, offset, size); + String[] headers = header.split(NetBareUtils.LINE_END_REGEX); + if (headers.length <= 1) { + return null; + } + for (int i = 1; i < headers.length; i++) { + String requestHeader = headers[i]; + // Reach the header end + if (requestHeader.isEmpty()) { + return null; + } + String[] nameValue = requestHeader.split(":"); + if (nameValue.length < 2) { + return null; + } + String name = nameValue[0].trim(); + String value = requestHeader.replaceFirst(nameValue[0] + ": ", "").trim(); + if (name.toLowerCase().equals("host")) { + return value; + } + } + return null; + } + + private String parseHttpsHost(byte[] buffer, int offset, int size) { + int limit = offset + size; + // Client Hello + if (size <= 43 || buffer[offset] != 0x16) { + mLog.w("Failed to get host from SNI: Bad ssl packet."); + return null; + } + // Skip 43 byte header + offset += 43; + + // Read sessionID + if (offset + 1 > limit) { + mLog.w("Failed to get host from SNI: No session id."); + return null; + } + int sessionIDLength = buffer[offset++] & 0xFF; + offset += sessionIDLength; + + // Read cipher suites + if (offset + 2 > limit) { + mLog.w("Failed to get host from SNI: No cipher suites."); + return null; + } + + int cipherSuitesLength = readShort(buffer, offset) & 0xFFFF; + offset += 2; + offset += cipherSuitesLength; + + // Read Compression method. + if (offset + 1 > limit) { + mLog.w("Failed to get host from SNI: No compression method."); + return null; + } + int compressionMethodLength = buffer[offset++] & 0xFF; + offset += compressionMethodLength; + + // Read Extensions + if (offset + 2 > limit) { + mLog.w("Failed to get host from SNI: no extensions."); + return null; + } + int extensionsLength = readShort(buffer, offset) & 0xFFFF; + offset += 2; + + if (offset + extensionsLength > limit) { + mLog.w("Failed to get host from SNI: no sni."); + return null; + } + + while (offset + 4 <= limit) { + int type0 = buffer[offset++] & 0xFF; + int type1 = buffer[offset++] & 0xFF; + int length = readShort(buffer, offset) & 0xFFFF; + offset += 2; + // Got the SNI info + if (type0 == 0x00 && type1 == 0x00 && length > 5) { + offset += 5; + length -= 5; + if (offset + length > limit) { + return null; + } + return new String(buffer, offset, length); + } else { + offset += length; + } + + } + mLog.w("Failed to get host from SNI: no host."); + return null; + } + + private short readShort(byte[] data, int offset) { + int r = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); + return (short) r; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java new file mode 100644 index 0000000..882ac79 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/NetBareXLog.java @@ -0,0 +1,174 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare; + +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.ip.Protocol; + +/** + * A log util using in NetBare, it uses the protocol, ip and port as the prefix. + * + * @author Megatron King + * @since 2018-10-14 10:25 + */ +public final class NetBareXLog { + + private final String mPrefix; + + /** + * Constructs a NetBareXLog instance with the net information. + * + * @param protocol The IP protocol. + * @param ip The ip address, a string value. + * @param port The port, a short value. + */ + public NetBareXLog(Protocol protocol, String ip, short port) { + this(protocol, ip, NetBareUtils.convertPort(port)); + } + + /** + * Constructs a NetBareXLog instance with the net information. + * + * @param protocol The IP protocol. + * @param ip The ip address, a int value. + * @param port The port, a short value. + */ + public NetBareXLog(Protocol protocol, int ip, short port) { + this(protocol, NetBareUtils.convertIp(ip), port); + } + + /** + * Constructs a NetBareXLog instance with the net information. + * + * @param protocol The IP protocol. + * @param ip The ip address, a int value. + * @param port The port, a int value. + */ + public NetBareXLog(Protocol protocol, int ip, int port) { + this(protocol, NetBareUtils.convertIp(ip), port); + } + + /** + * Constructs a NetBareXLog instance with the net information. + * + * @param session The session contains net information. + */ + public NetBareXLog(Session session) { + this(session.protocol, session.remoteIp, session.remotePort); + } + + /** + * Constructs a NetBareXLog instance with the net information. + * + * @param protocol The IP protocol. + * @param ip The ip address, a string value. + * @param port The port, a int value. + */ + public NetBareXLog(Protocol protocol, String ip, int port) { + this.mPrefix = "[" + protocol.name() + "][" + ip + ":" + port + "]"; + } + + /** + * Print a verbose level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + */ + public void v(String msg) { + NetBareLog.v(mPrefix + msg); + } + + + public void v(String msg, Object... args) { + NetBareLog.v(mPrefix + msg, args); + } + + /** + * Print a debug level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + */ + public void d(String msg) { + NetBareLog.d(mPrefix + msg); + } + + /** + * Print a verbose level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public void d(String msg, Object... args) { + NetBareLog.d(mPrefix + msg, args); + } + + /** + * Print a info level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + */ + public void i(String msg) { + NetBareLog.i(mPrefix + msg); + } + + /** + * Print a info level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public void i(String msg, Object... args) { + NetBareLog.i(mPrefix + msg, args); + } + + /** + * Print a error level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + */ + public void e(String msg) { + NetBareLog.e(mPrefix + msg); + } + + /** + * Print a error level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public void e(String msg, Object... args) { + NetBareLog.e(mPrefix + msg, args); + } + + /** + * Print a warning level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + */ + public void w(String msg) { + NetBareLog.w(mPrefix + msg); + } + + /** + * Print a warning level log in console, format is '[protocol][ip:port]message'. + * + * @param msg The message you would like logged. + * @param args Arguments referenced by the format specifiers in the format string. + */ + public void w(String msg, Object... args) { + NetBareLog.w(mPrefix + msg, args); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java new file mode 100644 index 0000000..5cab425 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGateway.java @@ -0,0 +1,70 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.net.Session; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link VirtualGateway} provides the interception service. Interceptors are organized as a list + * in chain, can observe and modify packets. Use {@link DefaultVirtualGatewayFactory} to create an + * instance. + * + * @author Megatron King + * @since 2018-11-01 23:35 + */ +/* package */ class DefaultVirtualGateway extends VirtualGateway { + + private final List mInterceptors; + + /* package */ DefaultVirtualGateway(Session session, Request request, Response response, + List factories) { + super(session, request, response); + this.mInterceptors = new ArrayList<>(factories.size()); + for (InterceptorFactory factory : factories) { + mInterceptors.add(factory.create()); + } + } + + @Override + public void sendRequest(ByteBuffer buffer) throws IOException { + new RequestChain(mRequest, mInterceptors).process(buffer); + } + + @Override + public void sendResponse(ByteBuffer buffer) throws IOException { + new ResponseChain(mResponse, mInterceptors).process(buffer); + } + + @Override + public void sendRequestFinished() { + for (Interceptor interceptor: mInterceptors) { + interceptor.onRequestFinished(mRequest); + } + } + + @Override + public void sendResponseFinished() { + for (Interceptor interceptor: mInterceptors) { + interceptor.onResponseFinished(mResponse); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java new file mode 100644 index 0000000..d1c70eb --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/DefaultVirtualGatewayFactory.java @@ -0,0 +1,64 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.net.Session; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link VirtualGatewayFactory} that produces the {@link DefaultVirtualGateway}. + * + * @author Megatron King + * @since 2018-11-01 23:29 + */ +public final class DefaultVirtualGatewayFactory implements VirtualGatewayFactory { + + private List mFactories; + + private DefaultVirtualGatewayFactory(@NonNull List factories) { + this.mFactories = factories; + } + + @Override + public VirtualGateway create(Session session, Request request, Response response) { + return new DefaultVirtualGateway(session, request, response, new ArrayList<>(mFactories)); + } + + /** + * Create a {@link VirtualGatewayFactory} instance with a collection of + * {@link InterceptorFactory}. + * + * @param factories a collection of {@link InterceptorFactory}. + * @return A instance of {@link DefaultVirtualGatewayFactory}. + */ + public static VirtualGatewayFactory create(@NonNull List factories) { + return new DefaultVirtualGatewayFactory(factories); + } + + /** + * Create a {@link VirtualGatewayFactory} instance that not contains {@link Interceptor}. + * + * @return A instance of {@link VirtualGatewayFactory}. + */ + public static VirtualGatewayFactory create() { + return create(new ArrayList()); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java new file mode 100644 index 0000000..b340bab --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Interceptor.java @@ -0,0 +1,80 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A virtual gateway interceptor, observes and modifies requests/responses. Interceptors are + * organized by a virtual gateway, and process net packets one by one. + *

+ * Methods are thread-safety due to interceptors are running in the local proxy server threads. + *

+ *

+ * Use {@link InterceptorFactory} to create an interceptor instance. + *

+ * @author Megatron King + * @since 2018-11-13 23:46 + */ +public interface Interceptor { + + /** + * Intercept request packet, and delivery it to next interceptor or the terminal. + *

+ * Remember do not block this method for a long time, because all the connections share the + * same thread. + *

+ * + * @param chain The request chain, call {@linkplain RequestChain#process(ByteBuffer)} to + * delivery the packet. + * @param buffer A nio buffer contains the packet data. + * @throws IOException If an I/O error has occurred. + */ + void intercept(@NonNull RequestChain chain, @NonNull ByteBuffer buffer) throws IOException; + + /** + * Intercept request packet, and delivery it to next interceptor or the terminal. + *

+ * Remember do not block this method for a long time, because all the connections share the + * same thread. + * + * @param chain The response chain, call {@linkplain ResponseChain#process(ByteBuffer)} to + * delivery the packet. + * @param buffer A nio buffer contains the packet data. + * @throws IOException If an I/O error has occurred. + */ + void intercept(@NonNull ResponseChain chain, @NonNull ByteBuffer buffer) throws IOException; + + /** + * Invoked when a session's request has finished. It means the client has no more data sent to + * server in this session, and it might invoked multi times if a connection is keep-alive. + * + * @param request The request. + */ + void onRequestFinished(@NonNull Request request); + + /** + * Invoked when a session's response has finished. It means the server has no more data sent to + * client in this session, and it might invoked multi times if a connection is keep-alive. + * + * @param response The response. + */ + void onResponseFinished(@NonNull Response response); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java new file mode 100644 index 0000000..6f51b47 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorChain.java @@ -0,0 +1,99 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A chain with multiple {@link Interceptor} in series. The interceptors process net packets one by + * one, and send the modified packets to tunnel in the end. + * + * @param An implementation of {@link TunnelFlow}, responsible for sending data to tunnel. + * @param An implementation of {@link Interceptor}. + * + * @author Megatron King + * @since 2018-11-13 23:00 + */ +public abstract class InterceptorChain { + + private T mFlow; + private List mInterceptors; + private int mIndex; + + /** + * Hand the net packets to the next {@link Interceptor}. + * + * @param buffer A buffer contains net packet data. + * @param flow A {@link TunnelFlow} implementation. + * @param interceptors A collection of all interceptors in chain. + * @param index The next interceptor index. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void processNext(ByteBuffer buffer, T flow, List interceptors, int index) + throws IOException; + + /** + * Constructs an intercept chain with a tunnel flow instance and a collection of interceptors. + * + * @param flow A {@link TunnelFlow} implementation. + * @param interceptors A collection of interceptors. + */ + public InterceptorChain(T flow, List interceptors) { + this(flow, interceptors, 0); + } + + /** + * Constructs a new intercept chain with the tunnel flow instance and a collection of + * interceptors. The chain will start from the given index. + * + * @param flow A {@link TunnelFlow} implementation. + * @param interceptors A collection of interceptors. + * @param index The head index. + */ + public InterceptorChain(T flow, List interceptors, int index) { + this.mFlow = flow; + this.mInterceptors = interceptors; + this.mIndex = index; + } + + /** + * Finish the interception and send the packet to tunnel. + * + * @param buffer A buffer contains net packet data. + * @throws IOException If an I/O error has occurred. + */ + public void processFinal(ByteBuffer buffer) throws IOException { + mFlow.process(buffer); + } + + /** + * Hand the net packets to the next. If all interceptors have been processed, the packets will + * be sent to tunnel, otherwise hand it to the next interceptor. + * + * @param buffer A buffer contains net packet data. + * @throws IOException If an I/O error has occurred. + */ + public void process(ByteBuffer buffer) throws IOException { + if (mIndex >= mInterceptors.size()) { + processFinal(buffer); + } else { + processNext(buffer, mFlow, mInterceptors, mIndex); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java new file mode 100644 index 0000000..48173d0 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/InterceptorFactory.java @@ -0,0 +1,36 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import androidx.annotation.NonNull; + +/** + * Factory used by developer to create their own interceptor for virtual gateway. + * + * @author Megatron King + * @since 2018-11-02 23:46 + */ +public interface InterceptorFactory { + + /** + * Creates an interceptor instance and immediately returns it, it must not be null. + * + * @return A newly created interceptor. + */ + @NonNull + Interceptor create(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java new file mode 100644 index 0000000..c99e3b1 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Request.java @@ -0,0 +1,48 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.tunnel.Tunnel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A client requester, it connects to the remote server tunnel directly. We can send packet to the + * remote server using {@link #process(ByteBuffer)}. + * + * @author Megatron King + * @since 2018-11-05 22:18 + */ +public class Request extends SessionTunnelFlow { + + private Tunnel mTunnel; + + public Request() { + } + + public Request(Tunnel tunnel) { + this.mTunnel = tunnel; + } + + @Override + public void process(ByteBuffer buffer) throws IOException { + if (mTunnel != null) { + mTunnel.write(buffer); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java new file mode 100644 index 0000000..8ac3813 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/RequestChain.java @@ -0,0 +1,58 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A request chain, responsible for intercepting request packets. + * + * @author Megatron King + * @since 2018-11-14 23:18 + */ +public class RequestChain extends InterceptorChain { + + private Request mRequest; + + /* package */ RequestChain(Request request, List interceptors) { + super(request, interceptors); + mRequest = request; + } + + private RequestChain(Request request, List interceptors, int index) { + super(request, interceptors, index); + mRequest = request; + } + + @Override + protected void processNext(ByteBuffer buffer, Request request, List interceptors, + int index) throws IOException { + Interceptor interceptor = interceptors.get(index); + if (interceptor != null) { + interceptor.intercept(new RequestChain(request, interceptors, ++index), buffer); + } + } + + @NonNull + public Request request() { + return mRequest; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java new file mode 100644 index 0000000..5fc46fb --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/Response.java @@ -0,0 +1,48 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.tunnel.Tunnel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A server response, it connects to VPN file descriptor. We can send packet to the client using + * {@link #process(ByteBuffer)}. + * + * @author Megatron King + * @since 2018-11-05 22:24 + */ +public class Response extends SessionTunnelFlow { + + private Tunnel mTunnel; + + public Response() { + } + + public Response(Tunnel tunnel) { + this.mTunnel = tunnel; + } + + @Override + public void process(ByteBuffer buffer) throws IOException { + if (mTunnel != null) { + mTunnel.write(buffer); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java new file mode 100644 index 0000000..0b65f5c --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/ResponseChain.java @@ -0,0 +1,58 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A response chain, responsible for intercepting response packets. + * + * @author Megatron King + * @since 2018-11-14 23:19 + */ +public class ResponseChain extends InterceptorChain { + + private Response mResponse; + + /* package */ ResponseChain(Response response, List interceptors) { + super(response, interceptors); + mResponse = response; + } + + private ResponseChain(Response response, List interceptors, int index) { + super(response, interceptors, index); + mResponse = response; + } + + @Override + protected void processNext(ByteBuffer buffer, Response response, List interceptors, + int index) throws IOException { + Interceptor interceptor = interceptors.get(index); + if (interceptor != null) { + interceptor.intercept(new ResponseChain(response, interceptors, ++index), buffer); + } + } + + @NonNull + public Response response() { + return mResponse; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java new file mode 100644 index 0000000..9d4bfa8 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SessionTunnelFlow.java @@ -0,0 +1,101 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.NetBareConfig; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.net.Session; + +/** + * A tunnel flow contains the session information. + * + * @author Megatron King + * @since 2018-11-05 21:43 + */ +public abstract class SessionTunnelFlow implements TunnelFlow { + + private Session mSession; + + /* package */ void setSession(Session session) { + mSession = session; + } + + /** + * Returns the session's unique id. + * + * @return The session id. + */ + public String id() { + return mSession.id; + } + + /** + * Returns the session created time, you can think of it as the start time of the request. + * + * @return Session created time. + */ + public long time() { + return mSession.time; + } + + /** + * Returns the identifier of this session's process uid. This value is not guaranteed, it is up + * to {@link NetBareConfig#dumpUid}. And if dumps the uid failed, it will return 0. + * + * @return The session's process uid. + */ + public int uid() { + return mSession.uid; + } + + /** + * Returns the remote server's IPV4 address. + * + * @return The remote server's IPV4 address. + */ + public String ip() { + return NetBareUtils.convertIp(mSession.remoteIp); + } + + /** + * Returns the remote server's host name. + * + * @return The remote server's host name. + */ + public String host() { + return mSession.host; + } + + /** + * Returns the remote server's port. + * + * @return The remote server's port. + */ + public int port() { + return NetBareUtils.convertPort(mSession.remotePort); + } + + /** + * Returns the IP protocol. + * + * @return IP protocol. + */ + public Protocol protocol() { + return mSession.protocol; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java new file mode 100644 index 0000000..bc8ed2a --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/SpecVirtualGateway.java @@ -0,0 +1,102 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.net.Session; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * The spec VirtualGateway filter the net packets by {@link Protocol}. + * + * @author Megatron King + * @since 2018-11-03 10:34 + */ +public abstract class SpecVirtualGateway extends VirtualGateway { + + private final boolean mIsSpec; + + public SpecVirtualGateway(Protocol protocol, Session session, Request request, + Response response) { + super(session, request, response); + this.mIsSpec = protocol == session.protocol; + } + + /** + * The specific protocol packets sent to server will flow through this method. + * + * @param buffer A byte buffer contains the net packet data. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void onSpecRequest(ByteBuffer buffer) throws IOException; + + /** + * The specific protocol packets sent to client will flow through this method. + * + * @param buffer A byte buffer contains the net packet data. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void onSpecResponse(ByteBuffer buffer) throws IOException; + + /** + * Notify virtual gateway that no longer has data sent to the server. + */ + protected abstract void onSpecRequestFinished(); + + /** + * Notify virtual gateway that no longer has data sent to the client. + */ + protected abstract void onSpecResponseFinished(); + + @Override + public final void sendRequest(ByteBuffer buffer) throws IOException { + if (mIsSpec) { + onSpecRequest(buffer); + } else { + super.sendRequest(buffer); + } + } + + @Override + public final void sendResponse(ByteBuffer buffer) throws IOException { + if (mIsSpec) { + onSpecResponse(buffer); + } else { + super.sendResponse(buffer); + } + } + + @Override + public final void sendRequestFinished() { + if (mIsSpec) { + onSpecRequestFinished(); + } else { + super.sendRequestFinished(); + } + } + + @Override + public final void sendResponseFinished() { + if (mIsSpec) { + onSpecResponseFinished(); + } else { + super.sendResponseFinished(); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java new file mode 100644 index 0000000..3427dee --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/TunnelFlow.java @@ -0,0 +1,37 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A tunnel flow interface, the implement class should bind a tunnel. + * + * @author Megatron King + * @since 2018-11-05 20:20 + */ +/* package */ interface TunnelFlow { + + /** + * Send a packet to remote tunnel, and the tunnel will send it to the terminal. + * + * @param buffer A net packet buffer. + * @throws IOException If an I/O error has occurred. + */ + void process(ByteBuffer buffer) throws IOException; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java new file mode 100644 index 0000000..a89b368 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGateway.java @@ -0,0 +1,92 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.net.Session; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Virtual Gateway is a virtual net packets interception distributor, all packets will flow through + * it. We can define our own virtual gateway to decode and encode the packets. The Virtual + * Gateway wraps a request tunnel {@link Request} and a response tunnel {@link Response}, these + * tunnels are responsible for communicating with the terminal(client and server). + * + * @author Megatron King + * @since 2018-11-01 23:48 + */ +public class VirtualGateway { + + /** + * The request tunnel connects to the server terminal. We can call + * {@link Request#process(ByteBuffer)} to send data. + */ + protected Request mRequest; + + /** + * The response tunnel connects to the client terminal. We can call + * {@link Response#process(ByteBuffer)} to send data. + */ + protected Response mResponse; + + /** + * Constructs a VirtualGateway object with the net session, request tunnel and response tunnel. + * + * @param session The net session contains basic net information such as IPs and ports. + * @param request The request tunnel connects to the server terminal. + * @param response The response tunnel connects to the client terminal. + */ + public VirtualGateway(Session session, Request request, Response response) { + request.setSession(session); + response.setSession(session); + this.mRequest = request; + this.mResponse = response; + } + + /** + * Send a packet to server terminal through the request tunnel. + * + * @param buffer A byte buffer contains the net packet data. + * @throws IOException If an I/O error has occurred. + */ + public void sendRequest(ByteBuffer buffer) throws IOException { + mRequest.process(buffer); + } + + /** + * Send a packet to client terminal through the response tunnel. + * + * @param buffer A byte buffer contains the net packet data. + * @throws IOException If an I/O error has occurred. + */ + public void sendResponse(ByteBuffer buffer) throws IOException { + mResponse.process(buffer); + } + + /** + * Notify virtual gateway that no longer has data sent to the server. + */ + public void sendRequestFinished() { + } + + /** + * Notify virtual gateway that no longer has data sent to the client. + */ + public void sendResponseFinished() { + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java new file mode 100644 index 0000000..bdfb9cc --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/gateway/VirtualGatewayFactory.java @@ -0,0 +1,37 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.gateway; + +import com.github.megatronking.netbare.net.Session; + +/** + * A factory that produces the {@link VirtualGateway}. + * + * @author Megatron King + * @since 2018-11-01 23:23 + */ +public interface VirtualGatewayFactory { + + /** + * Returns a new {@link VirtualGateway} for the given arguments. + * + * @param session A network session. + * @param request A request connects to the remote server tunnel. + * @param response A response connects to VPN file descriptor + */ + VirtualGateway create(Session session, Request request, Response response); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java new file mode 100644 index 0000000..0c8e07f --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/ContainerHttpInterceptor.java @@ -0,0 +1,196 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * One http virtual gateway may have multi http sessions, but we don't want to share interceptors + * between them. Use a container to manage different sessions, every session has independent + * interceptor instances. + * + * @author Megatron King + * @since 2019/1/6 16:13 + */ +/* package */ class ContainerHttpInterceptor extends HttpInterceptor { + + private final Map mSessions; + private final HttpInterceptorsFactory mSubInterceptorsFactory; + + /* package */ ContainerHttpInterceptor(HttpInterceptorsFactory factory) { + this.mSubInterceptorsFactory = factory; + this.mSessions = new ConcurrentHashMap<>(); + } + + @Override + protected void intercept(@NonNull final HttpRequestChain chain, @NonNull ByteBuffer buffer) + throws IOException { + HttpRequest request = chain.request(); + Session session = findSessionById(request.id()); + session.request = request; + if (session.interceptors == null) { + session.interceptors = mSubInterceptorsFactory.create(); + } + new HttpContainerRequestChain(chain, session.interceptors).process(buffer); + } + + @Override + protected void intercept(@NonNull final HttpResponseChain chain, @NonNull ByteBuffer buffer) + throws IOException { + HttpResponse response = chain.response(); + Session session = findSessionById(response.id()); + session.response = response; + if (session.interceptors == null) { + session.interceptors = mSubInterceptorsFactory.create(); + } + new HttpContainerResponseChain(chain, session.interceptors).process(buffer); + } + + @Override + protected void onRequestFinished(@NonNull HttpRequest request) { + if (request instanceof HttpZygoteRequest) { + // This means the connection is down, finish all. + for (Session session : mSessions.values()) { + if (session.request != null && session.interceptors != null) { + for (HttpInterceptor interceptor : session.interceptors) { + interceptor.onRequestFinished(session.request); + } + } + } + mSessions.clear(); + } else { + Session session = mSessions.remove(request.id()); + if (session != null && session.interceptors != null) { + for (HttpInterceptor interceptor : session.interceptors) { + interceptor.onRequestFinished(session.request); + } + } + } + } + + @Override + protected void onResponseFinished(@NonNull HttpResponse response) { + if (response instanceof HttpZygoteResponse) { + // This means the connection is down, finish all. + for (Session session : mSessions.values()) { + if (session != null && session.response != null && session.interceptors != null) { + for (HttpInterceptor interceptor : session.interceptors) { + interceptor.onResponseFinished(session.response); + } + } + } + } else { + Session session = mSessions.remove(response.id()); + if (session != null && session.interceptors != null) { + for (HttpInterceptor interceptor : session.interceptors) { + interceptor.onResponseFinished(session.response); + } + } + } + } + + private Session findSessionById(String id) { + Session session; + if (mSessions.containsKey(id)) { + session = mSessions.get(id); + } else { + session = new Session(); + mSessions.put(id, session); + } + return session; + } + + private static final class Session { + + private HttpRequest request; + private HttpResponse response; + private List interceptors; + + } + + private static final class HttpContainerRequestChain extends HttpRequestChain { + + private final HttpRequestChain mChain; + private final List mInterceptors; + private final int mIndex; + + private HttpContainerRequestChain(HttpRequestChain chain, List interceptors) { + this(chain, interceptors, 0); + } + + private HttpContainerRequestChain(HttpRequestChain chain, List interceptors, + int index) { + super(chain.zygoteRequest(), interceptors, index); + this.mChain = chain; + this.mInterceptors = interceptors; + this.mIndex = index; + } + + @Override + public void process(ByteBuffer buffer) throws IOException { + if (mIndex >= mInterceptors.size()) { + mChain.process(buffer); + } else { + HttpInterceptor interceptor = mInterceptors.get(mIndex); + if (interceptor != null) { + interceptor.intercept(new HttpContainerRequestChain(mChain, mInterceptors, + mIndex + 1), buffer); + } + } + } + + } + + private static final class HttpContainerResponseChain extends HttpResponseChain { + + private final HttpResponseChain mChain; + private final List mInterceptors; + private final int mIndex; + + private HttpContainerResponseChain(HttpResponseChain chain, List interceptors) { + this(chain, interceptors, 0); + } + + private HttpContainerResponseChain(HttpResponseChain chain, List interceptors, + int index) { + super(chain.zygoteResponse(), interceptors, index); + this.mChain = chain; + this.mInterceptors = interceptors; + this.mIndex = index; + } + + @Override + public void process(ByteBuffer buffer) throws IOException { + if (mIndex >= mInterceptors.size()) { + mChain.process(buffer); + } else { + HttpInterceptor interceptor = mInterceptors.get(mIndex); + if (interceptor != null) { + interceptor.intercept(new HttpContainerResponseChain(mChain, mInterceptors, + mIndex + 1), buffer); + } + } + } + + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java new file mode 100644 index 0000000..5395640 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/Http2SniffInterceptor.java @@ -0,0 +1,110 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.http2.Http2; +import com.google.common.primitives.Bytes; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Verifies the HTTP packet and determines whether it is a HTTP2 protocol packets. + * + * @author Megatron King + * @since 2019/1/5 14:02 + */ +/* package */ class Http2SniffInterceptor extends HttpIndexInterceptor { + + private SSLRefluxCallback mCallback; + private NetBareXLog mLog; + + /* package */ Http2SniffInterceptor(SSLRefluxCallback callback) { + this.mCallback = callback; + } + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (index == 0) { + HttpRequest request = chain.request(); + if (mLog == null) { + mLog = new NetBareXLog(request.protocol(), request.ip(), request.port()); + } + // HTTP2 is forces to use SSL connection. + if (request.isHttps()) { + if (buffer.hasRemaining() && Bytes.indexOf(buffer.array(), + Http2.CONNECTION_PREFACE) == buffer.position()) { + mLog.i("Send a connection preface to remote server."); + request.session().protocol = HttpProtocol.HTTP_2; + if (buffer.remaining() == Http2.CONNECTION_PREFACE.length) { + // Skip preface frame data. + mCallback.onRequest(request, buffer); + return; + } else { + ByteBuffer prefaceBuffer = ByteBuffer.allocate(Http2.CONNECTION_PREFACE.length); + prefaceBuffer.put(Http2.CONNECTION_PREFACE); + prefaceBuffer.flip(); + mCallback.onRequest(request, prefaceBuffer); + // The remaining data continues. + buffer.position(buffer.position() + Http2.CONNECTION_PREFACE.length); + } + } + } + } + if (buffer.hasRemaining()) { + chain.process(buffer); + } + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (index == 0) { + HttpResponse response = chain.response(); + if (mLog == null) { + mLog = new NetBareXLog(response.protocol(), response.ip(), response.port()); + } + // HTTP2 is forces to use SSL connection. + if (response.isHttps()) { + if (buffer.hasRemaining() && Bytes.indexOf(buffer.array(), + Http2.CONNECTION_PREFACE) == buffer.position()) { + mLog.i("Receive a connection preface from remote server."); + response.session().protocol = HttpProtocol.HTTP_2; + if (buffer.remaining() == Http2.CONNECTION_PREFACE.length) { + // Skip preface frame data. + mCallback.onResponse(response, buffer); + return; + } else { + ByteBuffer prefaceBuffer = ByteBuffer.allocate(Http2.CONNECTION_PREFACE.length); + prefaceBuffer.put(Http2.CONNECTION_PREFACE); + prefaceBuffer.flip(); + mCallback.onResponse(response, prefaceBuffer); + // The remaining data continues. + buffer.position(buffer.position() + Http2.CONNECTION_PREFACE.length); + } + } + } + } + if (buffer.hasRemaining()) { + chain.process(buffer); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java new file mode 100644 index 0000000..05ec77e --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderParseInterceptor.java @@ -0,0 +1,186 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Parse HTTP request header part and response header part from HTTP packets. The parse result will + * be set to {@link HttpSession}. + * + * @author Megatron King + * @since 2018-12-09 12:19 + */ +/* package */ final class HttpHeaderParseInterceptor extends HttpIndexInterceptor { + + private NetBareXLog mLog; + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (index > 0) { + chain.process(buffer); + return; + } + if (mLog == null) { + mLog = new NetBareXLog(Protocol.TCP, chain.request().ip(), chain.request().port()); + } + parseRequestHeader(chain.request().session(), buffer); + chain.process(buffer); + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (index > 0) { + chain.process(buffer); + return; + } + if (mLog == null) { + mLog = new NetBareXLog(Protocol.TCP, chain.response().ip(), chain.response().port()); + } + parseResponseHeader(chain.response().session(), buffer); + chain.process(buffer); + } + + private void parseRequestHeader(HttpSession session, ByteBuffer buffer) { + session.reqBodyOffset = buffer.remaining(); + String headerString = new String(buffer.array(), buffer.position(), buffer.remaining()); + String[] headers = headerString.split(NetBareUtils.LINE_END_REGEX); + String[] requestLine = headers[0].split(" "); + if (requestLine.length != 3) { + mLog.w("Unexpected http request line: " + headers[0]); + return; + } + // Method + HttpMethod method = HttpMethod.parse(requestLine[0]); + if (method == HttpMethod.UNKNOWN) { + mLog.w("Unknown http request method: " + requestLine[0]); + return; + } + session.method = method; + // Path + session.path = requestLine[1]; + // Http Protocol + HttpProtocol protocol = HttpProtocol.parse(requestLine[2]); + if (protocol == HttpProtocol.UNKNOWN) { + mLog.w("Unknown http request protocol: " + requestLine[0]); + return; + } + session.protocol = protocol; + + // Http request headers + if (headers.length <= 1) { + mLog.w("Unexpected http request headers."); + return; + } + for (int i = 1; i < headers.length; i++) { + String requestHeader = headers[i]; + // Reach the header end + if (requestHeader.isEmpty()) { + continue; + } + String[] nameValue = requestHeader.split(":"); + if (nameValue.length < 2) { + mLog.w("Unexpected http request header: " + requestHeader); + continue; + } + String name = nameValue[0].trim(); + String value = requestHeader.replaceFirst(nameValue[0] + ": ", "").trim(); + List header = session.requestHeaders.get(name); + if (header == null) { + header = new ArrayList<>(1); + session.requestHeaders.put(name, header); + } + header.add(value); + } + } + + private void parseResponseHeader(HttpSession session, ByteBuffer buffer) { + session.resBodyOffset = buffer.remaining(); + String headerString = new String(buffer.array(), buffer.position(), buffer.remaining()); + // In some condition, no request but has response, we set the method to unknown. + if (session.method == null) { + session.method = HttpMethod.UNKNOWN; + } + String[] headers = headerString.split(NetBareUtils.LINE_END_REGEX); + String[] responseLine = headers[0].split(" "); + if (responseLine.length < 2) { + mLog.w("Unexpected http response line: " + headers[0]); + return; + } + // Http Protocol + HttpProtocol protocol = HttpProtocol.parse(responseLine[0]); + if (protocol == HttpProtocol.UNKNOWN) { + mLog.w("Unknown http response protocol: " + responseLine[0]); + return; + } + if (session.protocol != protocol) { + // Protocol downgrade + if (session.protocol != null) { + mLog.w("Unmatched http protocol, request is " + session.protocol + + " but response is " + responseLine[0]); + } + session.protocol = protocol; + } + // Code + int code = NetBareUtils.parseInt(responseLine[1], -1); + if (code == -1) { + mLog.w("Unexpected http response code: " + responseLine[1]); + return; + } + session.code = code; + // Message + session.message = headers[0].replaceFirst(responseLine[0], "") + .replaceFirst(responseLine[1], "").trim(); + + // Http response headers + if (headers.length <= 1) { + mLog.w("Unexpected http response headers."); + return; + } + for (int i = 1; i < headers.length; i++) { + String responseHeader = headers[i]; + // Reach the header end + if (responseHeader.isEmpty()) { + continue; + } + String[] nameValue = responseHeader.split(":"); + if (nameValue.length < 2) { + mLog.w("Unexpected http response header: " + responseHeader); + continue; + } + String name = nameValue[0].trim(); + String value = responseHeader.replaceFirst(nameValue[0] + ": ", "").trim(); + List header = session.responseHeaders.get(name); + if (header == null) { + header = new ArrayList<>(1); + session.responseHeaders.put(name, header); + } + header.add(value); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java new file mode 100644 index 0000000..fbe83ee --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSeparateInterceptor.java @@ -0,0 +1,137 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.ip.Protocol; +import com.google.common.primitives.Bytes; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Separate HTTP header part and body part into different packets. + * + * @author Megatron King + * @since 2018-12-08 15:36 + */ +/* package */ final class HttpHeaderSeparateInterceptor extends HttpPendingInterceptor { + + private boolean mRequestHeaderHandled; + private boolean mResponseHeaderHandled; + + private NetBareXLog mLog; + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, int index) + throws IOException { + if (mLog == null) { + mLog = new NetBareXLog(Protocol.TCP, chain.request().ip(), chain.request().port()); + } + if (mRequestHeaderHandled) { + chain.process(buffer); + return; + } + if (!buffer.hasRemaining()) { + chain.process(buffer); + return; + } + buffer = mergeRequestBuffer(buffer); + // Check the part end line. + int headerEndIndex = Bytes.indexOf(buffer.array(), NetBareUtils.PART_END_BYTES); + if (headerEndIndex < 0) { + mLog.w("Http request header data is not enough."); + // Not found the part end line, maybe the data is not enough, wait next buffer coming. + pendRequestBuffer(buffer); + } else { + mRequestHeaderHandled = true; + // Check whether the header and the body are in the same buffer. + boolean hasMultiPart = headerEndIndex < buffer.limit() - NetBareUtils.PART_END_BYTES.length; + if (hasMultiPart) { + mLog.w("Multiple http request parts are founded."); + // Separate the header and body data to two buffers. + int offset = headerEndIndex + NetBareUtils.PART_END_BYTES.length; + ByteBuffer headerBuffer = ByteBuffer.wrap(buffer.array(), buffer.position(), offset); + // Allocate a new buffer, do not use wrap, different buffers will share the same array. + ByteBuffer bodyBuffer = ByteBuffer.allocate(buffer.limit() - offset); + bodyBuffer.put(buffer.array(), offset, buffer.limit() - offset); + bodyBuffer.flip(); + chain.process(headerBuffer); + chain.process(bodyBuffer); + } else { + chain.process(buffer); + } + } + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, int index) + throws IOException { + if (mLog == null) { + mLog = new NetBareXLog(Protocol.TCP, chain.response().ip(), chain.response().port()); + } + if (mResponseHeaderHandled) { + chain.process(buffer); + return; + } + if (!buffer.hasRemaining()) { + chain.process(buffer); + return; + } + buffer = mergeResponseBuffer(buffer); + // Check the part end line. + int headerEndIndex = Bytes.indexOf(buffer.array(), NetBareUtils.PART_END_BYTES); + if (headerEndIndex < 0) { + mLog.w("Http response header data is not enough."); + // Not found the part end line, maybe the data is not enough, wait next buffer coming. + pendResponseBuffer(buffer); + } else { + mResponseHeaderHandled = true; + // Check whether the header and the body are in the same buffer. + boolean hasMultiPart = headerEndIndex < buffer.limit() - NetBareUtils.PART_END_BYTES.length; + if (hasMultiPart) { + mLog.w("Multiple http response parts are founded."); + // Separate the header and body data to two buffers. + int offset = headerEndIndex + NetBareUtils.PART_END_BYTES.length; + ByteBuffer headerBuffer = ByteBuffer.wrap(buffer.array(), buffer.position(), offset); + // Allocate a new buffer, do not use wrap, different buffers will share the same array. + ByteBuffer bodyBuffer = ByteBuffer.allocate(buffer.limit() - offset); + bodyBuffer.put(buffer.array(), offset, buffer.limit() - offset); + bodyBuffer.flip(); + chain.process(headerBuffer); + chain.process(bodyBuffer); + } else { + chain.process(buffer); + } + } + } + + @Override + protected void onRequestFinished(@NonNull HttpRequest request) { + super.onRequestFinished(request); + mRequestHeaderHandled = false; + } + + @Override + protected void onResponseFinished(@NonNull HttpResponse response) { + super.onResponseFinished(response); + mResponseHeaderHandled = false; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java new file mode 100644 index 0000000..d52d945 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpHeaderSniffInterceptor.java @@ -0,0 +1,174 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareLog; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Detect the plaintext packet header to determine is it the HTTP protocol. + * + * @author Megatron King + * @since 2019/1/31 16:13 + */ +/* package */ class HttpHeaderSniffInterceptor extends HttpIndexInterceptor { + + private final SSLRefluxCallback mCallback; + + private boolean mRealHttpProtocol; + + /* package */ HttpHeaderSniffInterceptor(SSLRefluxCallback callback) { + this.mCallback = callback; + } + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (!buffer.hasRemaining()) { + return; + } + if (chain.request().httpProtocol() != null) { + chain.process(buffer); + return; + } + if (index == 0) { + if (requestHeaderFirstByteNotPassed(buffer.get(buffer.position()))) { + mCallback.onRequest(chain.request(), buffer); + return; + } + // Sniff request header method + if (buffer.remaining() >= 7 && requestHeaderMethodNotPassed(buffer)) { + mCallback.onRequest(chain.request(), buffer); + return; + } + mRealHttpProtocol = true; + chain.process(buffer); + } else { + if (mRealHttpProtocol) { + chain.process(buffer); + } else { + mCallback.onRequest(chain.request(), buffer); + } + } + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (!buffer.hasRemaining()) { + return; + } + if (chain.response().httpProtocol() != null) { + chain.process(buffer); + return; + } + if (index == 0) { + if (responseHeaderFirstByteNotPassed(buffer.get(buffer.position()))) { + mCallback.onResponse(chain.response(), buffer); + return; + } + // Sniff response header protocol + if (buffer.remaining() >= 8 && responseHeaderProtocolNotPassed(buffer)) { + mCallback.onResponse(chain.response(), buffer); + return; + } + mRealHttpProtocol = true; + chain.process(buffer); + } else { + if (mRealHttpProtocol) { + chain.process(buffer); + } else { + mCallback.onResponse(chain.response(), buffer); + } + } + } + + private boolean requestHeaderFirstByteNotPassed(byte first) { + switch (first) { + // GET + case 'G': + // HEAD + case 'H': + // POST, PUT, PATCH + case 'P': + // DELETE + case 'D': + // OPTIONS + case 'O': + // TRACE + case 'T': + // CONNECT + case 'C': + return false; + default: + // Unknown first byte data. + NetBareLog.w("Unknown first request header byte : " + first); + break; + } + return true; + } + + private boolean requestHeaderMethodNotPassed(ByteBuffer buffer) { + String headerMethod = new String(buffer.array(), buffer.position(), + buffer.position() + 7); + for (HttpMethod method : HttpMethod.values()) { + if (method == HttpMethod.UNKNOWN) { + continue; + } + if (headerMethod.startsWith(method.name())) { + return false; + } + } + NetBareLog.w("Unknown request header method : " + headerMethod); + return true; + } + + private boolean responseHeaderFirstByteNotPassed(byte first) { + switch (first) { + // h2 + case 'h': + // HTTP1.x + case 'H': + return false; + default: + // Unknown first byte data. + NetBareLog.w("Unknown first response header byte : " + first); + break; + } + return true; + } + + private boolean responseHeaderProtocolNotPassed(ByteBuffer buffer) { + String headerProtocol = new String(buffer.array(), buffer.position(), + buffer.position() + 8); + for (HttpProtocol protocol : HttpProtocol.values()) { + if (protocol == HttpProtocol.UNKNOWN || protocol == HttpProtocol.H2_PRIOR_KNOWLEDGE + || protocol == HttpProtocol.SPDY_3 || protocol == HttpProtocol.QUIC) { + continue; + } + if (headerProtocol.startsWith(protocol.toString())) { + return false; + } + } + NetBareLog.w("Unknown response header protocol : " + headerProtocol); + return true; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java new file mode 100644 index 0000000..aa168ae --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpId.java @@ -0,0 +1,42 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import java.util.UUID; + +/** + * Regenerated http unique id for multi-sessions in one connection. + * + * @author Megatron King + * @since 2018-12-19 16:35 + */ +public class HttpId { + + public String id; + public long time; + public int streamId; + + public HttpId() { + this(-1); + } + + public HttpId(int streamId) { + this.id = UUID.randomUUID().toString(); + this.time = System.currentTimeMillis(); + this.streamId = streamId; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java new file mode 100644 index 0000000..662b6e3 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpIndexInterceptor.java @@ -0,0 +1,86 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Add the index parameter in the {@link #intercept(HttpRequestChain, ByteBuffer)} and + * {@link #intercept(HttpResponseChain, ByteBuffer)}, it indicates the packet index in the session. + *

+ * The index will be reset when the session finished. + *

+ * + * @author Megatron King + * @since 2018-12-03 21:00 + */ +public abstract class HttpIndexInterceptor extends HttpInterceptor { + + private int mRequestIndex; + private int mResponseIndex; + + /** + * The same like {@link #intercept(HttpRequestChain, ByteBuffer)}. + * + * @param chain The request chain, call {@linkplain HttpRequestChain#process(ByteBuffer)} to + * delivery the packet. + * @param buffer A nio buffer contains the packet data. + * @param index The packet index, started from 0. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException; + + /** + * The same like {@link #intercept(HttpResponseChain, ByteBuffer)}. + * + * @param chain The response chain, call {@linkplain HttpResponseChain#process(ByteBuffer)} to + * delivery the packet. + * @param buffer A nio buffer contains the packet data. + * @param index The packet index, started from 0. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException; + + @Override + protected final void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) + throws IOException { + intercept(chain, buffer, mRequestIndex++); + } + + @Override + protected final void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) + throws IOException { + intercept(chain, buffer, mResponseIndex++); + } + + @Override + protected void onRequestFinished(@NonNull HttpRequest request) { + super.onRequestFinished(request); + mRequestIndex = 0; + } + + @Override + protected void onResponseFinished(@NonNull HttpResponse response) { + super.onResponseFinished(response); + mResponseIndex = 0; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java new file mode 100644 index 0000000..4ea6f26 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptor.java @@ -0,0 +1,111 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.Interceptor; +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.RequestChain; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.gateway.ResponseChain; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A specific interceptor designed for {@link HttpVirtualGateway}, it focuses on the http protocol + * packets. The interceptor is an implement of {@link Interceptor}, methods are thread-safety and + * runs in local proxy server threads. + * + *

+ * Use {@link HttpInterceptorFactory} to create an http interceptor instance. + *

+ * + * @author Megatron King + * @since 2018-11-15 19:40 + */ +public abstract class HttpInterceptor implements Interceptor { + + /** + * Intercept http request packet, and delivery it to next interceptor or the terminal. + *

+ * Remember do not block this method for a long time, because all the connections share the + * same thread. + *

+ * + * @param chain The request chain, call {@linkplain HttpRequestChain#process(ByteBuffer)} to + * delivery the packet. + * @param buffer A nio buffer contains the packet data. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) + throws IOException; + + /** + * Intercept http response packet, and delivery it to next interceptor or the terminal. + *

+ * Remember do not block this method for a long time, because all the connections share the + * same thread. + *

+ * + * @param chain The response chain, call {@linkplain HttpResponseChain#process(ByteBuffer)} to + * delivery the packet. + * @param buffer A nio buffer contains the packet data. + * @throws IOException If an I/O error has occurred. + */ + protected abstract void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) + throws IOException; + + /** + * Invoked when a session's request has finished. It means the client has no more data sent to + * server in this session, and it might invoked multi times if a connection is keep-alive. + * + * @param request The request. + */ + protected void onRequestFinished(@NonNull HttpRequest request) { + } + + /** + * Invoked when a session's response has finished. It means the server has no more data sent to + * client in this session, and it might invoked multi times if a connection is keep-alive. + * + * @param response The response. + */ + protected void onResponseFinished(@NonNull HttpResponse response) { + } + + @Override + public final void intercept(@NonNull RequestChain chain, @NonNull ByteBuffer buffer) { + // Override the abstract method instead. + } + + @Override + public final void intercept(@NonNull ResponseChain chain, @NonNull ByteBuffer buffer) { + // Override the abstract method instead. + } + + @Override + public final void onRequestFinished(@NonNull Request request) { + // Override the abstract method instead. + } + + @Override + public final void onResponseFinished(@NonNull Response response) { + // Override the abstract method instead. + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java new file mode 100644 index 0000000..3816f23 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorFactory.java @@ -0,0 +1,39 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.InterceptorFactory; + +/** + * Factory used by developer to create their own interceptor for {@link HttpVirtualGateway}. + * + * @author Megatron King + * @since 2018-11-15 21:58 + */ +public interface HttpInterceptorFactory extends InterceptorFactory { + + /** + * Creates a http interceptor instance and immediately returns it, it must not be null. + * + * @return A newly created http interceptor. + */ + @NonNull + @Override + HttpInterceptor create(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java new file mode 100644 index 0000000..62a778e --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpInterceptorsFactory.java @@ -0,0 +1,39 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import java.util.List; + +/** + * Factory creates a collection {@link HttpInterceptor}s. + * + * @author Megatron King + * @since 2018-11-15 21:58 + */ +/* package */ interface HttpInterceptorsFactory { + + /** + * Creates a collection of http interceptor instances and immediately returns it, + * it must not be null. + * + * @return A http interceptor list. + */ + @NonNull + List create(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java new file mode 100644 index 0000000..4d2b937 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMethod.java @@ -0,0 +1,101 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +/** + * HTTP defines a set of request methods to indicate the desired action to be performed for a given + * resource. + * + * See https://tools.ietf.org/html/rfc7231#section-4 + * + * @author Megatron King + * @since 2018-10-15 19:59 + */ +public enum HttpMethod { + + /** + * It means NetBare does not know the method. + */ + UNKNOWN, + + /** + * The GET method requests a representation of the specified resource. Requests using GET + * should only retrieve data. + */ + GET, + + /** + * The HEAD method asks for a response identical to that of a GET request, but without the + * response body. + */ + HEAD, + + /** + * The POST method is used to submit an entity to the specified resource, often causing + * a change in state or side effects on the server. + */ + POST, + + /** + * The PUT method replaces all current representations of the target resource with the request + * payload. + */ + PUT, + + /** + * The DELETE method deletes the specified resource. + */ + DELETE, + + /** + * The CONNECT method establishes a tunnel to the server identified by the target resource. + */ + CONNECT, + + /** + * The OPTIONS method is used to describe the communication options for the target resource. + */ + OPTIONS, + + /** + * The TRACE method performs a message loop-back test along the path to the target resource. + */ + TRACE, + + /** + * The PATCH method is used to apply partial modifications to a resource. + */ + PATCH; + + /** + * Returns the request method enum. + * + * @param methodValue A string method presents in request line. + * @return A HttpMethod enum. + */ + @NonNull + public static HttpMethod parse(@NonNull String methodValue) { + for (HttpMethod method : values()) { + if (method.name().equals(methodValue)) { + return method; + } + } + return UNKNOWN; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java new file mode 100644 index 0000000..e3d730d --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpMultiplexInterceptor.java @@ -0,0 +1,89 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * If a HTTP connection is keep-alive, there will be multiple sessions go through the same virtual + * gateway. Those sessions are saw as one and not distinguished, this will increase the difficulty + * of interception. We use this interceptor to separate them into independent sessions and + * intercept them one by one. + * + * @author Megatron King + * @since 2018-12-15 15:17 + */ +/* package */ class HttpMultiplexInterceptor extends HttpIndexInterceptor { + + private final HttpZygoteRequest mZygoteRequest; + private final HttpZygoteResponse mZygoteResponse; + + private int mResponseIndex; + private NetBareXLog mLog; + + private boolean mWebSocket; + + /* package */ HttpMultiplexInterceptor(HttpZygoteRequest zygoteRequest, + HttpZygoteResponse zygoteResponse) { + this.mZygoteRequest = zygoteRequest; + this.mZygoteResponse = zygoteResponse; + } + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (chain.request().httpProtocol() != HttpProtocol.HTTP_1_1) { + chain.process(buffer); + return; + } + // Check the protocol is web socket + if (!mWebSocket) { + mWebSocket = mZygoteResponse.isWebSocket(); + } + if (mResponseIndex > 0 && !mWebSocket) { + if (mLog == null) { + mLog = new NetBareXLog(Protocol.TCP, chain.request().ip(), chain.request().port()); + } + mResponseIndex = 0; + mLog.w("Multiplex is found in one connection."); + // Multiplex sessions. + HttpId newId = new HttpId(); + mZygoteRequest.zygote(newId); + mZygoteResponse.zygote(newId); + } + chain.process(buffer); + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + mResponseIndex++; + chain.process(buffer); + } + + @Override + protected void onResponseFinished(@NonNull HttpResponse response) { + mResponseIndex = 0; + super.onResponseFinished(response); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java new file mode 100644 index 0000000..e8b7c6f --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpPendingInterceptor.java @@ -0,0 +1,123 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * An abstract interceptor provides multi-apis for packet pending. The packet will be stored in a + * queue, and you can merge them with another packet. + * + * @author Megatron King + * @since 2018-12-09 12:07 + */ +public abstract class HttpPendingInterceptor extends HttpIndexInterceptor { + + private List mRequestPendingBuffers; + private List mResponsePendingBuffers; + + /** + * Constructs a {@link HttpPendingInterceptor} instance. + */ + public HttpPendingInterceptor() { + mRequestPendingBuffers = new ArrayList<>(); + mResponsePendingBuffers = new ArrayList<>(); + } + + @Override + protected void onRequestFinished(@NonNull HttpRequest request) { + super.onRequestFinished(request); + mRequestPendingBuffers.clear(); + } + + @Override + protected void onResponseFinished(@NonNull HttpResponse response) { + super.onResponseFinished(response); + mResponsePendingBuffers.clear(); + } + + /** + * Pend a request packet buffer to waiting queue. + * + * @param buffer A request packet. + */ + protected void pendRequestBuffer(ByteBuffer buffer) { + mRequestPendingBuffers.add(buffer); + } + + /** + * Pend a response packet buffer to waiting queue. + * + * @param buffer A response packet. + */ + protected void pendResponseBuffer(ByteBuffer buffer) { + mResponsePendingBuffers.add(buffer); + } + + /** + * Merge all the request pending buffers and a given buffer, and output a new buffer which + * contains all data. The pending buffers will be clear after the merge action. + * + * @param buffer A fresh packet buffer. + * @return A new buffer. + */ + protected ByteBuffer mergeRequestBuffer(ByteBuffer buffer) { + return merge(mRequestPendingBuffers, buffer); + } + + /** + * Merge all the response pending buffers and a given buffer, and output a new buffer which + * contains all data. The pending buffers will be clear after the merge action. + * + * @param buffer A fresh packet buffer. + * @return A new buffer. + */ + protected ByteBuffer mergeResponseBuffer(ByteBuffer buffer) { + return merge(mResponsePendingBuffers, buffer); + } + + private ByteBuffer merge(List pendingBuffers, ByteBuffer buffer) { + if (!pendingBuffers.isEmpty()) { + int total = 0; + for (ByteBuffer pendingBuffer : pendingBuffers) { + total += pendingBuffer.remaining(); + } + total += buffer.remaining(); + + // Merge elder buffer first. + int offset = 0; + byte[] array = new byte[total]; + for (ByteBuffer pendingBuffer : pendingBuffers) { + System.arraycopy(pendingBuffer.array(), pendingBuffer.position(), array, offset, + pendingBuffer.remaining()); + offset += pendingBuffer.remaining(); + } + + // Merge the incoming buffer + System.arraycopy(buffer.array(), buffer.position(), array, offset, buffer.remaining()); + + buffer = ByteBuffer.wrap(array); + // Clear all data. + pendingBuffers.clear(); + } + return buffer; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java new file mode 100644 index 0000000..dca37ea --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpProtocol.java @@ -0,0 +1,115 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +/** + * Http protocols that NetBare defined. + * + * @author Megatron King + * @since 2018-10-15 19:50 + */ +public enum HttpProtocol { + + /** + * It means NetBare does not know the protocol. + */ + UNKNOWN("unknown"), + + /** + * An obsolete plaintext framing that does not use persistent sockets by default. + */ + HTTP_1_0("HTTP/1.0"), + + /** + * A plaintext framing that includes persistent connections. + * + *

This version of OkHttp implements RFC + * 7230, and tracks revisions to that spec. + */ + HTTP_1_1("HTTP/1.1"), + + /** + * Chromium's binary-framed protocol that includes header compression, multiplexing multiple + * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on SPDY/3. + */ + SPDY_3("spdy/3.1"), + + /** + * The IETF's binary-framed protocol that includes header compression, multiplexing multiple + * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on HTTP/2. + */ + HTTP_2("h2"), + + /** + * Cleartext HTTP/2 with no "upgrade" round trip. This option requires the client to have prior + * knowledge that the server supports cleartext HTTP/2. + * + * @see Starting HTTP/2 with Prior + * Knowledge + */ + H2_PRIOR_KNOWLEDGE("h2_prior_knowledge"), + + /** + * QUIC (Quick UDP Internet Connection) is a new multiplexed and secure transport atop UDP, + * designed from the ground up and optimized for HTTP/2 semantics. + * HTTP/1.1 semantics are layered on HTTP/2. + */ + QUIC("quic"); + + private final String protocol; + + HttpProtocol(String protocol) { + this.protocol = protocol; + } + + /** + * Returns the protocol identified by {@code protocol}. + * + * @param protocol A string protocol presents in request line and status line. + * @return A HttpProtocol enum. + */ + @NonNull + public static HttpProtocol parse(@NonNull String protocol) { + if (protocol.equalsIgnoreCase(HTTP_1_0.protocol)) { + return HTTP_1_0; + } else if (protocol.equalsIgnoreCase(HTTP_1_1.protocol)) { + return HTTP_1_1; + } else if (protocol.equalsIgnoreCase(H2_PRIOR_KNOWLEDGE.protocol)) { + return H2_PRIOR_KNOWLEDGE; + } else if (protocol.equalsIgnoreCase(HTTP_2.protocol)) { + return HTTP_2; + } else if (protocol.equalsIgnoreCase(SPDY_3.protocol)) { + return SPDY_3; + } else if (protocol.equalsIgnoreCase(QUIC.protocol)) { + return QUIC; + } else { + return UNKNOWN; + } + } + + /** + * Returns the protocol string value rather than it's name. + * + * @return Protocol value. + */ + @Override + public String toString() { + return this.protocol; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java new file mode 100644 index 0000000..e0107c6 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequest.java @@ -0,0 +1,206 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.http2.Http2Settings; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +/** + * It is an implementation of {@link Request} represents the HTTP protocol. Instances of this + * class are not immutable. + * + * @author Megatron King + * @since 2018-11-11 23:37 + */ +public class HttpRequest extends Request { + + private Request mRequest; + + private HttpId mHttpId; + private HttpSession mSession; + + /* package */ HttpRequest(Request request, HttpSession session) { + this(request, null, session); + } + + /* package */ HttpRequest(Request request, HttpId httpId, HttpSession session) { + this.mRequest = request; + this.mHttpId = httpId; + this.mSession = session; + } + + /* package */ HttpSession session() { + return mSession; + } + + @Override + public void process(ByteBuffer buffer) throws IOException { + mRequest.process(buffer); + } + + @Override + public String id() { + return mHttpId != null ? mHttpId.id : mRequest.id(); + } + + @Override + public long time() { + return mHttpId != null ? mHttpId.time : mRequest.time(); + } + + @Override + public int uid() { + return mRequest.uid(); + } + + @Override + public String ip() { + return mRequest.ip(); + } + + @Override + public int port() { + return mRequest.port(); + } + + @Override + public Protocol protocol() { + return mRequest.protocol(); + } + + @Override + public String host() { + return mRequest.host(); + } + + /** + * Returns the request method for this request. + * + * @return The request method. + */ + public HttpMethod method() { + return mSession.method; + } + + /** + * Returns this request's http protocol, such as {@link HttpProtocol#HTTP_1_1} or + * {@link HttpProtocol#HTTP_1_0}. + * + * @return The request protocol. + */ + public HttpProtocol httpProtocol() { + return mSession.protocol; + } + + /** + * Returns this request's path. + * + * @return The request path. + */ + public String path() { + return mSession.path; + } + + /** + * Whether the request is a HTTPS request. + * + * @return HTTPS returns true. + */ + public boolean isHttps() { + return mSession.isHttps; + } + + /** + * Returns this request's URL. + * + * @return The request URL. + */ + public String url() { + String path = path() == null ? "" : path(); + return (isHttps() ? "https://" : "http://") + host() + path; + } + + /** + * Returns this request's headers. + * + * @return A map of headers. + */ + public Map> requestHeaders() { + return mSession.requestHeaders; + } + + /** + * Returns this request's header values by name. + * + * @param name A header name. + * @return A collection of header values. + */ + public List requestHeader(String name) { + return requestHeaders().get(name); + } + + /** + * Returns the offset of request body's starting index in request data. + * + * @return Offset of request body. + */ + public int requestBodyOffset() { + return mSession.reqBodyOffset; + } + + /** + * Returns the HTTP/2 stream id. + * + * @return A stream id. + */ + public int streamId() { + return mHttpId != null ? mHttpId.streamId : -1; + } + + /** + * Returns the HTTP/2 client settings. + * + * @return Client settings. + */ + public Http2Settings clientHttp2Settings() { + return mSession.clientHttp2Settings; + } + + /** + * Returns the HTTP/2 peer settings. + * + * @return Client settings. + */ + public Http2Settings peerHttp2Settings() { + return mSession.peerHttp2Settings; + } + + /** + * Whether the current HTTP2 request stream is end. + * + * @return End is true. + */ + public boolean requestStreamEnd() { + return mSession.requestStreamEnd; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java new file mode 100644 index 0000000..df42e13 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpRequestChain.java @@ -0,0 +1,65 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.InterceptorChain; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Http request chain, responsible for intercepting http request packets. + * + * @author Megatron King + * @since 2018-11-16 23:21 + */ +public class HttpRequestChain extends InterceptorChain { + + private HttpZygoteRequest mZygoteRequest; + + /* package */ HttpRequestChain(HttpZygoteRequest request, List interceptors) { + this(request, interceptors, 0); + } + + /* package */ HttpRequestChain(HttpZygoteRequest request, List interceptors, + int index) { + super(request, interceptors, index); + this.mZygoteRequest = request; + } + + HttpZygoteRequest zygoteRequest() { + return mZygoteRequest; + } + + @Override + protected void processNext(ByteBuffer buffer, HttpRequest request, + List interceptors, int index) throws IOException { + HttpInterceptor interceptor = interceptors.get(index); + if (interceptor != null) { + interceptor.intercept(new HttpRequestChain(mZygoteRequest, interceptors, ++index), buffer); + } + } + + @NonNull + public HttpRequest request() { + HttpRequest active = mZygoteRequest.getActive(); + return active != null ? active : mZygoteRequest; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java new file mode 100644 index 0000000..94658ea --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponse.java @@ -0,0 +1,278 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.http2.Http2Settings; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +/** + * It is an implementation of {@link Response} represents the HTTP protocol. Instances of this + * class are not immutable. + * + * @author Megatron King + * @since 2018-11-11 23:37 + */ +public class HttpResponse extends Response { + + private Response mResponse; + + private HttpId mHttpId; + private HttpSession mSession; + + /* package */ HttpResponse(Response response, HttpSession session) { + this(response, null, session); + } + + /* package */ HttpResponse(Response response, HttpId httpId, HttpSession session) { + this.mResponse = response; + this.mHttpId = httpId; + this.mSession = session; + } + + /* package */ HttpSession session() { + return mSession; + } + + @Override + public void process(ByteBuffer buffer) throws IOException { + mResponse.process(buffer); + } + + @Override + public String id() { + return mHttpId != null ? mHttpId.id : mResponse.id(); + } + + @Override + public long time() { + return mHttpId != null ? mHttpId.time : mResponse.time(); + } + + @Override + public int uid() { + return mResponse.uid(); + } + + @Override + public String ip() { + return mResponse.ip(); + } + + @Override + public int port() { + return mResponse.port(); + } + + @Override + public Protocol protocol() { + return mResponse.protocol(); + } + + @Override + public String host() { + return mResponse.host(); + } + + /** + * Returns the request method for this request. + * + * @return The request method. + */ + public HttpMethod method() { + return mSession.method; + } + + /** + * Returns this response's http protocol, such as {@link HttpProtocol#HTTP_1_1} or + * {@link HttpProtocol#HTTP_1_0}. + * + * @return The response protocol. + */ + public HttpProtocol httpProtocol() { + return mSession.protocol; + } + + /** + * Returns this request's path. + * + * @return The request path. + */ + public String path() { + return mSession.path; + } + + /** + * Whether the request is a HTTPS request. + * + * @return HTTPS returns true. + */ + public boolean isHttps() { + return mSession.isHttps; + } + + /** + * Whether the request is a web socket protocol. + * + * @return Web socket protocol returns true. + */ + public boolean isWebSocket() { + if (mSession.code != 101) { + return false; + } + List upgradeHeaderValues = null; + for (Map.Entry> entry : mSession.responseHeaders.entrySet()) { + if ("upgrade".equalsIgnoreCase(entry.getKey())) { + upgradeHeaderValues = entry.getValue(); + } + } + if (upgradeHeaderValues == null || upgradeHeaderValues.isEmpty()) { + return false; + } + for (String value : upgradeHeaderValues) { + if ("websocket".equalsIgnoreCase(value)) { + return true; + } + } + return false; + } + + /** + * Returns this request's URL. + * + * @return The request URL. + */ + public String url() { + String path = path() == null ? "" : path(); + return (isHttps() ? "https://" : "http://") + host() + path; + } + + /** + * Returns this request's headers. + * + * @return A map of headers. + */ + public Map> requestHeaders() { + return mSession.requestHeaders; + } + + /** + * Returns this response's headers. + * + * @return A map of headers. + */ + public Map> responseHeaders() { + return mSession.responseHeaders; + } + + /** + * Returns this request's header values by name. + * + * @param name A header name. + * @return A collection of header values. + */ + public List requestHeader(String name) { + return requestHeaders().get(name); + } + + /** + * Returns this response's header values by name. + * + * @param name A header name. + * @return A collection of header values. + */ + public List responseHeader(String name) { + return responseHeaders().get(name); + } + + /** + * Returns the HTTP status code. + * + * @return Status code. + */ + public int code() { + return mSession.code; + } + + /** + * Returns the HTTP status message. + * + * @return Status message. + */ + public String message() { + return mSession.message; + } + + /** + * Returns the offset of request body's starting index in request data. + * + * @return Offset of request body. + */ + public int requestBodyOffset() { + return mSession.reqBodyOffset; + } + + /** + * Returns the offset of response body's starting index in request data. + * + * @return Offset of request body. + */ + public int responseBodyOffset() { + return mSession.resBodyOffset; + } + + /** + * Returns the HTTP2 stream id. + * + * @return A stream id. + */ + public int streamId() { + return mHttpId != null ? mHttpId.streamId : -1; + } + + /** + * Returns the HTTP/2 client settings. + * + * @return Client settings. + */ + public Http2Settings clientHttp2Settings() { + return mSession.clientHttp2Settings; + } + + /** + * Returns the HTTP/2 peer settings. + * + * @return Client settings. + */ + public Http2Settings peerHttp2Settings() { + return mSession.peerHttp2Settings; + } + + /** + * Whether the current HTTP2 response stream is end. + * + * @return End is true. + */ + public boolean responseStreamEnd() { + return mSession.responseStreamEnd; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java new file mode 100644 index 0000000..ca813d9 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpResponseChain.java @@ -0,0 +1,65 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.InterceptorChain; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Http response chain, responsible for intercepting http response packets. + * + * @author Megatron King + * @since 2018-11-16 23:21 + */ +public class HttpResponseChain extends InterceptorChain { + + private HttpZygoteResponse mZygoteResponse; + + /* package */ HttpResponseChain(HttpZygoteResponse response, List interceptors) { + this(response, interceptors, 0); + } + + /* package */ HttpResponseChain(HttpZygoteResponse response, List interceptors, + int index) { + super(response, interceptors, index); + this.mZygoteResponse = response; + } + + HttpZygoteResponse zygoteResponse() { + return mZygoteResponse; + } + + @Override + protected void processNext(ByteBuffer buffer, HttpResponse response, + List interceptors, int index) throws IOException { + HttpInterceptor interceptor = interceptors.get(index); + if (interceptor != null) { + interceptor.intercept(new HttpResponseChain(mZygoteResponse, interceptors, ++index), buffer); + } + } + + @NonNull + public HttpResponse response() { + HttpResponse active = mZygoteResponse.getActive(); + return active != null ? active : mZygoteResponse; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java new file mode 100644 index 0000000..1dc17ba --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSession.java @@ -0,0 +1,48 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.http2.Http2Settings; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Provides HTTP protocol session information. + * + * @author Megatron King + * @since 2018-11-10 11:56 + */ +/* package */ class HttpSession { + + boolean isHttps; + HttpProtocol protocol; + HttpMethod method; + String path; + Map> requestHeaders = new LinkedHashMap<>(); + Map> responseHeaders = new LinkedHashMap<>(); + int code; + String message; + int reqBodyOffset; + int resBodyOffset; + // Belows is for HTTP2 + Http2Settings clientHttp2Settings; + Http2Settings peerHttp2Settings; + boolean requestStreamEnd; + boolean responseStreamEnd; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java new file mode 100644 index 0000000..5a58f36 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSessionFactory.java @@ -0,0 +1,46 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import java.util.HashMap; +import java.util.Map; + +/** + * A factory creates {@link HttpSession} instance by id. + * + * @author Megatron King + * @since 2019/1/6 19:42 + */ +/* package */ class HttpSessionFactory { + + private final Map mHttpSession; + + /* package */ HttpSessionFactory() { + mHttpSession = new HashMap<>(1); + } + + HttpSession create(String id) { + HttpSession httpSession; + if (mHttpSession.containsKey(id)) { + httpSession = mHttpSession.get(id); + } else { + httpSession = new HttpSession(); + mHttpSession.put(id, httpSession); + } + return httpSession; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java new file mode 100644 index 0000000..b0953ca --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpSniffInterceptor.java @@ -0,0 +1,111 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.ssl.SSLCodec; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A fronted interceptor verifies the first net packet in order to determine whether it is a HTTP + * protocol packet. If the packet is not a valid HTTP packet, it will be sent to tunnel directly, + * otherwise sent to the next interceptor. + * + * @author Megatron King + * @since 2018-12-04 11:58 + */ +/* package */ final class HttpSniffInterceptor extends HttpIndexInterceptor { + + private static final int TYPE_HTTP = 1; + private static final int TYPE_HTTPS = 2; + private static final int TYPE_INVALID = 3; + + private final HttpSession mSession; + + private int mType; + + /* package */ HttpSniffInterceptor(HttpSession session) { + this.mSession = session; + } + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (index == 0) { + mType = chain.request().host() == null ? TYPE_INVALID : verifyHttpType(buffer); + } + if (mType == TYPE_HTTPS) { + mSession.isHttps = true; + } + if (mType == TYPE_INVALID) { + chain.processFinal(buffer); + return; + } + chain.process(buffer); + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (mType == TYPE_INVALID) { + chain.processFinal(buffer); + return; + } + chain.process(buffer); + } + + private int verifyHttpType(ByteBuffer buffer) { + if (!buffer.hasRemaining()) { + return TYPE_INVALID; + } + byte first = buffer.get(buffer.position()); + switch (first) { + // GET + case 'G': + // HEAD + case 'H': + // POST, PUT, PATCH + case 'P': + // DELETE + case 'D': + // OPTIONS + case 'O': + // TRACE + case 'T': + // CONNECT + case 'C': + return TYPE_HTTP; + // HTTPS + case SSLCodec.SSL_CONTENT_TYPE_ALERT: + case SSLCodec.SSL_CONTENT_TYPE_APPLICATION_DATA: + case SSLCodec.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC: + case SSLCodec.SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT: + case SSLCodec.SSL_CONTENT_TYPE_HANDSHAKE: + return TYPE_HTTPS; + default: + // Unknown first byte data. + NetBareLog.e("Unknown first request byte : " + first); + break; + } + return TYPE_INVALID; + } + + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java new file mode 100644 index 0000000..ce85481 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGateway.java @@ -0,0 +1,135 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.gateway.SpecVirtualGateway; +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.http2.Http2DecodeInterceptor; +import com.github.megatronking.netbare.http2.Http2EncodeInterceptor; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.ssl.JKS; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link VirtualGateway} that is responsible for HTTP(S) packets interception. It integrates + * several internal {@link HttpInterceptor}s to decode and parse HTTP(S) packets. And also it + * supports extensional {@link HttpInterceptor}s. Use {@link HttpVirtualGatewayFactory} to + * create an instance. + * + * @author Megatron King + * @since 2018-11-20 23:43 + */ +/* package */ class HttpVirtualGateway extends SpecVirtualGateway { + + private HttpZygoteRequest mHttpZygoteRequest; + private HttpZygoteResponse mHttpZygoteResponse; + + private List mInterceptors; + + /* package */ HttpVirtualGateway(Session session, Request request, Response response, JKS jks, + final List factories) { + super(Protocol.TCP, session, request, response); + + HttpSessionFactory sessionFactory = new HttpSessionFactory(); + this.mHttpZygoteRequest = new HttpZygoteRequest(request, sessionFactory); + this.mHttpZygoteResponse = new HttpZygoteResponse(response, sessionFactory); + + // Add default interceptors. + SSLCodecInterceptor codecInterceptor = new SSLCodecInterceptor(jks, request, response); + this.mInterceptors = new ArrayList<>(8); + + mInterceptors.add(new HttpSniffInterceptor(sessionFactory.create(session.id))); + mInterceptors.add(codecInterceptor); + mInterceptors.add(new Http2SniffInterceptor(codecInterceptor)); + mInterceptors.add(new Http2DecodeInterceptor(codecInterceptor, mHttpZygoteRequest, mHttpZygoteResponse)); + mInterceptors.add(new HttpMultiplexInterceptor(mHttpZygoteRequest, mHttpZygoteResponse)); + mInterceptors.add(new HttpHeaderSniffInterceptor(codecInterceptor)); + mInterceptors.add(new ContainerHttpInterceptor(new HttpInterceptorsFactory() { + @NonNull + @Override + public List create() { + List subs = new ArrayList<>(factories.size() + 2); + subs.add(new HttpHeaderSeparateInterceptor()); + subs.add(new HttpHeaderParseInterceptor()); + // Add extension interceptors. + for (HttpInterceptorFactory factory : factories) { + subs.add(factory.create()); + } + return subs; + } + })); + // Goalkeepers. + mInterceptors.add(mInterceptors.size(), new Http2EncodeInterceptor()); + mInterceptors.add(mInterceptors.size(), new SSLRefluxInterceptor(codecInterceptor)); + + // + // SSL Flow Model: + // + // Request Response + // + // out in in out + // ⇈ ⇊ ⇊ ⇈ + // Encrypted Encrypted + // ⇈ ⇊ ⇊ ⇈ + // ----------------------------------------------------------- + // | Codec Interceptor | + // ----------------------------------------------------------- + // ⇈ | ⇊ ... ⇊ | ⇈ + // | ⇊ ... ⇊ | + // ⇈ | Decrypted | interceptors | Decrypted | ⇈ + // | ⇊ ... ⇊ | + // ⇈ | ⇊ ... ⇊ | ⇈ + // ----------------------------------------------------------- + // | Reflux Interceptor | + // ----------------------------------------------------------- + // ⇈ ⇇ ⇇ ⇇ ⇊ ⇊ ⇉ ⇉ ⇉ ⇈ + // + } + + @Override + public void onSpecRequest(ByteBuffer buffer) throws IOException { + new HttpRequestChain(mHttpZygoteRequest, mInterceptors).process(buffer); + } + + @Override + public void onSpecResponse(ByteBuffer buffer) throws IOException { + new HttpResponseChain(mHttpZygoteResponse, mInterceptors).process(buffer); + } + + @Override + public void onSpecRequestFinished() { + for (HttpInterceptor interceptor: mInterceptors) { + interceptor.onRequestFinished(mHttpZygoteRequest); + } + } + + @Override + public void onSpecResponseFinished() { + for (HttpInterceptor interceptor: mInterceptors) { + interceptor.onResponseFinished(mHttpZygoteResponse); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java new file mode 100644 index 0000000..9c4a5f4 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpVirtualGatewayFactory.java @@ -0,0 +1,71 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.gateway.VirtualGatewayFactory; +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.ssl.JKS; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link VirtualGatewayFactory} that produces the {@link HttpVirtualGateway}. + * + * @author Megatron King + * @since 2018-11-20 23:50 + */ +public class HttpVirtualGatewayFactory implements VirtualGatewayFactory { + + private List mFactories; + private JKS mJKS; + + /** + * Constructs a {@link HttpVirtualGatewayFactory} instance with {@link JKS} and a collection of + * {@link HttpInterceptorFactory}. + * + * @param factories a collection of {@link HttpInterceptorFactory}. + * @return A instance of {@link HttpVirtualGatewayFactory}. + */ + public HttpVirtualGatewayFactory(@NonNull JKS jks, + @NonNull List factories) { + this.mJKS = jks; + this.mFactories = factories; + } + + @Override + public VirtualGateway create(Session session, Request request, Response response) { + return new HttpVirtualGateway(session, request, response, mJKS, new ArrayList<>(mFactories)); + } + + /** + * Create a {@link HttpVirtualGatewayFactory} instance with {@link JKS} and a collection of + * {@link HttpInterceptorFactory}. + * + * @param factories a collection of {@link HttpInterceptorFactory}. + * @return A instance of {@link HttpVirtualGatewayFactory}. + */ + public static VirtualGatewayFactory create(@NonNull JKS authority, + @NonNull List factories) { + return new HttpVirtualGatewayFactory(authority, factories); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java new file mode 100644 index 0000000..cfd29df --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteRequest.java @@ -0,0 +1,79 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.http2.Http2Settings; +import com.github.megatronking.netbare.http2.Http2Updater; + +import java.util.HashMap; +import java.util.Map; + +/** + * A zygote http request class, it creates the real http request instance. + * + * @author Megatron King + * @since 2019/1/6 17:00 + */ +public class HttpZygoteRequest extends HttpRequest implements Http2Updater { + + private final Request mRequest; + private final HttpSessionFactory mSessionFactory; + private final Map mCachedRequests; + + private HttpRequest mActiveRequest; + + /* package */ HttpZygoteRequest(Request request, HttpSessionFactory factory) { + super(request, factory.create(request.id())); + this.mRequest = request; + this.mSessionFactory = factory; + this.mCachedRequests = new HashMap<>(); + } + + public void zygote(HttpId id) { + if (mCachedRequests.containsKey(id.id)) { + mActiveRequest = mCachedRequests.get(id.id); + } else { + HttpSession originSession = session(); + HttpSession session = mSessionFactory.create(id.id); + session.isHttps = originSession.isHttps; + session.protocol = originSession.protocol; + session.clientHttp2Settings = originSession.clientHttp2Settings; + session.peerHttp2Settings = originSession.peerHttp2Settings; + HttpRequest request = new HttpRequest(mRequest, id, session); + mCachedRequests.put(id.id, request); + mActiveRequest = request; + } + } + + @Override + public void onSettingsUpdate(Http2Settings http2Settings) { + session().clientHttp2Settings = http2Settings; + } + + @Override + public void onStreamFinished() { + HttpRequest request = getActive(); + if (request != null) { + request.session().requestStreamEnd = true; + } + } + + HttpRequest getActive() { + return mActiveRequest; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java new file mode 100644 index 0000000..714fdea --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/HttpZygoteResponse.java @@ -0,0 +1,80 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.http2.Http2Settings; +import com.github.megatronking.netbare.http2.Http2Updater; + +import java.util.HashMap; +import java.util.Map; + +/** + * A zygote http response class, it creates the real http response instance. + * + * @author Megatron King + * @since 2019/1/6 17:09 + */ +public class HttpZygoteResponse extends HttpResponse implements Http2Updater { + + private final Response mResponse; + private final HttpSessionFactory mSessionFactory; + private final Map mCachedResponses; + + private HttpResponse mActiveResponse; + + /* package */ HttpZygoteResponse(Response response, HttpSessionFactory factory) { + super(response, factory.create(response.id())); + this.mResponse = response; + this.mSessionFactory = factory; + this.mCachedResponses = new HashMap<>(); + } + + public void zygote(HttpId id) { + if (mCachedResponses.containsKey(id.id)) { + mActiveResponse = mCachedResponses.get(id.id); + } else { + HttpSession originSession = super.session(); + HttpSession session = mSessionFactory.create(id.id); + session.isHttps = originSession.isHttps; + session.protocol = originSession.protocol; + session.clientHttp2Settings = originSession.clientHttp2Settings; + session.peerHttp2Settings = originSession.peerHttp2Settings; + HttpResponse response = new HttpResponse(mResponse, id, session); + mCachedResponses.put(id.id, response); + mActiveResponse = response; + } + } + + @Override + public void onSettingsUpdate(Http2Settings http2Settings) { + session().peerHttp2Settings = http2Settings; + } + + @Override + public void onStreamFinished() { + HttpResponse response = getActive(); + if (response != null) { + response.session().responseStreamEnd = true; + } + } + + HttpResponse getActive() { + return mActiveResponse; + } + +} + diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java new file mode 100644 index 0000000..128ffac --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLCodecInterceptor.java @@ -0,0 +1,256 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.ssl.JKS; +import com.github.megatronking.netbare.ssl.SSLCodec; +import com.github.megatronking.netbare.ssl.SSLEngineFactory; +import com.github.megatronking.netbare.ssl.SSLUtils; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; + +/** + * An interceptor decodes SSL encrypt packets to plaintext packets. + * + * @author Megatron King + * @since 2018-11-15 15:39 + */ +/* package */ class SSLCodecInterceptor extends HttpPendingInterceptor implements SSLRefluxCallback { + + private static SSLEngineFactory sEngineFactory; + + private Request mRequest; + private Response mResponse; + + private JKS mJKS; + + private SSLHttpRequestCodec mRequestCodec; + private SSLHttpResponseCodec mResponseCodec; + + private NetBareXLog mLog; + + private boolean mClientAlpnResolved; + + /* package */ SSLCodecInterceptor(JKS jks, Request request, Response response) { + this.mJKS = jks; + this.mRequest = request; + this.mResponse = response; + + if (sEngineFactory == null) { + try { + sEngineFactory = new SSLEngineFactory(jks); + } catch (GeneralSecurityException | IOException e) { + NetBareLog.e("Create SSLEngineFactory failed: " + e.getMessage()); + } + } + + mRequestCodec = new SSLHttpRequestCodec(sEngineFactory); + mResponseCodec = new SSLHttpResponseCodec(sEngineFactory); + + mLog = new NetBareXLog(Protocol.TCP, request.ip(), request.port()); + } + + @Override + protected void intercept(@NonNull final HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (!chain.request().isHttps()) { + chain.process(buffer); + } else if (!mJKS.isInstalled()) { + // Skip all interceptors + chain.processFinal(buffer); + mLog.w("JSK not installed, skip all interceptors!"); + } else { + if (!mClientAlpnResolved) { + buffer = mergeRequestBuffer(buffer); + int verifyResult = SSLUtils.verifyPacket(buffer); + if (verifyResult == SSLUtils.PACKET_NOT_ENCRYPTED) { + throw new IOException("SSL packet is not encrypt."); + } + if (verifyResult == SSLUtils.PACKET_NOT_ENOUGH) { + pendRequestBuffer(buffer); + return; + } + + mRequestCodec.setRequest(chain.request()); + // Start handshake with remote server + mResponseCodec.setRequest(chain.request()); + + // Parse the ALPN protocol of client. + HttpProtocol[] protocols = SSLUtils.parseClientHelloAlpn(buffer); + mClientAlpnResolved = true; + + if (protocols == null || protocols.length == 0) { + mRequestCodec.setSelectedAlpnResolved(); + mResponseCodec.setSelectedAlpnResolved(); + mResponseCodec.prepareHandshake(); + } else { + // Detect remote server's ALPN and then continue request. + mResponseCodec.prepareHandshake(protocols, new SSLHttpResponseCodec.AlpnResolvedCallback() { + @Override + public void onResult(String selectedAlpnProtocol) throws IOException { + if (selectedAlpnProtocol != null) { + HttpProtocol protocol = HttpProtocol.parse(selectedAlpnProtocol); + // Only accept Http1.1 and Http2.0 + if (protocol == HttpProtocol.HTTP_1_1 || protocol == HttpProtocol.HTTP_2) { + mRequestCodec.setSelectedAlpnProtocol(protocol); + chain.request().session().protocol = protocol; + mLog.i("Server selected ALPN protocol: " + protocol.toString()); + } else { + mLog.w("Unexpected server ALPN protocol: " + protocol.toString()); + } + } + mRequestCodec.setSelectedAlpnResolved(); + // Continue request. + decodeRequest(chain, ByteBuffer.allocate(0)); + } + }); + } + } + // Hold the request buffer until the server ALPN configuration resolved. + if (!mRequestCodec.selectedAlpnResolved()) { + pendRequestBuffer(buffer); + return; + } + + decodeRequest(chain, buffer); + } + } + + @Override + protected void intercept(@NonNull final HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (!chain.response().isHttps()) { + chain.process(buffer); + } else if (!mJKS.isInstalled()) { + // Skip all interceptors + chain.processFinal(buffer); + mLog.w("JSK not installed, skip all interceptors!"); + } else { + // Merge buffers + decodeResponse(chain, buffer); + } + } + + @Override + public void onRequest(HttpRequest request, ByteBuffer buffer) throws IOException { + mResponseCodec.encode(buffer, new SSLCodec.CodecCallback() { + @Override + public void onPending(ByteBuffer buffer) { + } + + @Override + public void onProcess(ByteBuffer buffer) { + } + + @Override + public void onEncrypt(ByteBuffer buffer) throws IOException { + // The encrypt request data is sent to remote server + mRequest.process(buffer); + } + + @Override + public void onDecrypt(ByteBuffer buffer) { + } + }); + } + + @Override + public void onResponse(HttpResponse response, ByteBuffer buffer) throws IOException { + mRequestCodec.encode(buffer, new SSLCodec.CodecCallback() { + @Override + public void onPending(ByteBuffer buffer) { + } + + @Override + public void onProcess(ByteBuffer buffer) { + } + + @Override + public void onEncrypt(ByteBuffer buffer) throws IOException { + // The encrypt response data is sent to proxy server + mResponse.process(buffer); + } + + @Override + public void onDecrypt(ByteBuffer buffer) { + } + }); + } + + private void decodeRequest(final HttpRequestChain chain, ByteBuffer buffer) throws IOException { + // Merge buffers + mRequestCodec.decode(mergeRequestBuffer(buffer), + new SSLCodec.CodecCallback() { + @Override + public void onPending(ByteBuffer buffer) { + pendRequestBuffer(buffer); + } + + @Override + public void onProcess(ByteBuffer buffer) throws IOException { + chain.processFinal(buffer); + } + + @Override + public void onEncrypt(ByteBuffer buffer) throws IOException { + mResponse.process(buffer); + } + + @Override + public void onDecrypt(ByteBuffer buffer) throws IOException { + chain.process(buffer); + } + }); + } + + + private void decodeResponse(final HttpResponseChain chain, ByteBuffer buffer) throws IOException { + // Merge buffers + mResponseCodec.decode(mergeResponseBuffer(buffer), + new SSLCodec.CodecCallback() { + @Override + public void onPending(ByteBuffer buffer) { + pendResponseBuffer(buffer); + } + + @Override + public void onProcess(ByteBuffer buffer) throws IOException { + chain.processFinal(buffer); + } + + @Override + public void onEncrypt(ByteBuffer buffer) throws IOException { + mRequest.process(buffer); + } + + @Override + public void onDecrypt(ByteBuffer buffer) throws IOException { + chain.process(buffer); + } + + }); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java new file mode 100644 index 0000000..d49fa03 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpRequestCodec.java @@ -0,0 +1,134 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.ssl.SSLEngineFactory; +import com.github.megatronking.netbare.ssl.SSLRequestCodec; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.Charset; + +import javax.net.ssl.SSLEngine; + +/** + * Http request ssl codec enables Application-Layer Protocol Negotiation(ALPN). + * + * See http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 + * + * @author Megatron King + * @since 2019/1/3 23:01 + */ +/* package */ class SSLHttpRequestCodec extends SSLRequestCodec { + + private HttpProtocol mSelectedAlpnProtocol; + private boolean mAlpnEnabled; + private boolean mSelectedAlpnResolved; + + /* package */ SSLHttpRequestCodec(SSLEngineFactory factory) { + super(factory); + } + + @Override + protected SSLEngine createEngine(SSLEngineFactory factory) { + return enableALPN(super.createEngine(factory)); + } + + public void setSelectedAlpnProtocol(HttpProtocol protocol) { + mSelectedAlpnProtocol = protocol; + } + + public void setSelectedAlpnResolved() { + mSelectedAlpnResolved = true; + } + + public boolean selectedAlpnResolved() { + return mSelectedAlpnResolved; + } + + private SSLEngine enableALPN(SSLEngine sslEngine) { + if (sslEngine == null) { + return null; + } + if (mAlpnEnabled) { + return sslEngine; + } + mAlpnEnabled = true; + // Nothing to enable. + if (mSelectedAlpnProtocol == null) { + return sslEngine; + } + try { + String sslEngineName = sslEngine.getClass().getSimpleName(); + if (sslEngineName.equals("Java8EngineWrapper")) { + enableJava8EngineWrapperAlpn(sslEngine); + } else if (sslEngineName.equals("ConscryptEngine")) { + enableConscryptEngineAlpn(sslEngine); + } else { + enableOpenSSLEngineImplAlpn(sslEngine); + } + } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + NetBareLog.wtf(e); + } + return sslEngine; + } + + private void enableJava8EngineWrapperAlpn(SSLEngine sslEngine) throws NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + Method setApplicationProtocolsMethod = sslEngine.getClass().getDeclaredMethod( + "setApplicationProtocols", String[].class); + setApplicationProtocolsMethod.setAccessible(true); + String[] protocols = {mSelectedAlpnProtocol.toString().toLowerCase()}; + setApplicationProtocolsMethod.invoke(sslEngine, new Object[]{protocols}); + } + + private void enableConscryptEngineAlpn(SSLEngine sslEngine) throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + Method setAlpnProtocolsMethod = sslEngine.getClass().getDeclaredMethod( + "setAlpnProtocols", String[].class); + setAlpnProtocolsMethod.setAccessible(true); + String[] protocols = {mSelectedAlpnProtocol.toString().toLowerCase()}; + setAlpnProtocolsMethod.invoke(sslEngine, new Object[]{protocols}); + } + + private void enableOpenSSLEngineImplAlpn(SSLEngine sslEngine) throws NoSuchFieldException, + IllegalAccessException { + Field sslParametersField = sslEngine.getClass().getDeclaredField("sslParameters"); + sslParametersField.setAccessible(true); + Object sslParameters = sslParametersField.get(sslEngine); + if (sslParameters != null) { + Field alpnProtocolsField = sslParameters.getClass().getDeclaredField("alpnProtocols"); + alpnProtocolsField.setAccessible(true); + alpnProtocolsField.set(sslParameters, concatLengthPrefixed()); + } + } + + + private byte[] concatLengthPrefixed() { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + String protocolStr = mSelectedAlpnProtocol.toString().toLowerCase(); + os.write(protocolStr.length()); + os.write(protocolStr.getBytes(Charset.forName("UTF-8")), 0, protocolStr.length()); + return os.toByteArray(); + } + + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java new file mode 100644 index 0000000..27881c1 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLHttpResponseCodec.java @@ -0,0 +1,230 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import android.annotation.SuppressLint; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.ssl.SSLEngineFactory; +import com.github.megatronking.netbare.ssl.SSLResponseCodec; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import javax.net.ssl.SSLEngine; + +/** + * Http SSL codec enables Application-Layer Protocol Negotiation(ALPN). + * + * See http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 + * + * @author Megatron King + * @since 2019/1/3 23:31 + */ +/* package */ class SSLHttpResponseCodec extends SSLResponseCodec { + + private SSLEngine mSSLEngine; + + private boolean mAlpnEnabled; + private boolean mSelectedAlpnResolved; + + private HttpProtocol[] mClientAlpns; + private AlpnResolvedCallback mAlpnCallback; + + /* package */ SSLHttpResponseCodec(SSLEngineFactory factory) { + super(factory); + } + + @Override + protected SSLEngine createEngine(SSLEngineFactory factory) { + if (mSSLEngine == null) { + mSSLEngine = super.createEngine(factory); + if (mSSLEngine != null && mClientAlpns != null) { + enableAlpn(); + } + } + return mSSLEngine; + } + + @Override + public void decode(ByteBuffer buffer, @NonNull CodecCallback callback) throws IOException { + super.decode(buffer, callback); + // ALPN is put in ServerHello, once we receive the remote server packet, the ALPN must be + // resolved. + if (!mSelectedAlpnResolved) { + mAlpnCallback.onResult(getAlpnSelectedProtocol()); + } + mSelectedAlpnResolved = true; + } + + public void setSelectedAlpnResolved() { + mSelectedAlpnResolved = true; + } + + public void prepareHandshake(HttpProtocol[] protocols, AlpnResolvedCallback callback) + throws IOException { + this.mClientAlpns = protocols; + this.mAlpnCallback = callback; + super.prepareHandshake(); + } + + private void enableAlpn() { + try { + String sslEngineName = mSSLEngine.getClass().getSimpleName(); + if (sslEngineName.equals("Java8EngineWrapper")) { + enableJava8EngineWrapperAlpn(); + } else if (sslEngineName.equals("ConscryptEngine")) { + enableConscryptEngineAlpn(); + } else { + enableOpenSSLEngineImplAlpn(); + } + mAlpnEnabled = true; + } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + NetBareLog.wtf(e); + } + } + + private void enableJava8EngineWrapperAlpn() throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + Method setApplicationProtocolsMethod = mSSLEngine.getClass().getDeclaredMethod( + "setApplicationProtocols", String[].class); + setApplicationProtocolsMethod.setAccessible(true); + String[] protocols = new String[mClientAlpns.length]; + for (int i = 0; i < protocols.length; i++) { + protocols[i] = mClientAlpns[i].toString().toLowerCase(); + } + setApplicationProtocolsMethod.invoke(mSSLEngine, new Object[]{protocols}); + + Method setUseSessionTicketsMethod = mSSLEngine.getClass().getDeclaredMethod( + "setUseSessionTickets", boolean.class); + setUseSessionTicketsMethod.setAccessible(true); + setUseSessionTicketsMethod.invoke(mSSLEngine, true); + } + + private void enableConscryptEngineAlpn() throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + Method setAlpnProtocolsMethod = mSSLEngine.getClass().getDeclaredMethod( + "setAlpnProtocols", String[].class); + setAlpnProtocolsMethod.setAccessible(true); + String[] protocols = new String[mClientAlpns.length]; + for (int i = 0; i < protocols.length; i++) { + protocols[i] = mClientAlpns[i].toString().toLowerCase(); + } + setAlpnProtocolsMethod.invoke(mSSLEngine, new Object[]{protocols}); + + Method setUseSessionTicketsMethod = mSSLEngine.getClass().getDeclaredMethod( + "setUseSessionTickets", boolean.class); + setUseSessionTicketsMethod.setAccessible(true); + setUseSessionTicketsMethod.invoke(mSSLEngine, true); + } + + private void enableOpenSSLEngineImplAlpn() throws NoSuchFieldException, IllegalAccessException { + Field sslParametersField = mSSLEngine.getClass().getDeclaredField("sslParameters"); + sslParametersField.setAccessible(true); + Object sslParameters = sslParametersField.get(mSSLEngine); + if (sslParameters == null) { + throw new IllegalAccessException("sslParameters value is null"); + } + Field useSessionTicketsField = sslParameters.getClass().getDeclaredField("useSessionTickets"); + useSessionTicketsField.setAccessible(true); + useSessionTicketsField.set(sslParameters, true); + Field useSniField = sslParameters.getClass().getDeclaredField("useSni"); + useSniField.setAccessible(true); + useSniField.set(sslParameters, true); + Field alpnProtocolsField = sslParameters.getClass().getDeclaredField("alpnProtocols"); + alpnProtocolsField.setAccessible(true); + alpnProtocolsField.set(sslParameters, concatLengthPrefixed(mClientAlpns)); + } + + private byte[] concatLengthPrefixed(HttpProtocol ... protocols) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (HttpProtocol protocol : protocols) { + String protocolStr = protocol.toString().toLowerCase(); + os.write(protocolStr.length()); + os.write(protocolStr.getBytes(Charset.forName("UTF-8")), 0, protocolStr.length()); + } + return os.toByteArray(); + } + + + @SuppressLint("PrivateApi") + private String getAlpnSelectedProtocol() { + if (!mAlpnEnabled) { + return null; + } + String alpnResult = null; + try { + String sslEngineName = mSSLEngine.getClass().getSimpleName(); + if (sslEngineName.equals("Java8EngineWrapper")) { + alpnResult = getJava8EngineWrapperAlpn(); + } else if (sslEngineName.equals("ConscryptEngine")){ + alpnResult = getConscryptEngineAlpn(); + } else { + alpnResult = getOpenSSLEngineImplAlpn(); + } + } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException + | IllegalAccessException | InvocationTargetException e) { + NetBareLog.e(e.getMessage()); + } + return alpnResult; + } + + private String getJava8EngineWrapperAlpn() throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + Method getApplicationProtocolMethod = mSSLEngine.getClass().getDeclaredMethod( + "getApplicationProtocol"); + getApplicationProtocolMethod.setAccessible(true); + return (String) getApplicationProtocolMethod.invoke(mSSLEngine); + } + + private String getConscryptEngineAlpn() throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + Method getAlpnSelectedProtocolMethod = mSSLEngine.getClass().getDeclaredMethod( + "getAlpnSelectedProtocol"); + getAlpnSelectedProtocolMethod.setAccessible(true); + byte[] selectedProtocol = (byte[]) getAlpnSelectedProtocolMethod.invoke(mSSLEngine); + return selectedProtocol != null ? new String(selectedProtocol, Charset.forName("UTF-8")) : null; + } + + @SuppressLint("PrivateApi") + private String getOpenSSLEngineImplAlpn() throws ClassNotFoundException, NoSuchMethodException, + NoSuchFieldException, IllegalAccessException, InvocationTargetException { + Class nativeCryptoClass = Class.forName("com.android.org.conscrypt.NativeCrypto"); + Method SSL_get0_alpn_selectedMethod = nativeCryptoClass.getDeclaredMethod( + "SSL_get0_alpn_selected", long.class); + SSL_get0_alpn_selectedMethod.setAccessible(true); + + Field sslNativePointerField = mSSLEngine.getClass().getDeclaredField("sslNativePointer"); + sslNativePointerField.setAccessible(true); + long sslNativePointer = (long) sslNativePointerField.get(mSSLEngine); + byte[] selectedProtocol = (byte[]) SSL_get0_alpn_selectedMethod.invoke(null, sslNativePointer); + return selectedProtocol != null ? new String(selectedProtocol, Charset.forName("UTF-8")) : null; + } + + interface AlpnResolvedCallback { + + void onResult(String selectedAlpnProtocol) throws IOException; + + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java new file mode 100644 index 0000000..c6139ba --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxCallback.java @@ -0,0 +1,33 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A callback to receive SSL plaintext packets and input them to {@link javax.net.ssl.SSLEngine}. + * + * @author Megatron King + * @since 2018-11-15 14:35 + */ +public interface SSLRefluxCallback { + + void onRequest(HttpRequest request, ByteBuffer buffer) throws IOException; + + void onResponse(HttpResponse response, ByteBuffer buffer) throws IOException; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java new file mode 100644 index 0000000..611898a --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http/SSLRefluxInterceptor.java @@ -0,0 +1,58 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * An interceptor locates at the last layer of the interceptors. It is responsible for send + * plaintext packets to {@link SSLCodecInterceptor}. + * + * @author Megatron King + * @since 2018-11-15 15:39 + */ +/* package */ class SSLRefluxInterceptor extends HttpInterceptor { + + private SSLRefluxCallback mRefluxCallback; + + /* package */ SSLRefluxInterceptor(SSLRefluxCallback refluxCallback) { + this.mRefluxCallback = refluxCallback; + } + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) + throws IOException { + if (chain.request().isHttps()) { + mRefluxCallback.onRequest(chain.request(), buffer); + } else { + chain.process(buffer); + } + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) + throws IOException { + if (chain.response().isHttps()) { + mRefluxCallback.onResponse(chain.response(), buffer); + } else { + chain.process(buffer); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java new file mode 100644 index 0000000..13d6525 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/DecodeCallback.java @@ -0,0 +1,35 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A callback for HTTP2 header and data decodes. + * + * @author Megatron King + * @since 2019/1/5 20:36 + */ +/* package */ interface DecodeCallback { + + void onPending(ByteBuffer buffer); + + void onResult(ByteBuffer buffer, boolean isFinished) throws IOException; + + void onSkip(ByteBuffer buffer) throws IOException; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java new file mode 100644 index 0000000..02e6285 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/EncodeCallback.java @@ -0,0 +1,31 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A callback for HTTP2 header and data encodes. + * + * @author Megatron King + * @since 2019/1/6 21:54 + */ +/* package */ interface EncodeCallback { + + void onResult(ByteBuffer buffer) throws IOException; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java new file mode 100644 index 0000000..d144c93 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/ErrorCode.java @@ -0,0 +1,87 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.http2; + +/** + * http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-7 + * + * @author Megatron King + * @since 2019/1/11 23:10 + */ +/* package */ enum ErrorCode { + + /** + * Not an error! + */ + NO_ERROR(0), + + PROTOCOL_ERROR(1), + + INTERNAL_ERROR(2), + + FLOW_CONTROL_ERROR(3), + + SETTINGS_TIMEOUT(4), + + STREAM_CLOSED(5), + + FRAME_SIZE_ERROR(6), + + REFUSED_STREAM(7), + + CANCEL(8), + + COMPRESSION_ERROR(9), + + CONNECT_ERROR(0xa), + + ENHANCE_YOUR_CALM(0xb), + + INADEQUATE_SECURITY(0xc), + + HTTP_1_1_REQUIRED(0xd); + + public final int httpCode; + + ErrorCode(int httpCode) { + this.httpCode = httpCode; + } + + /* package */ static ErrorCode fromHttp2(int code) { + for (ErrorCode errorCode : ErrorCode.values()) { + if (errorCode.httpCode == code) { + return errorCode; + } + } + return null; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java new file mode 100644 index 0000000..e6780f6 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/FrameType.java @@ -0,0 +1,120 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +import androidx.annotation.Nullable; + +/** + * This specification defines a number of frame types, each identified by a unique 8-bit type code. + * + * See https://httpwg.org/specs/rfc7540.html#FrameTypes + * + * @author Megatron King + * @since 2019/1/5 15:30 + */ +/* package */ enum FrameType { + + /** + * DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with + * a stream. One or more DATA frames are used, for instance, to carry HTTP request or response + * payloads. + */ + DATA((byte)0x0), + + /** + * The HEADERS frame (type=0x1) is used to open a stream (Section 5.1), and additionally + * carries a header block fragment. HEADERS frames can be sent on a stream in the "idle", + * "reserved (local)", "open", or "half-closed (remote)" state. + */ + HEADERS((byte)0x1), + + /** + * The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream (Section 5.3). + * It can be sent in any stream state, including idle or closed streams. + */ + PRIORITY((byte)0x2), + + /** + * The RST_STREAM frame (type=0x3) allows for immediate termination of a stream. RST_STREAM is + * sent to request cancellation of a stream or to indicate that an error condition has occurred. + */ + RST_STREAM((byte)0x3), + + /** + * The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints + * communicate, such as preferences and constraints on peer behavior. The SETTINGS frame is + * also used to acknowledge the receipt of those parameters. Individually, a SETTINGS parameter + * can also be referred to as a "setting". + */ + SETTINGS((byte)0x4), + + /** + * The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams + * the sender intends to initiate. The PUSH_PROMISE frame includes the unsigned 31-bit + * identifier of the stream the endpoint plans to create along with a set of headers that + * provide additional context for the stream. + */ + PUSH_PROMISE((byte)0x5), + + /** + * The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the + * sender, as well as determining whether an idle connection is still functional. PING frames + * can be sent from any endpoint. + */ + PING((byte)0x6), + + /** + * The GOAWAY frame (type=0x7) is used to initiate shutdown of a connection or to signal + * serious error conditions. GOAWAY allows an endpoint to gracefully stop accepting new streams + * while still finishing processing of previously established streams. This enables + * administrative actions, like server maintenance. + */ + GOAWAY((byte)0x7), + + /** + * The WINDOW_UPDATE frame (type=0x8) is used to implement flow control. + */ + WINDOW_UPDATE((byte)0x8), + + /** + * The CONTINUATION frame (type=0x9) is used to continue a sequence of header block fragments + * (Section 4.3). Any number of CONTINUATION frames can be sent, as long as the preceding frame + * is on the same stream and is a HEADERS, PUSH_PROMISE, or CONTINUATION frame without the + * END_HEADERS flag set. + */ + CONTINUATION((byte)0x9); + + private final byte value; + + FrameType(byte value) { + this.value = value; + } + + /* package */ byte get() { + return value; + } + + @Nullable + /* package */ static FrameType parse(byte value) { + for (FrameType type : values()) { + if (type.value == value) { + return type; + } + } + return null; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java new file mode 100644 index 0000000..e28eaa0 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Hpack.java @@ -0,0 +1,753 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.http2; + +import android.text.TextUtils; + +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.http.HttpMethod; +import com.github.megatronking.netbare.http.HttpProtocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Read and write HPACK v10. + * + * https://httpwg.org/specs/rfc7540.html#HeaderBlock + * + * This implementation uses an array for the dynamic table and a list for indexed entries. Dynamic + * entries are added to the array, starting in the last position moving forward. When the array + * fills, it is doubled. + * + * @author Megatron King + * @since 2019/1/5 20:13 + */ +/* package */ final class Hpack { + + private static final int PREFIX_4_BITS = 0x0f; + private static final int PREFIX_5_BITS = 0x1f; + private static final int PREFIX_6_BITS = 0x3f; + private static final int PREFIX_7_BITS = 0x7f; + + private static final Header[] STATIC_HEADER_TABLE = new Header[] { + new Header(Header.TARGET_AUTHORITY, ""), + new Header(Header.TARGET_METHOD, "GET"), + new Header(Header.TARGET_METHOD, "POST"), + new Header(Header.TARGET_PATH, "/"), + new Header(Header.TARGET_PATH, "/index.html"), + new Header(Header.TARGET_SCHEME, "http"), + new Header(Header.TARGET_SCHEME, "https"), + new Header(Header.RESPONSE_STATUS, "200"), + new Header(Header.RESPONSE_STATUS, "204"), + new Header(Header.RESPONSE_STATUS, "206"), + new Header(Header.RESPONSE_STATUS, "304"), + new Header(Header.RESPONSE_STATUS, "400"), + new Header(Header.RESPONSE_STATUS, "404"), + new Header(Header.RESPONSE_STATUS, "500"), + new Header("accept-charset", ""), + new Header("accept-encoding", "gzip, deflate"), + new Header("accept-language", ""), + new Header("accept-ranges", ""), + new Header("accept", ""), + new Header("access-control-allow-origin", ""), + new Header("age", ""), + new Header("allow", ""), + new Header("authorization", ""), + new Header("cache-control", ""), + new Header("content-disposition", ""), + new Header("content-encoding", ""), + new Header("content-language", ""), + new Header("content-length", ""), + new Header("content-location", ""), + new Header("content-range", ""), + new Header("content-type", ""), + new Header("cookie", ""), + new Header("date", ""), + new Header("etag", ""), + new Header("expect", ""), + new Header("expires", ""), + new Header("from", ""), + new Header("host", ""), + new Header("if-match", ""), + new Header("if-modified-since", ""), + new Header("if-none-match", ""), + new Header("if-range", ""), + new Header("if-unmodified-since", ""), + new Header("last-modified", ""), + new Header("link", ""), + new Header("location", ""), + new Header("max-forwards", ""), + new Header("proxy-authenticate", ""), + new Header("proxy-authorization", ""), + new Header("range", ""), + new Header("referer", ""), + new Header("refresh", ""), + new Header("retry-after", ""), + new Header("server", ""), + new Header("set-cookie", ""), + new Header("strict-transport-security", ""), + new Header("transfer-encoding", ""), + new Header("user-agent", ""), + new Header("vary", ""), + new Header("via", ""), + new Header("www-authenticate", "") + }; + + private static final List HTTP_2_SKIPPED_REQUEST_HEADERS = Arrays.asList( + "connection", + "host", + "keep_alive", + "proxy_connection", + "te", + "transfer_encoding", + "encoding", + "upgrade"); + + private static final List HTTP_2_SKIPPED_RESPONSE_HEADERS = Arrays.asList( + "connection", + "host", + "keep_alive", + "proxy_connection", + "te", + "transfer_encoding", + "encoding", + "upgrade"); + + private static final Map NAME_TO_FIRST_INDEX = nameToFirstIndex(); + + private static Map nameToFirstIndex() { + Map result = new LinkedHashMap<>(STATIC_HEADER_TABLE.length); + for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { + if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { + result.put(STATIC_HEADER_TABLE[i].name, i); + } + } + return Collections.unmodifiableMap(result); + } + + private static final int DEFAULT_HEADER_TABLE_SIZE_SETTING = 4096; + private static final int SETTINGS_HEADER_TABLE_SIZE_LIMIT = 16384; + + static final class Reader { + + private final List

mHeaders; + + private int mHeaderTableSizeSetting; + + private Header[] mDynamicTable; + private int mNextHeaderIndex; + private int mHeaderCount; + private int mDynamicTableByteCount; + + private int mMaxDynamicTableByteCount; + + /* package */ Reader() { + this.mHeaders = new ArrayList<>(); + this.mDynamicTable = new Header[8]; + this.mNextHeaderIndex = mDynamicTable.length - 1; + this.mMaxDynamicTableByteCount = DEFAULT_HEADER_TABLE_SIZE_SETTING; + this.mHeaderTableSizeSetting = DEFAULT_HEADER_TABLE_SIZE_SETTING; + } + + void setHeaderTableSizeSetting(int headerTableSizeSetting) { + if (mHeaderTableSizeSetting == headerTableSizeSetting) { + return; + } + this.mHeaderTableSizeSetting = headerTableSizeSetting; + int effectiveHeaderTableSize = Math.min(headerTableSizeSetting, + SETTINGS_HEADER_TABLE_SIZE_LIMIT); + + if (mMaxDynamicTableByteCount == effectiveHeaderTableSize) { + return; // No change. + } + mMaxDynamicTableByteCount = effectiveHeaderTableSize; + adjustDynamicTableByteCount(); + } + + void readHeaders(ByteBuffer buffer, byte flags, DecodeCallback callback) throws IOException, + IndexOutOfBoundsException { + mHeaders.clear(); + while (buffer.hasRemaining()) { + int b = buffer.get() & 0xff; + if (b == 0x80) { // 10000000 + throw new IOException("Hpack read headers failed: index == 0"); + } else if ((b & 0x80) == 0x80) { // 1NNNNNNN + int index = readInt(buffer, b, PREFIX_7_BITS); + readIndexedHeader(index - 1); + } else if (b == 0x40) { // 01000000 + readLiteralHeaderWithIncrementalIndexingNewName(buffer); + } else if ((b & 0x40) == 0x40) { // 01NNNNNN + int index = readInt(buffer, b, PREFIX_6_BITS); + readLiteralHeaderWithIncrementalIndexingIndexedName(buffer, index - 1); + } else if ((b & 0x20) == 0x20) { // 001NNNNN + mMaxDynamicTableByteCount = readInt(buffer, b, PREFIX_5_BITS); + if (mMaxDynamicTableByteCount < 0 + || mMaxDynamicTableByteCount > mHeaderTableSizeSetting) { + throw new IOException("Hpack read headers failed: Invalid dynamic table " + + "size update " + mMaxDynamicTableByteCount); + } + adjustDynamicTableByteCount(); + } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. + readLiteralHeaderWithoutIndexingNewName(buffer); + } else { // 000?NNNN - Ignore never indexed bit. + int index = readInt(buffer, b, PREFIX_4_BITS); + readLiteralHeaderWithoutIndexingIndexedName(buffer, index - 1); + } + } + // Build normal http header part + String method = null; + String path = null; + String host = null; + String status = null; + List
headers = new ArrayList<>(); + for (Header header : mHeaders) { + if (header.name.equals(Header.TARGET_METHOD)) { + method = header.value; + } else if (header.name.equals(Header.TARGET_PATH)) { + path = header.value; + } else if (header.name.equals(Header.TARGET_AUTHORITY)) { + host = header.value; + } else if (header.name.equalsIgnoreCase("host")) { + host = header.value; + } else if (header.name.equalsIgnoreCase(Header.RESPONSE_STATUS)) { + status = header.value; + } else { + headers.add(header); + } + } + StringBuilder sb = new StringBuilder(); + if (method != null && path != null) { + sb.append(method).append(" ").append(path).append(" ").append(HttpProtocol.HTTP_2.toString()); + sb.append(NetBareUtils.LINE_END); + } + if (status != null) { + sb.append(HttpProtocol.HTTP_2.toString()).append(" ").append(status); + sb.append(NetBareUtils.LINE_END); + } + if (host != null) { + headers.add(0, new Header("Host", host)); + } + for (Header header : headers) { + if (header.name.equals(Header.TARGET_SCHEME)) { + continue; + } + sb.append(header.name).append(": "); + if (header.value != null) { + sb.append(header.value); + } + sb.append(NetBareUtils.LINE_END); + } + if ((flags & Http2.FLAG_END_HEADERS) != 0) { + sb.append(NetBareUtils.LINE_END); + } + callback.onResult(ByteBuffer.wrap(sb.toString().getBytes()), + (flags & Http2.FLAG_END_STREAM) != 0); + } + + private int readInt(ByteBuffer buffer, int firstByte, int prefixMask) { + int prefix = firstByte & prefixMask; + if (prefix < prefixMask) { + return prefix; // This was a single byte value. + } + + // This is a multibyte value. Read 7 bits at a time. + int result = prefixMask; + int shift = 0; + while (true) { + int b = buffer.get(); + if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255]. + result += (b & 0x7f) << shift; + shift += 7; + } else { + result += b << shift; // Last byte. + break; + } + } + return result; + } + + private int readByte(ByteBuffer buffer) { + return buffer.get() & 0xff; + } + + private void readIndexedHeader(int index) throws IOException { + if (isStaticHeader(index)) { + Header staticEntry = STATIC_HEADER_TABLE[index]; + mHeaders.add(staticEntry); + } else { + int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length); + if (dynamicTableIndex < 0 || dynamicTableIndex >= mDynamicTable.length) { + throw new IOException("Hpack read headers failed: Header index too large " + + (index + 1)); + } + Header header = mDynamicTable[dynamicTableIndex]; + if (header == null) { + throw new IOException("Hpack read headers failed: read dynamic table failed!"); + } + mHeaders.add(header); + } + } + + private int dynamicTableIndex(int index) { + return mNextHeaderIndex + 1 + index; + } + + private boolean isStaticHeader(int index) { + return index >= 0 && index <= STATIC_HEADER_TABLE.length - 1; + } + + private void readLiteralHeaderWithoutIndexingIndexedName(ByteBuffer buffer, int index) + throws IOException { + String name = getName(index); + String value = readString(buffer); + mHeaders.add(new Header(name, value)); + } + + private String getName(int index) throws IOException { + if (isStaticHeader(index)) { + return STATIC_HEADER_TABLE[index].name; + } else { + int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length); + if (dynamicTableIndex < 0 || dynamicTableIndex >= mDynamicTable.length) { + throw new IOException("Hpack read headers failed: Header index too large " + + (index + 1)); + } + return mDynamicTable[dynamicTableIndex].name; + } + } + + private String readString(ByteBuffer buffer) throws IOException { + int firstByte = readByte(buffer); + boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN + int length = readInt(buffer, firstByte, PREFIX_7_BITS); + if (buffer.remaining() < length) { + throw new IOException("Hpack read headers failed: data not enough, expect: " + + length + " actual: " + buffer.remaining()); + } + byte[] data = new byte[length]; + buffer.get(data); + return new String(huffmanDecode ? Huffman.get().decode(data) : data); + } + + private void readLiteralHeaderWithIncrementalIndexingNewName(ByteBuffer buffer) + throws IOException { + String name = checkLowercase(readString(buffer)); + String value = readString(buffer); + insertIntoDynamicTable(-1, new Header(name, value)); + } + + private void readLiteralHeaderWithIncrementalIndexingIndexedName(ByteBuffer buffer, + int nameIndex) + throws IOException { + String name = getName(nameIndex); + String value = readString(buffer); + insertIntoDynamicTable(-1, new Header(name, value)); + } + + private void readLiteralHeaderWithoutIndexingNewName(ByteBuffer buffer) throws IOException { + String name = checkLowercase(readString(buffer)); + String value = readString(buffer); + mHeaders.add(new Header(name, value)); + } + + private void insertIntoDynamicTable(int index, Header entry) throws IOException { + mHeaders.add(entry); + + int delta = entry.hpackSize(); + if (index != -1) { // Index -1 == new header. + Header header = mDynamicTable[dynamicTableIndex(index)]; + if (header == null) { + throw new IOException("Hpack read headers failed: insert dynamic table failed!"); + } + delta -= header.hpackSize(); + } + + // if the new or replacement header is too big, drop all entries. + if (delta > mMaxDynamicTableByteCount) { + clearDynamicTable(); + return; + } + + // Evict headers to the required length. + int bytesToRecover = (mDynamicTableByteCount + delta) - mMaxDynamicTableByteCount; + int entriesEvicted = evictToRecoverBytes(bytesToRecover); + + if (index == -1) { // Adding a value to the dynamic table. + if (mHeaderCount + 1 > mDynamicTable.length) { // Need to grow the dynamic table. + Header[] doubled = new Header[mDynamicTable.length * 2]; + System.arraycopy(mDynamicTable, 0, doubled, mDynamicTable.length, + mDynamicTable.length); + mNextHeaderIndex = mDynamicTable.length - 1; + mDynamicTable = doubled; + } + index = mNextHeaderIndex--; + mDynamicTable[index] = entry; + mHeaderCount++; + } else { // Replace value at same position. + index += dynamicTableIndex(index) + entriesEvicted; + mDynamicTable[index] = entry; + } + mDynamicTableByteCount += delta; + } + + private void clearDynamicTable() { + Arrays.fill(mDynamicTable, null); + mNextHeaderIndex = mDynamicTable.length - 1; + mHeaderCount = 0; + mDynamicTableByteCount = 0; + } + + private int evictToRecoverBytes(int bytesToRecover) { + int entriesToEvict = 0; + if (bytesToRecover > 0) { + // determine how many headers need to be evicted. + for (int j = mDynamicTable.length - 1; j >= mNextHeaderIndex && bytesToRecover > 0; j--) { + bytesToRecover -= mDynamicTable[j].hpackSize(); + mDynamicTableByteCount -= mDynamicTable[j].hpackSize(); + mHeaderCount--; + entriesToEvict++; + } + System.arraycopy(mDynamicTable, mNextHeaderIndex + 1, mDynamicTable, + mNextHeaderIndex + 1 + entriesToEvict, mHeaderCount); + mNextHeaderIndex += entriesToEvict; + } + return entriesToEvict; + } + + private void adjustDynamicTableByteCount() { + if (mMaxDynamicTableByteCount < mDynamicTableByteCount) { + if (mMaxDynamicTableByteCount == 0) { + clearDynamicTable(); + } else { + evictToRecoverBytes(mDynamicTableByteCount - mMaxDynamicTableByteCount); + } + } + } + + private String checkLowercase(String name) throws IOException { + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c >= 'A' && c <= 'Z') { + throw new IOException("Hpack read headers failed: mixed case name: " + name); + } + } + return name; + } + + } + + static final class Writer { + + private int mSmallestHeaderTableSizeSetting; + private boolean mEmitDynamicTableSizeUpdate; + + private int mHeaderTableSizeSetting; + private int mMaxDynamicTableByteCount; + + // Visible for testing. + private Header[] mDynamicTable; + // Array is populated back to front, so new entries always have lowest index. + private int mNextHeaderIndex; + private int mHeaderCount = 0; + private int mDynamicTableByteCount = 0; + + private ByteArrayOutputStream mOut; + + Writer() { + this.mDynamicTable = new Header[8]; + this.mNextHeaderIndex = mDynamicTable.length - 1; + this.mSmallestHeaderTableSizeSetting = Integer.MAX_VALUE; + this.mHeaderTableSizeSetting = DEFAULT_HEADER_TABLE_SIZE_SETTING; + this.mMaxDynamicTableByteCount = DEFAULT_HEADER_TABLE_SIZE_SETTING; + } + + byte[] writeRequestHeaders(HttpMethod method, String path, String host, + Map> headers) throws IOException { + mOut = new ByteArrayOutputStream(); + List
hpackHeaders = new ArrayList<>(); + hpackHeaders.add(new Header(Header.TARGET_METHOD, method.name())); + hpackHeaders.add(new Header(Header.TARGET_PATH, path)); + hpackHeaders.add(new Header(Header.TARGET_AUTHORITY, host)); + hpackHeaders.add(new Header(Header.TARGET_SCHEME, "https")); + for (Map.Entry> entry : headers.entrySet()) { + if (HTTP_2_SKIPPED_REQUEST_HEADERS.contains(entry.getKey().toLowerCase())) { + continue; + } + for (String value : entry.getValue()) { + hpackHeaders.add(new Header(entry.getKey(), value)); + } + } + return writeHeaders(hpackHeaders); + } + + byte[] writeResponseHeaders(int code, String message, Map> headers) + throws IOException { + mOut = new ByteArrayOutputStream(); + List
hpackHeaders = new ArrayList<>(); + hpackHeaders.add(new Header(Header.RESPONSE_STATUS, TextUtils.isEmpty(message) ? String.valueOf(code) : + code + " " + message)); + for (Map.Entry> entry : headers.entrySet()) { + if (HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(entry.getKey().toLowerCase())) { + continue; + } + for (String value : entry.getValue()) { + hpackHeaders.add(new Header(entry.getKey(), value)); + } + } + return writeHeaders(hpackHeaders); + } + + private void clearDynamicTable() { + Arrays.fill(mDynamicTable, null); + mNextHeaderIndex = mDynamicTable.length - 1; + mHeaderCount = 0; + mDynamicTableByteCount = 0; + } + + private int evictToRecoverBytes(int bytesToRecover) { + int entriesToEvict = 0; + if (bytesToRecover > 0) { + // determine how many headers need to be evicted. + for (int j = mDynamicTable.length - 1; j >= mNextHeaderIndex && bytesToRecover > 0; j--) { + bytesToRecover -= mDynamicTable[j].hpackSize(); + mDynamicTableByteCount -= mDynamicTable[j].hpackSize(); + mHeaderCount--; + entriesToEvict++; + } + System.arraycopy(mDynamicTable, mNextHeaderIndex + 1, mDynamicTable, + mNextHeaderIndex + 1 + entriesToEvict, mHeaderCount); + Arrays.fill(mDynamicTable, mNextHeaderIndex + 1, mNextHeaderIndex + 1 + entriesToEvict, null); + mNextHeaderIndex += entriesToEvict; + } + return entriesToEvict; + } + + private void insertIntoDynamicTable(Header entry) { + int delta = entry.hpackSize(); + + // if the new or replacement header is too big, drop all entries. + if (delta > mMaxDynamicTableByteCount) { + clearDynamicTable(); + return; + } + + // Evict headers to the required length. + int bytesToRecover = (mDynamicTableByteCount + delta) - mMaxDynamicTableByteCount; + evictToRecoverBytes(bytesToRecover); + + if (mHeaderCount + 1 > mDynamicTable.length) { // Need to grow the dynamic table. + Header[] doubled = new Header[mDynamicTable.length * 2]; + System.arraycopy(mDynamicTable, 0, doubled, mDynamicTable.length, mDynamicTable.length); + mNextHeaderIndex = mDynamicTable.length - 1; + mDynamicTable = doubled; + } + int index = mNextHeaderIndex--; + mDynamicTable[index] = entry; + mHeaderCount++; + mDynamicTableByteCount += delta; + } + + private byte[] writeHeaders(List
headerBlock) throws IOException { + if (mEmitDynamicTableSizeUpdate) { + if (mSmallestHeaderTableSizeSetting < mMaxDynamicTableByteCount) { + // Multiple dynamic table size updates! + writeInt(mSmallestHeaderTableSizeSetting, PREFIX_5_BITS, 0x20); + } + mEmitDynamicTableSizeUpdate = false; + mSmallestHeaderTableSizeSetting = Integer.MAX_VALUE; + writeInt(mMaxDynamicTableByteCount, PREFIX_5_BITS, 0x20); + } + + for (int i = 0, size = headerBlock.size(); i < size; i++) { + Header header = headerBlock.get(i); + String name = header.name.toLowerCase(); + String value = header.value; + int headerIndex = -1; + int headerNameIndex = -1; + + Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); + if (staticIndex != null) { + headerNameIndex = staticIndex + 1; + if (headerNameIndex > 1 && headerNameIndex < 8) { + // Only search a subset of the static header table. Most entries have an empty value, so + // it's unnecessary to waste cycles looking at them. This check is built on the + // observation that the header entries we care about are in adjacent pairs, and we + // always know the first index of the pair. + if (TextUtils.equals(STATIC_HEADER_TABLE[headerNameIndex - 1].value, value)) { + headerIndex = headerNameIndex; + } else if (TextUtils.equals(STATIC_HEADER_TABLE[headerNameIndex].value, value)) { + headerIndex = headerNameIndex + 1; + } + } + } + + if (headerIndex == -1) { + for (int j = mNextHeaderIndex + 1, length = mDynamicTable.length; j < length; j++) { + if (TextUtils.equals(mDynamicTable[j].name, name)) { + if (TextUtils.equals(mDynamicTable[j].value, value)) { + headerIndex = j - mNextHeaderIndex + STATIC_HEADER_TABLE.length; + break; + } else if (headerNameIndex == -1) { + headerNameIndex = j - mNextHeaderIndex + STATIC_HEADER_TABLE.length; + } + } + } + } + + if (headerIndex != -1) { + // Indexed Header Field. + writeInt(headerIndex, PREFIX_7_BITS, 0x80); + } else if (headerNameIndex == -1) { + // Literal Header Field with Incremental Indexing - New Name. + mOut.write(0x40); + writeString(name); + writeString(value); + insertIntoDynamicTable(header); + } else if (name.startsWith(Header.PSEUDO_PREFIX) && !Header.TARGET_AUTHORITY.equals(name)) { + // Follow Chromes lead - only include the :authority pseudo header, but exclude all other + // pseudo headers. Literal Header Field without Indexing - Indexed Name. + writeInt(headerNameIndex, PREFIX_4_BITS, 0); + writeString(value); + } else { + // Literal Header Field with Incremental Indexing - Indexed Name. + writeInt(headerNameIndex, PREFIX_6_BITS, 0x40); + writeString(value); + insertIntoDynamicTable(header); + } + } + return mOut.toByteArray(); + } + + private void writeInt(int value, int prefixMask, int bits) { + // Write the raw value for a single byte value. + if (value < prefixMask) { + mOut.write(bits | value); + return; + } + + // Write the mask to start a multibyte value. + mOut.write(bits | prefixMask); + value -= prefixMask; + + // Write 7 bits at a time 'til we're done. + while (value >= 0x80) { + int b = value & 0x7f; + mOut.write(b | 0x80); + value >>>= 7; + } + mOut.write(value); + } + + private void writeString(String data) throws IOException { + byte[] stringBytes = data.getBytes(); + if (Huffman.get().encodedLength(data) < stringBytes.length) { + ByteBuffer buffer = ByteBuffer.allocate(stringBytes.length); + Huffman.get().encode(data, buffer); + buffer.flip(); + writeInt(buffer.remaining(), PREFIX_7_BITS, 0x80); + mOut.write(buffer.array(), buffer.position(), buffer.remaining()); + } else { + writeInt(stringBytes.length, PREFIX_7_BITS, 0); + mOut.write(data.getBytes()); + } + } + + void setHeaderTableSizeSetting(int headerTableSizeSetting) { + if (mHeaderTableSizeSetting == headerTableSizeSetting) { + return; + } + this.mHeaderTableSizeSetting = headerTableSizeSetting; + int effectiveHeaderTableSize = Math.min(headerTableSizeSetting, + SETTINGS_HEADER_TABLE_SIZE_LIMIT); + + if (mMaxDynamicTableByteCount == effectiveHeaderTableSize) { + return; // No change. + } + + if (effectiveHeaderTableSize < mMaxDynamicTableByteCount) { + mSmallestHeaderTableSizeSetting = Math.min(mSmallestHeaderTableSizeSetting, + effectiveHeaderTableSize); + } + mEmitDynamicTableSizeUpdate = true; + mMaxDynamicTableByteCount = effectiveHeaderTableSize; + adjustDynamicTableByteCount(); + } + + private void adjustDynamicTableByteCount() { + if (mMaxDynamicTableByteCount < mDynamicTableByteCount) { + if (mMaxDynamicTableByteCount == 0) { + clearDynamicTable(); + } else { + evictToRecoverBytes(mDynamicTableByteCount - mMaxDynamicTableByteCount); + } + } + } + + } + + private static final class Header { + + // Special header names defined in HTTP/2 spec. + private static final String PSEUDO_PREFIX = ":"; + private static final String RESPONSE_STATUS = ":status"; + private static final String TARGET_METHOD = ":method"; + private static final String TARGET_PATH = ":path"; + private static final String TARGET_SCHEME = ":scheme"; + private static final String TARGET_AUTHORITY = ":authority"; + + private String name; + private String value; + + private Header(String name, String value) { + this.name = name; + this.value = value; + } + + private int hpackSize() { + return 32 + name.getBytes().length + value.getBytes().length; + } + + @Override + public String toString() { + return name + ": " + value; + } + + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java new file mode 100644 index 0000000..2fd0af8 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2.java @@ -0,0 +1,77 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +/** + * HTTP2 protocol constants and common methods. + * + * See https://httpwg.org/specs/rfc7540.html + * + * @author Megatron King + * @since 2019/1/5 14:14 + */ +public final class Http2 { + + /** + * In HTTP/2, each endpoint is required to send a connection preface as a final confirmation of + * the protocol in use and to establish the initial settings for the HTTP/2 connection. + */ + public static final byte[] CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(); + + static final int FRAME_HEADER_LENGTH = 9; + + /** + * The initial max frame size, applied independently writing to, or reading from the peer. + * + * 0x4000 = 2^14 = 16384 + */ + static final int INITIAL_MAX_FRAME_SIZE = 0x4000; + + + static final byte FLAG_NONE = 0x0; + + /** + * Used for settings and ping. + */ + static final byte FLAG_ACK = 0x1; + + /** + * Used for headers and data. + */ + static final byte FLAG_END_STREAM = 0x1; + + /** + * Used for headers and continuation. + */ + static final byte FLAG_END_HEADERS = 0x4; + static final byte FLAG_END_PUSH_PROMISE = 0x4; + + /** + * Used for headers and data. + */ + static final byte FLAG_PADDED = 0x8; + + /** + * Used for headers. + */ + static final byte FLAG_PRIORITY = 0x20; + + /** + * Used for data. + */ + static final byte FLAG_COMPRESSED = 0x20; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java new file mode 100644 index 0000000..b533ecd --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2DecodeInterceptor.java @@ -0,0 +1,481 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.http.HttpId; +import com.github.megatronking.netbare.http.HttpPendingInterceptor; +import com.github.megatronking.netbare.http.HttpProtocol; +import com.github.megatronking.netbare.http.HttpRequest; +import com.github.megatronking.netbare.http.HttpRequestChain; +import com.github.megatronking.netbare.http.HttpResponse; +import com.github.megatronking.netbare.http.HttpResponseChain; +import com.github.megatronking.netbare.http.HttpZygoteRequest; +import com.github.megatronking.netbare.http.HttpZygoteResponse; +import com.github.megatronking.netbare.http.SSLRefluxCallback; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Decodes HTTP2 request and response packets. + * + * @author Megatron King + * @since 2019/1/5 14:19 + */ +public final class Http2DecodeInterceptor extends HttpPendingInterceptor { + + private final SSLRefluxCallback mRefluxCallback; + + private final HttpZygoteRequest mZygoteRequest; + private final HttpZygoteResponse mZygoteResponse; + + private final Map mHttpIds; + + private final Http2Stream mRequestStream; + private final Http2Stream mResponseStream; + + private Hpack.Reader mHpackRequestReader; + private Hpack.Reader mHpackResponseReader; + + private NetBareXLog mLog; + + public Http2DecodeInterceptor(SSLRefluxCallback refluxCallback, HttpZygoteRequest zygoteRequest, + HttpZygoteResponse zygoteResponse) { + this.mRefluxCallback = refluxCallback; + + this.mZygoteRequest = zygoteRequest; + this.mZygoteResponse = zygoteResponse; + + this.mHttpIds = new ConcurrentHashMap<>(); + + this.mRequestStream = new Http2Stream(); + this.mResponseStream = new Http2Stream(); + } + + @Override + protected void intercept(@NonNull final HttpRequestChain chain, @NonNull ByteBuffer buffer, + int index) throws IOException { + if (chain.request().httpProtocol() == HttpProtocol.HTTP_2) { + if (!buffer.hasRemaining()) { + return; + } + if (mLog == null) { + HttpRequest request = chain.request(); + mLog = new NetBareXLog(request.protocol(), request.ip(), request.port()); + } + if (mHpackRequestReader == null) { + mHpackRequestReader = new Hpack.Reader(); + } + decode(mergeRequestBuffer(buffer), mHpackRequestReader, new DecodeCallback() { + + @Override + public void onPending(ByteBuffer buffer) { + pendRequestBuffer(buffer); + } + + @Override + public void onResult(ByteBuffer buffer, boolean isFinished) throws IOException { + int streamId = mRequestStream.id; + if (streamId < 0) { + throw new IOException("Http2 stream id is < 0"); + } + HttpId id = mHttpIds.get(streamId); + if (id == null) { + id = new HttpId(streamId); + mHttpIds.put(streamId, id); + } + mZygoteRequest.zygote(id); + if (isFinished) { + mZygoteRequest.onStreamFinished(); + } + if (!buffer.hasRemaining()) { + return; + } + chain.process(buffer); + } + + @Override + public void onSkip(ByteBuffer buffer) throws IOException { + mRefluxCallback.onRequest(chain.request(), buffer); + } + + }, mRequestStream, new Http2Updater() { + @Override + public void onSettingsUpdate(Http2Settings http2Settings) { + mZygoteRequest.onSettingsUpdate(http2Settings); + if (http2Settings.getHeaderTableSize() > 0) { + if (mHpackResponseReader == null) { + mHpackResponseReader = new Hpack.Reader(); + } + mHpackResponseReader.setHeaderTableSizeSetting(http2Settings.getHeaderTableSize()); + } + } + + @Override + public void onStreamFinished() { + mZygoteRequest.onStreamFinished(); + } + }); + } else { + chain.process(buffer); + } + } + + @Override + protected void intercept(@NonNull final HttpResponseChain chain, @NonNull ByteBuffer buffer, + int index) + throws IOException { + if (chain.response().httpProtocol() == HttpProtocol.HTTP_2) { + if (!buffer.hasRemaining()) { + return; + } + if (mLog == null) { + HttpResponse response = chain.response(); + mLog = new NetBareXLog(response.protocol(), response.ip(), response.port()); + } + if (mHpackResponseReader == null) { + mHpackResponseReader = new Hpack.Reader(); + } + decode(mergeResponseBuffer(buffer), mHpackResponseReader, new DecodeCallback() { + + @Override + public void onPending(ByteBuffer buffer) { + pendResponseBuffer(buffer); + } + + @Override + public void onResult(ByteBuffer buffer, boolean isFinished) throws IOException { + int streamId = mResponseStream.id; + if (streamId < 0) { + throw new IOException("Http2 stream id is < 0"); + } + HttpId id = mHttpIds.get(streamId); + if (id == null) { + id = new HttpId(streamId); + mHttpIds.put(streamId, id); + } + mZygoteResponse.zygote(id); + if (isFinished) { + mZygoteResponse.onStreamFinished(); + } + if (!buffer.hasRemaining()) { + return; + } + chain.process(buffer); + } + + @Override + public void onSkip(ByteBuffer buffer) throws IOException { + mRefluxCallback.onResponse(chain.response(), buffer); + } + + }, mResponseStream, new Http2Updater() { + @Override + public void onSettingsUpdate(Http2Settings http2Settings) { + mZygoteResponse.onSettingsUpdate(http2Settings); + if (http2Settings.getHeaderTableSize() > 0) { + if (mHpackRequestReader == null) { + mHpackRequestReader = new Hpack.Reader(); + } + mHpackRequestReader.setHeaderTableSizeSetting(http2Settings.getHeaderTableSize()); + } + } + + @Override + public void onStreamFinished() { + mZygoteResponse.onStreamFinished(); + } + }); + } else { + chain.process(buffer); + } + } + + private void decode(ByteBuffer buffer, Hpack.Reader reader, DecodeCallback callback, + Http2Stream stream, Http2Updater updater) + throws IOException { + // HTTP2 frame structure + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Length (24) | + // +---------------+---------------+---------------+ + // | Type (8) | Flags (8) | + // +-+-+-----------+---------------+-------------------------------+ + // |R| Stream Identifier (31) | + // +=+=============================================================+ + // | Frame Payload (0...) ... + // +---------------------------------------------------------------+ + if (buffer.remaining() < Http2.FRAME_HEADER_LENGTH) { + callback.onPending(buffer); + return; + } + int length = readMedium(buffer); + if (length < 0 || length > Http2.INITIAL_MAX_FRAME_SIZE) { + // Values greater than 214 (16,384) MUST NOT be sent unless the receiver has set a + // larger value for SETTINGS_MAX_FRAME_SIZE. + throw new IOException("Http2 frame size error: " + length); + } + // Check payload length + if (length + 6 > buffer.remaining()) { + mLog.w("No enough http2 frame length, expect: %d actual: %d", length, + buffer.remaining() - 6); + // Packet not enough for one frame, wait next packet. + // Revert position. + buffer.position(buffer.position() - 3); + callback.onPending(buffer); + return; + } else if (length + 6 < buffer.remaining()) { + mLog.w("Multi http2 frames in one buffer, first frame length : %d, buffer length: %d", + length + 9, buffer.remaining() + 3); + // Separate multi-frames + ByteBuffer newBuffer = ByteBuffer.allocate(length + 9); + newBuffer.put(buffer.array(), buffer.position() - 3, newBuffer.capacity()); + newBuffer.flip(); + decode(newBuffer, reader, callback, stream, updater); + // Process the left data + buffer.position(buffer.position() + length + 6); + decode(buffer, reader, callback, stream, updater); + return; + } + + byte type = (byte) (buffer.get() & 0xff); + byte flags = (byte) (buffer.get() & 0xff); + int streamId = buffer.getInt() & 0x7fffffff; + FrameType frameType = FrameType.parse(type); + if (frameType == null) { + mLog.e("Unexpected http2 frame type: " + type); + // Discard frames that have unknown or unsupported types. + return; + } + if (stream.id != -1) { + if (streamId != stream.id && frameType == FrameType.CONTINUATION) { + throw new IOException("Http2 TYPE_CONTINUATION streamId changed!"); + } + } + mLog.i("Decode a http2 frame: " + frameType + " stream(" + streamId + + ") length(" + length + ")"); + stream.id = streamId; + switch (frameType) { + case DATA: + decodeData(buffer, length, flags, streamId, callback); + return; + case HEADERS: + case CONTINUATION: + decodeHeaders(buffer, reader, length, flags, streamId, callback); + return; + case SETTINGS: + decodeSettings(buffer, length, flags, streamId, updater); + // No return + break; + case GOAWAY: + decodeGoAway(buffer, length, flags, streamId); + // No return + break; + default: + break; + } + // Encrypt and send it to remote server directly. + buffer.position(buffer.position() - Http2.FRAME_HEADER_LENGTH); + callback.onSkip(buffer); + } + + private int readMedium(ByteBuffer buffer) { + return (buffer.get() & 0xff) << 16 + | (buffer.get() & 0xff) << 8 + | (buffer.get() & 0xff); + } + + private void decodeSettings(ByteBuffer buffer, int length, byte flags, int streamId, + Http2Updater receiver) + throws IOException { + if (streamId != 0) { + throw new IOException("Http2 TYPE_SETTINGS streamId != 0"); + } + if ((flags & Http2.FLAG_ACK) != 0) { + if (length != 0) { + throw new IOException("Http2 FRAME_SIZE_ERROR ack frame should be empty!"); + } + mLog.i("Http2 ack the settings"); + return; + } + if (length % 6 != 0) { + throw new IOException("Http2 TYPE_SETTINGS length %% 6 != 0: " + length); + } + int initPosition = buffer.position(); + Http2Settings settings = new Http2Settings(); + for (int i = 0; i < length; i += 6) { + int id = buffer.getShort() & 0xFFFF; + int value = buffer.getInt(); + switch (id) { + case 1: // SETTINGS_HEADER_TABLE_SIZE + mLog.i("Http2 SETTINGS_HEADER_TABLE_SIZE: " + value); + break; + case 2: // SETTINGS_ENABLE_PUSH + if (value != 0 && value != 1) { + throw new IOException("Http2 PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); + } + break; + case 3: // SETTINGS_MAX_CONCURRENT_STREAMS + id = 4; // Renumbered in draft 10. + mLog.i("Http2 SETTINGS_MAX_CONCURRENT_STREAMS: " + value); + break; + case 4: // SETTINGS_INITIAL_WINDOW_SIZE + id = 7; // Renumbered in draft 10. + if (value < 0) { + throw new IOException("Http2 PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); + } + mLog.i("Http2 SETTINGS_INITIAL_WINDOW_SIZE: " + value); + break; + case 5: // SETTINGS_MAX_FRAME_SIZE + if (value < Http2.INITIAL_MAX_FRAME_SIZE || value > 16777215) { + throw new IOException("Http2 PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: " + value); + } + mLog.i("Http2 INITIAL_MAX_FRAME_SIZE: " + value); + break; + case 6: // SETTINGS_MAX_HEADER_LIST_SIZE + break; // Advisory only, so ignored. + default: + break; // Must ignore setting with unknown id. + } + settings.set(id, value); + } + // Reverse the position and sent to terminal. + buffer.position(initPosition); + receiver.onSettingsUpdate(settings); + } + + private void decodeHeaders(ByteBuffer buffer, Hpack.Reader reader, int length, byte flags, + int streamId, DecodeCallback callback) throws IOException { + // +---------------+ + // |Pad Length? (8)| + // +-+-------------+-----------------------------------------------+ + // |E| Stream Dependency? (31) | + // +-+-------------+-----------------------------------------------+ + // | Weight? (8) | + // +-+-------------+-----------------------------------------------+ + // | Header Block Fragment (*) ... + // +---------------------------------------------------------------+ + // | Padding (*) ... + // +---------------------------------------------------------------+ + if (streamId == 0) { + throw new IOException("Http2 PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); + } + short padding = (flags & Http2.FLAG_PADDED) != 0 ? (short) (buffer.get() & 0xff) : 0; + if ((flags & Http2.FLAG_PRIORITY) != 0) { + // Skip priority. + buffer.position(buffer.position() + 5); + } + length = lengthWithoutPadding(length, flags, padding); + boolean endStream = (flags & Http2.FLAG_END_STREAM) != 0; + if (length > 0) { + decodeHeaderBlock(buffer, reader, flags, callback); + } else { + // Notify stream is end + callback.onResult(ByteBuffer.allocate(0), endStream); + if (endStream) { + callback.onSkip(endStream(FrameType.HEADERS, streamId)); + } + } + } + + private void decodeHeaderBlock(ByteBuffer buffer, Hpack.Reader reader, byte flags, + DecodeCallback callback) throws IOException { + try { + reader.readHeaders(buffer, flags, callback); + } catch (IndexOutOfBoundsException e) { + throw new IOException("Http2 decode header block failed."); + } + } + + private void decodeData(ByteBuffer buffer, int length, byte flags, int streamId, + DecodeCallback callback) throws IOException { + if (streamId == 0) { + throw new IOException("Http2 PROTOCOL_ERROR: TYPE_DATA streamId == 0"); + } + boolean gzipped = (flags & Http2.FLAG_COMPRESSED) != 0; + if (gzipped) { + throw new IOException("Http2 PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); + } + short padding = (flags & Http2.FLAG_PADDED) != 0 ? (short) (buffer.get() & 0xff) : 0; + length = lengthWithoutPadding(length, flags, padding); + boolean endStream = (flags & Http2.FLAG_END_STREAM) != 0; + if (length > 0) { + callback.onResult(ByteBuffer.wrap(Arrays.copyOfRange(buffer.array(), buffer.position(), + buffer.position() + length)), endStream); + } else { + // Notify stream is end + callback.onResult(ByteBuffer.allocate(0), endStream); + if (endStream) { + callback.onSkip(endStream(FrameType.DATA, streamId)); + } + } + } + + private void decodeGoAway(ByteBuffer buffer, int length, byte flags, int streamId) + throws IOException { + if (length < 8) { + throw new IOException("Http2 TYPE_GOAWAY length < 8: " + length); + } + if (streamId != 0) { + throw new IOException("Http2 TYPE_GOAWAY streamId != 0"); + } + int initPosition = buffer.position(); + int lastStreamId = buffer.getInt(); + int errorCodeInt = buffer.getInt(); + int opaqueDataLength = length - 8; + ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); + if (errorCode == null) { + throw new IOException("Http2 TYPE_GOAWAY unexpected error code: " + errorCodeInt); + } + mLog.e("Http2 TYPE_GOAWAY error code: " + errorCode + " last stream: " + lastStreamId); + if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. + byte[] debugData = new byte[opaqueDataLength]; + buffer.get(debugData); + mLog.e("Http2 TYPE_GOAWAY debug data: " + new String(debugData)); + } + buffer.position(initPosition); + } + + private int lengthWithoutPadding(int length, byte flags, short padding) throws IOException { + if ((flags & Http2.FLAG_PADDED) != 0) { + length--; // Account for reading the padding length. + } + if (padding > length) { + throw new IOException("Http2 PROTOCOL_ERROR padding " + padding + " > remaining length " + length); + } + return (short) (length - padding); + } + + private ByteBuffer endStream(FrameType frameType, int streamId) { + ByteBuffer endBuffer = ByteBuffer.allocate(Http2.FRAME_HEADER_LENGTH); + endBuffer.put((byte) 0); + endBuffer.put((byte) 0); + endBuffer.put((byte) 0); + endBuffer.put((byte) (frameType.get() & 0xff)); + endBuffer.put((byte) (Http2.FLAG_END_STREAM & 0xff)); + endBuffer.putInt(streamId & 0x7fffffff); + endBuffer.flip(); + return endBuffer; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java new file mode 100644 index 0000000..26df14b --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2EncodeInterceptor.java @@ -0,0 +1,245 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.gateway.InterceptorChain; +import com.github.megatronking.netbare.http.HttpInterceptor; +import com.github.megatronking.netbare.http.HttpProtocol; +import com.github.megatronking.netbare.http.HttpRequest; +import com.github.megatronking.netbare.http.HttpRequestChain; +import com.github.megatronking.netbare.http.HttpResponse; +import com.github.megatronking.netbare.http.HttpResponseChain; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Encodes HTTP2 request and response packets. + * + * @author Megatron King + * @since 2019/1/5 14:24 + */ +public final class Http2EncodeInterceptor extends HttpInterceptor { + + private final Map mStreamRequestIndexes; + private final Map mStreamResponseIndexes; + + private NetBareXLog mLog; + + private Hpack.Writer mHpackRequestWriter; + private Hpack.Writer mHpackResponseWriter; + + public Http2EncodeInterceptor() { + mStreamRequestIndexes = new ConcurrentHashMap<>(); + mStreamResponseIndexes = new ConcurrentHashMap<>(); + } + + @Override + protected void intercept(@NonNull HttpRequestChain chain, @NonNull ByteBuffer buffer) + throws IOException { + if (chain.request().httpProtocol() == HttpProtocol.HTTP_2) { + if (mLog == null) { + HttpRequest request = chain.request(); + mLog = new NetBareXLog(request.protocol(), request.ip(), request.port()); + } + if (mHpackRequestWriter == null) { + mHpackRequestWriter = new Hpack.Writer(); + } + int index; + int streamId = chain.request().streamId(); + Integer requestIndex = mStreamRequestIndexes.get(streamId); + if (requestIndex != null) { + index = requestIndex + 1; + } else { + index = 0; + } + mStreamRequestIndexes.put(streamId, index); + if (index == 0) { + encodeRequestHeader(chain); + } else { + encodeRequestData(chain, buffer); + } + } else { + chain.process(buffer); + } + + } + + @Override + protected void intercept(@NonNull HttpResponseChain chain, @NonNull ByteBuffer buffer) + throws IOException { + if (chain.response().httpProtocol() == HttpProtocol.HTTP_2) { + if (mLog == null) { + HttpResponse response = chain.response(); + mLog = new NetBareXLog(response.protocol(), response.ip(), response.port()); + } + if (mHpackResponseWriter == null) { + mHpackResponseWriter = new Hpack.Writer(); + } + int index; + int streamId = chain.response().streamId(); + Integer responseIndex = mStreamResponseIndexes.get(streamId); + if (responseIndex != null) { + index = responseIndex + 1; + } else { + index = 0; + } + mStreamResponseIndexes.put(streamId, index); + if (index == 0) { + encodeResponseHeader(chain); + } else { + encodeResponseData(chain, buffer); + } + } else { + chain.process(buffer); + } + } + + private void encodeRequestHeader(HttpRequestChain chain) throws IOException { + HttpRequest request = chain.request(); + Http2Settings peerHttp2Settings = request.peerHttp2Settings(); + if (peerHttp2Settings != null) { + int headerTableSize = peerHttp2Settings.getHeaderTableSize(); + if (headerTableSize != -1) { + mHpackRequestWriter.setHeaderTableSizeSetting(headerTableSize); + } + } + byte[] headerBlock = mHpackRequestWriter.writeRequestHeaders(request.method(), + request.path(), request.host(), request.requestHeaders()); + sendHeaderBlockFrame(chain, headerBlock, peerHttp2Settings, request.streamId(), + request.requestStreamEnd()); + } + + private void encodeResponseHeader(HttpResponseChain chain) throws IOException { + HttpResponse response = chain.response(); + Http2Settings clientHttp2Settings = response.clientHttp2Settings(); + if (clientHttp2Settings != null) { + int headerTableSize = clientHttp2Settings.getHeaderTableSize(); + if (headerTableSize != -1) { + mHpackResponseWriter.setHeaderTableSizeSetting(headerTableSize); + } + } + byte[] headerBlock = mHpackResponseWriter.writeResponseHeaders(response.code(), + response.message(), response.responseHeaders()); + sendHeaderBlockFrame(chain, headerBlock, clientHttp2Settings, response.streamId(), + response.responseStreamEnd()); + } + + private void encodeRequestData(HttpRequestChain chain, ByteBuffer buffer) throws IOException { + byte[] data = Arrays.copyOfRange(buffer.array(), buffer.position(), buffer.limit()); + HttpRequest request = chain.request(); + sendDataFrame(chain, data, request.peerHttp2Settings(), request.streamId(), + request.requestStreamEnd()); + } + + private void encodeResponseData(HttpResponseChain chain, ByteBuffer buffer) throws IOException { + byte[] data = Arrays.copyOfRange(buffer.array(), buffer.position(), buffer.limit()); + HttpResponse response = chain.response(); + sendDataFrame(chain, data, response.clientHttp2Settings(), response.streamId(), + response.responseStreamEnd()); + } + + private void sendHeaderBlockFrame(InterceptorChain chain, byte[] headerBlock, Http2Settings http2Settings, + int streamId, boolean endStream) throws IOException { + int maxFrameSize = http2Settings == null ? Http2.INITIAL_MAX_FRAME_SIZE : + http2Settings.getMaxFrameSize(Http2.INITIAL_MAX_FRAME_SIZE); + int byteCount = headerBlock.length; + int length = Math.min(maxFrameSize, byteCount); + byte type = FrameType.HEADERS.get(); + byte flags = 0; + if (byteCount == length) { + flags |= Http2.FLAG_END_HEADERS; + if (endStream) { + flags |= Http2.FLAG_END_STREAM; + } + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write(frameHeader(streamId, length, type, flags)); + os.write(headerBlock, 0, length); + chain.process(ByteBuffer.wrap(os.toByteArray())); + if (byteCount > length) { + byte[] left = Arrays.copyOfRange(headerBlock, length, byteCount); + sendContinuationFrame(chain, left, streamId, maxFrameSize, byteCount - length, + endStream); + } + } + + private void sendContinuationFrame(InterceptorChain chain, byte[] headerBlock, int streamId, + int maxFrameSize, long byteCount, boolean endStream) throws IOException { + int offset = 0; + while (byteCount > 0) { + int length = (int) Math.min(maxFrameSize, byteCount); + byteCount -= length; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte flags = 0; + if (byteCount == 0) { + flags |= Http2.FLAG_END_HEADERS; + if (endStream) { + mLog.i("Http2 stream end: " + streamId); + flags |= Http2.FLAG_END_STREAM; + } + } + os.write(frameHeader(streamId, length, FrameType.CONTINUATION.get(), flags)); + os.write(headerBlock, offset, length); + offset += length; + chain.process(ByteBuffer.wrap(os.toByteArray())); + } + } + + private void sendDataFrame(InterceptorChain chain, byte[] data, Http2Settings http2Settings, + int streamId, boolean endStream) throws IOException { + int maxFrameSize = http2Settings == null ? Http2.INITIAL_MAX_FRAME_SIZE : + http2Settings.getMaxFrameSize(Http2.INITIAL_MAX_FRAME_SIZE); + int byteCount = data.length; + byte type = FrameType.DATA.get(); + int offset = 0; + while (byteCount > 0) { + int length = Math.min(maxFrameSize, byteCount); + byteCount -= length; + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte flags = 0; + if (byteCount == 0 && endStream) { + mLog.i("Http2 stream end: " + streamId); + flags |= Http2.FLAG_END_STREAM; + } + os.write(frameHeader(streamId, length, type, flags)); + os.write(data, offset, length); + offset += length; + chain.process(ByteBuffer.wrap(os.toByteArray())); + } + } + + private byte[] frameHeader(int streamId, int length, byte type, byte flags) { + mLog.i("Encode a http2 frame: " + FrameType.parse(type) + " stream(" + streamId + + ") length(" + length + ")"); + ByteBuffer header = ByteBuffer.allocate(Http2.FRAME_HEADER_LENGTH); + header.put((byte) ((length >>> 16) & 0xff)); + header.put((byte) ((length >>> 8) & 0xff)); + header.put((byte) (length & 0xff)); + header.put((byte) (type & 0xff)); + header.put((byte) (flags & 0xff)); + header.putInt(streamId & 0x7fffffff); + return header.array(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java new file mode 100644 index 0000000..a0f4991 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Settings.java @@ -0,0 +1,165 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.http2; + +import java.util.Arrays; + +/** + * Http/2 peer settings. + * + * @author Megatron King + * @since 2019/1/6 23:14 + */ +public final class Http2Settings { + + /** + * From the HTTP/2 specs, the default initial window size for all streams is 64 KiB. (Chrome 25 + * uses 10 MiB). + */ + private static final int DEFAULT_INITIAL_WINDOW_SIZE = 65535; + + /** + * HTTP/2: Size in bytes of the table used to decode the sender's header blocks. + */ + private static final int HEADER_TABLE_SIZE = 1; + /** + * HTTP/2: The peer must not send a PUSH_PROMISE frame when this is 0. + */ + private static final int ENABLE_PUSH = 2; + + /** + * Sender's maximum number of concurrent streams. + */ + private static final int MAX_CONCURRENT_STREAMS = 4; + + /** + * HTTP/2: Size in bytes of the largest frame payload the sender will accept. + */ + private static final int MAX_FRAME_SIZE = 5; + + /** + * HTTP/2: Advisory only. Size in bytes of the largest header list the sender will accept. + */ + private static final int MAX_HEADER_LIST_SIZE = 6; + + /** + * Window size in bytes. + */ + private static final int INITIAL_WINDOW_SIZE = 7; + + /** + * Total number of settings. + */ + private static final int COUNT = 10; + + /** + * Bitfield of which flags that values. + */ + private int set; + + /** + * Flag values. + */ + private final int[] values = new int[COUNT]; + + void clear() { + set = 0; + Arrays.fill(values, 0); + } + + Http2Settings set(int id, int value) { + if (id < 0 || id >= values.length) { + return this; // Discard unknown settings. + } + + int bit = 1 << id; + set |= bit; + values[id] = value; + return this; + } + + boolean isSet(int id) { + int bit = 1 << id; + return (set & bit) != 0; + } + + int get(int id) { + return values[id]; + } + + int size() { + return Integer.bitCount(set); + } + + int getHeaderTableSize() { + int bit = 1 << HEADER_TABLE_SIZE; + return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1; + } + + boolean getEnablePush(boolean defaultValue) { + int bit = 1 << ENABLE_PUSH; + return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1; + } + + int getMaxConcurrentStreams(int defaultValue) { + int bit = 1 << MAX_CONCURRENT_STREAMS; + return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; + } + + int getMaxFrameSize(int defaultValue) { + int bit = 1 << MAX_FRAME_SIZE; + return (bit & set) != 0 ? values[MAX_FRAME_SIZE] : defaultValue; + } + + int getMaxHeaderListSize(int defaultValue) { + int bit = 1 << MAX_HEADER_LIST_SIZE; + return (bit & set) != 0 ? values[MAX_HEADER_LIST_SIZE] : defaultValue; + } + + int getInitialWindowSize() { + int bit = 1 << INITIAL_WINDOW_SIZE; + return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : DEFAULT_INITIAL_WINDOW_SIZE; + } + + /** + * Writes {@code other} into this. If any setting is populated by this and {@code other}, the + * value and flags from {@code other} will be kept. + */ + public void merge(Http2Settings other) { + for (int i = 0; i < COUNT; i++) { + if (!other.isSet(i)) { + continue; + } + set(i, other.get(i)); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java new file mode 100644 index 0000000..03cfca2 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Stream.java @@ -0,0 +1,33 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +/** + * A "stream" is an independent, bidirectional sequence of frames exchanged between the client and + * server within an HTTP/2 connection. + * + * @author Megatron King + * @since 2019/1/5 23:16 + */ +/* package */ class Http2Stream { + + public int id; + + /* package */ Http2Stream() { + this.id = -1; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java new file mode 100644 index 0000000..cd461c8 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Http2Updater.java @@ -0,0 +1,30 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http2; + +/** + * A updater observes changes of HTTP2 session. + * + * @author Megatron King + * @since 2019/1/6 23:23 + */ +public interface Http2Updater { + + void onSettingsUpdate(Http2Settings http2Settings); + + void onStreamFinished(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java new file mode 100644 index 0000000..c5b4747 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/http2/Huffman.java @@ -0,0 +1,236 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.http2; + +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; + +/** + * This class was originally composed from the following classes in Twitter Hpack. + *

+ *

    + *
  • {@code com.twitter.hpack.HuffmanEncoder}
  • + *
  • {@code com.twitter.hpack.HuffmanDecoder}
  • + *
  • {@code com.twitter.hpack.HpackUtil}
  • + *
+ */ +/* package */ final class Huffman { + + // Appendix C: Huffman Codes + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-B + private static final int[] CODES = { + 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, + 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, + 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, + 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, + 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18, + 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, + 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, + 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, + 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, + 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, + 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, + 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3, + 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, + 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, + 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, + 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, + 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, + 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, + 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, + 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, + 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, + 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, + 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, + 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee + }; + + private static final byte[] CODE_LENGTHS = { + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 30, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, 5, + 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, + 6, 6, 5, 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, 20, 22, 20, 20, 22, 22, 22, 23, + 22, 23, 23, 23, 23, 23, 24, 23, 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, + 24, 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, 21, 21, 22, 21, 23, 22, + 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, + 26, 24, 25, 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, 20, 24, 20, 21, + 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, + 27, 27, 27, 27, 26 + }; + + private static final Huffman INSTANCE = new Huffman(); + + public static Huffman get() { + return INSTANCE; + } + + private final Node root = new Node(); + + private Huffman() { + buildTree(); + } + + byte[] decode(byte[] buf) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Node node = root; + int current = 0; + int nbits = 0; + for (byte bb : buf) { + int b = bb & 0xFF; + current = (current << 8) | b; + nbits += 8; + while (nbits >= 8) { + int c = (current >>> (nbits - 8)) & 0xFF; + node = node.children[c]; + if (node.children == null) { + // terminal node + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } else { + // non-terminal node + nbits -= 8; + } + } + } + + while (nbits > 0) { + int c = (current << (8 - nbits)) & 0xFF; + node = node.children[c]; + if (node.children != null || node.terminalBits > nbits) { + break; + } + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } + + return baos.toByteArray(); + } + + void encode(String data, ByteBuffer out) { + long current = 0; + int n = 0; + for (byte bb : data.getBytes()) { + int b = bb & 0xFF; + int code = CODES[b]; + int nbits = CODE_LENGTHS[b]; + + current <<= nbits; + current |= code; + n += nbits; + + while (n >= 8) { + n -= 8; + out.put((byte) (current >> n)); + } + } + + if (n > 0) { + current <<= (8 - n); + current |= (0xFF >>> n); + out.put((byte) current); + } + } + + int encodedLength(String data) { + long len = 0; + for (byte bb : data.getBytes()) { + int b = bb & 0xFF; + len += CODE_LENGTHS[b]; + } + return (int) ((len + 7) >> 3); + } + + private void buildTree() { + for (int i = 0; i < CODE_LENGTHS.length; i++) { + addCode(i, CODES[i], CODE_LENGTHS[i]); + } + } + + private void addCode(int sym, int code, byte len) { + Node terminal = new Node(sym, len); + + Node current = root; + while (len > 8) { + len -= 8; + int i = ((code >>> len) & 0xFF); + if (current.children == null) { + throw new IllegalStateException("invalid dictionary: prefix not unique"); + } + if (current.children[i] == null) { + current.children[i] = new Node(); + } + current = current.children[i]; + } + + int shift = 8 - len; + int start = (code << shift) & 0xFF; + int end = 1 << shift; + for (int i = start; i < start + end; i++) { + current.children[i] = terminal; + } + } + + private static final class Node { + + /** + * Null if terminal. + */ + private final Node[] children; + + /** + * Terminal nodes have a symbol. + */ + private final int symbol; + + /** + * Number of bits represented in the terminal node. + */ + private final int terminalBits; + + private Node() { + this.children = new Node[256]; + this.symbol = 0; // Not read. + this.terminalBits = 0; // Not read. + } + + private Node(int symbol, int bits) { + this.children = null; + this.symbol = symbol; + int b = bits & 0x07; + this.terminalBits = b == 0 ? 8 : b; + } + } +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java new file mode 100644 index 0000000..803856f --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Header.java @@ -0,0 +1,80 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +/** + * An abstract header object for ip protocol packets, provides some common apis. + * + * @author Megatron King + * @since 2018-10-09 16:28 + */ +/* package */ abstract class Header { + + byte[] packet; + int offset; + + /* package */ Header(byte[] packet, int offset) { + this.packet = packet; + this.offset = offset; + } + + byte readByte(int offset) { + return packet[offset]; + } + + void writeByte(byte value, int offset) { + packet[offset] = value; + } + + short readShort(int offset) { + int r = ((packet[offset] & 0xFF) << 8) | (packet[offset + 1] & 0xFF); + return (short) r; + } + + void writeShort(short value, int offset) { + packet[offset] = (byte) (value >> 8); + packet[offset + 1] = (byte) (value); + } + + int readInt(int offset) { + return ((packet[offset] & 0xFF) << 24) + | ((packet[offset + 1] & 0xFF) << 16) + | ((packet[offset + 2] & 0xFF) << 8) + | (packet[offset + 3] & 0xFF); + } + + void writeInt(int value, int offset) { + packet[offset] = (byte) (value >> 24); + packet[offset + 1] = (byte) (value >> 16); + packet[offset + 2] = (byte) (value >> 8); + packet[offset + 3] = (byte) value; + } + + long getSum(int offset, int len) { + long sum = 0; + while (len > 1) { + sum += readShort(offset) & 0xFFFF; + offset += 2; + len -= 2; + } + + if (len > 0) { + sum += (packet[offset] & 0xFF) << 8; + } + return sum; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java new file mode 100644 index 0000000..160546e --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IcmpHeader.java @@ -0,0 +1,91 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +/** + * ICMP messages are sent using the basic IP header. The first octet of the data portion of the + * datagram is a ICMP type field; the value of this field determines the format of the remaining + * data. Any field labeled "unused" is reserved for later extensions and must be zero when sent, + * but receivers should not use these fields (except to include them in the checksum). + * Unless otherwise noted under the individual format descriptions, the values of the internet + * header fields are as follows: + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Type | Code | Checksum | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | TBD | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Optional | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * See https://tools.ietf.org/html/rfc792 + * + * @author Megatron King + * @since 2018-10-10 23:04 + */ +public class IcmpHeader extends Header { + + private static final short OFFSET_TYPE = 0; + private static final short OFFSET_CODE = 1; + private static final short OFFSET_CRC = 2; + + private IpHeader mIpHeader; + + public IcmpHeader(IpHeader header, byte[] packet, int offset) { + super(packet, offset); + mIpHeader = header; + } + + public IpHeader getIpHeader() { + return mIpHeader; + } + + public byte getType() { + return readByte(offset + OFFSET_TYPE); + } + + public byte getCode() { + return readByte(offset + OFFSET_CODE); + } + + public short getCrc() { + return readShort(offset + OFFSET_CRC); + } + + public void setCrc(short crc) { + writeShort(crc, offset + OFFSET_CRC); + } + + public void updateChecksum() { + setCrc((short) 0); + setCrc(computeChecksum()); + } + + private short computeChecksum() { + int dataLength = mIpHeader.getDataLength(); + long sum = mIpHeader.getIpSum(); + sum += mIpHeader.getProtocol() & 0xFF; + sum += dataLength; + sum += getSum(offset, dataLength); + while ((sum >> 16) > 0) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + return (short) ~sum; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java new file mode 100644 index 0000000..cb73518 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpAddress.java @@ -0,0 +1,83 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +public class IpAddress implements Parcelable { + + public String address; + public int prefixLength; + + public IpAddress(String address, int prefixLength) { + this.address = address; + this.prefixLength = prefixLength; + } + + @Override + public String toString() { + return address + "/" + prefixLength; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof IpAddress)) { + return false; + } + // Compare string value. + return toString().equals(o.toString()); + } + + @Override + public int hashCode() { + return Objects.hash(address, prefixLength); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.address); + dest.writeInt(this.prefixLength); + } + + private IpAddress(Parcel in) { + this.address = in.readString(); + this.prefixLength = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public IpAddress createFromParcel(Parcel source) { + return new IpAddress(source); + } + + @Override + public IpAddress[] newArray(int size) { + return new IpAddress[size]; + } + }; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java new file mode 100644 index 0000000..de4aa46 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/IpHeader.java @@ -0,0 +1,130 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +/** + * A summary of the contents of the internet header follows: + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |Version| IHL |Type of Service| Total Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Identification |Flags| Fragment Offset | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Time to Live | Protocol | Header Checksum | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Source Address | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Destination Address | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Options | Padding | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * See https://tools.ietf.org/html/rfc791#section-3.1 + * + * @author Megatron King + * @since 2018-10-10 21:02 + */ +public final class IpHeader extends Header { + + public static final int MIN_HEADER_LENGTH = 20; + + private static final int OFFSET_PROTOCOL = 9; + private static final int OFFSET_VER_IHL = 0; + private static final int OFFSET_SRC_IP = 12; + private static final int OFFSET_DEST_IP = 16; + private static final int OFFSET_TLEN = 2; + private static final int OFFSET_CRC = 10; + + public IpHeader(byte[] packet, int offset) { + super(packet, offset); + } + + public byte getProtocol() { + return packet[offset + OFFSET_PROTOCOL]; + } + + public void setProtocol(byte value) { + packet[offset + OFFSET_PROTOCOL] = value; + } + + public int getHeaderLength() { + return (packet[offset + OFFSET_VER_IHL] & 0x0F) * 4; + } + + public void setHeaderLength(int value) { + packet[offset + OFFSET_VER_IHL] = (byte) ((4 << 4) | (value / 4)); + } + + public int getSourceIp() { + return readInt(offset + OFFSET_SRC_IP); + } + + public void setSourceIp(int ip) { + writeInt(ip, offset + OFFSET_SRC_IP); + } + + public int getDestinationIp() { + return readInt(offset + OFFSET_DEST_IP); + } + + public void setDestinationIp(int ip) { + writeInt(ip, offset + OFFSET_DEST_IP); + } + + public int getDataLength() { + return this.getTotalLength() - this.getHeaderLength(); + } + + public int getTotalLength() { + return readShort(offset + OFFSET_TLEN) & 0xFFFF; + } + + public void setTotalLength(short len) { + writeShort(len, offset + OFFSET_TLEN); + } + + public short getCrc() { + return readShort(offset + OFFSET_CRC); + } + + public void setCrc(short crc) { + writeShort(crc, offset + OFFSET_CRC); + } + + public void updateChecksum() { + setCrc((short) 0); + setCrc(computeChecksum()); + } + + public long getIpSum() { + // length 8 = src ip(4) + dest ip(4) + return getSum(offset + OFFSET_SRC_IP, 8); + } + + private short computeChecksum() { + // The checksum field is the 16 bit one's complement of the one's complement sum of all + // 16 bit words in the header. + int len = getHeaderLength(); + long sum = getSum(offset, len); + while ((sum >> 16) > 0) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + return (short) ~sum; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java new file mode 100644 index 0000000..3f54a1c --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/Protocol.java @@ -0,0 +1,68 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +import androidx.annotation.Nullable; + +/** + * The enum defines all supported IP protocols. + * + * Internet Protocol numbers see: + * https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers + * + * @author Megatron King + * @since 2018-10-11 00:13 + */ +public enum Protocol { + + /** + * Internet Control Message Protocol. + */ + ICMP((byte)1), + + /** + * Transmission Control Protocol. + */ + TCP((byte)6), + + /** + * User Datagram Protocol. + */ + UDP((byte)17); + + final byte number; + + Protocol(byte number) { + this.number = number; + } + + /** + * Parse the protocol by number. + * + * @param number Protocol number. + * @return The supported protocol number or null. + */ + @Nullable + public static Protocol parse(int number) { + for (Protocol protocol : values()) { + if (protocol.number == number) { + return protocol; + } + } + return null; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java new file mode 100644 index 0000000..121488f --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/TcpHeader.java @@ -0,0 +1,157 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +import java.util.Locale; + +/** + * TCP segments are sent as internet datagrams. The Internet Protocol header carries several + * information fields, including the source and destination host addresses. A TCP header follows + * the internet header, supplying information specific to the TCP protocol. This division allows + * for the existence of host level protocols other than TCP. + * + * TCP Header Format: + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Source Port | Destination Port | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Sequence Number | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Acknowledgment Number | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Data | |U|A|P|R|S|F| | + * | Offset| Reserved |R|C|S|S|Y|I| Window | + * | | |G|K|H|T|N|N| | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Checksum | Urgent Pointer | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Options | Padding | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | data | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * See https://tools.ietf.org/html/rfc793#section-3.1 + * + * @author Megatron King + * @since 2018-10-10 22:19 + */ +public class TcpHeader extends Header { + + private static final int OFFSET_SRC_PORT = 0; + private static final int OFFSET_DEST_PORT = 2; + private static final int OFFSET_LENRES = 12; + private static final int OFFSET_CRC = 16; + private static final int OFFSET_FLAG = 13; + private static final int OFFSET_SEQ = 4; + private static final int OFFSET_ACK = 8; + + private static final int FIN = 1; + private static final int SYN = 2; + private static final int RST = 4; + private static final int PSH = 8; + private static final int ACK = 16; + private static final int URG = 32; + + private IpHeader mIpHeader; + + public TcpHeader(IpHeader header, byte[] packet, int offset) { + super(packet, offset); + mIpHeader = header; + } + + public void updateOffset(int offset) { + this.offset = offset; + } + + public short getSourcePort() { + return readShort(offset + OFFSET_SRC_PORT); + } + + public void setSourcePort(short port) { + writeShort(port, offset + OFFSET_SRC_PORT); + } + + public short getDestinationPort() { + return readShort(offset + OFFSET_DEST_PORT); + } + + public void setDestinationPort(short port) { + writeShort(port, offset + OFFSET_DEST_PORT); + } + + public int getHeaderLength() { + int lenres = packet[offset + OFFSET_LENRES] & 0xFF; + return (lenres >> 4) * 4; + } + + public short getCrc() { + return readShort(offset + OFFSET_CRC); + } + + public void setCrc(short crc) { + writeShort(crc, offset + OFFSET_CRC); + } + + public byte getFlag() { + return packet[offset + OFFSET_FLAG]; + } + + public int getSeqID() { + return readInt(offset + OFFSET_SEQ); + } + + public int getAckID() { + return readInt(offset + OFFSET_ACK); + } + + public void updateChecksum() { + setCrc((short) 0); + setCrc(computeChecksum()); + } + + private short computeChecksum() { + // Sum = Ip Sum(Source Address + Destination Address) + Protocol + TCP Length + // The checksum field is the 16 bit one's complement of the one's complement sum of all 16 + // bit words in the header and text. + int dataLength = mIpHeader.getDataLength(); + long sum = mIpHeader.getIpSum(); + sum += mIpHeader.getProtocol() & 0xFF; + sum += dataLength; + sum += getSum(offset, dataLength); + while ((sum >> 16) > 0) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + return (short) ~sum; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "%s%s%s%s%s%s %d -> %d %s:%s", + (getFlag() & SYN) == SYN ? "SYN" : "", + (getFlag() & ACK) == ACK ? "ACK" : "", + (getFlag() & PSH) == PSH ? "PSH" : "", + (getFlag() & RST) == RST ? "RST" : "", + (getFlag() & FIN) == FIN ? "FIN" : "", + (getFlag() & URG) == URG ? "URG" : "", + getSourcePort() & 0xFFFF, + getDestinationPort() & 0xFFFF, + getSeqID(), + getAckID()); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java new file mode 100644 index 0000000..1e813f7 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ip/UdpHeader.java @@ -0,0 +1,145 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ip; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Locale; + +/** + * The UDP module must be able to determine the source and destination internet addresses and + * the protocol field from the internet header. + * + * UDP Header Format: + * + * 0 7 8 15 16 23 24 31 + * +--------+--------+--------+--------+ + * | Source | Destination | + * | Port | Port | + * +--------+--------+--------+--------+ + * | | | + * | Length | Checksum | + * +--------+--------+--------+--------+ + * | + * | data octets ... + * +---------------- ... + * + * See https://tools.ietf.org/html/rfc768 + * + * @author Megatron King + * @since 2018-10-10 23:04 + */ +public class UdpHeader extends Header { + + private static final short OFFSET_SRC_PORT = 0; + private static final short OFFSET_DEST_PORT = 2; + private static final short OFFSET_TLEN = 4; + private static final short OFFSET_CRC = 6; + + private IpHeader mIpHeader; + + public UdpHeader(IpHeader header, byte[] packet, int offset) { + super(packet, offset); + mIpHeader = header; + } + + public IpHeader getIpHeader() { + return mIpHeader; + } + + public short getSourcePort() { + return readShort(offset + OFFSET_SRC_PORT); + } + + public void setSourcePort(short port) { + writeShort(port, offset + OFFSET_SRC_PORT); + } + + public short getDestinationPort() { + return readShort(offset + OFFSET_DEST_PORT); + } + + public void setDestinationPort(short port) { + writeShort(port, offset + OFFSET_DEST_PORT); + } + + public short getCrc() { + return readShort(offset + OFFSET_CRC); + } + + public void setCrc(short crc) { + writeShort(crc, offset + OFFSET_CRC); + } + + public int getHeaderLength() { + return 8; + } + + public int getTotalLength() { + return readShort(offset + OFFSET_TLEN) & 0xFFFF; + } + + public void setTotalLength(short len) { + writeShort(len, offset + OFFSET_TLEN); + } + + public void updateChecksum() { + setCrc((short) 0); + setCrc(computeChecksum()); + } + + private short computeChecksum() { + // Sum = Ip Sum(Source Address + Destination Address) + Protocol + UDP Length + // Checksum is the 16-bit one's complement of the one's complement sum of a + // pseudo header of information from the IP header, the UDP header, and the + // data, padded with zero octets at the end (if necessary) to make a + // multiple of two octets. + int dataLength = mIpHeader.getDataLength(); + long sum = mIpHeader.getIpSum(); + sum += mIpHeader.getProtocol() & 0xFF; + sum += dataLength; + sum += getSum(offset, dataLength); + while ((sum >> 16) > 0) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + return (short) ~sum; + } + + @Override + public String toString() { + return String.format(Locale.getDefault(), "%d -> %d", getSourcePort() & 0xFFFF, + getDestinationPort() & 0xFFFF); + } + + public UdpHeader copy() { + byte[] copyArray = Arrays.copyOf(packet, packet.length); + IpHeader ipHeader = new IpHeader(copyArray, 0); + return new UdpHeader(ipHeader, copyArray, offset); + } + + public ByteBuffer data() { + int size = mIpHeader.getDataLength() - getHeaderLength(); + int dataOffset = mIpHeader.getHeaderLength() + getHeaderLength(); + byte[] data = new byte[size]; + System.arraycopy(packet, dataOffset, data, 0, size); + return ByteBuffer.wrap(data); + } + + public ByteBuffer buffer() { + return ByteBuffer.wrap(packet, 0, mIpHeader.getTotalLength()); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java new file mode 100644 index 0000000..140c991 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/DumpCallback.java @@ -0,0 +1,33 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.net; + +/** + * A callback for dumping net info from /proc/net. + * + * @author Megatron King + * @since 2018-12-01 16:30 + */ +/* package*/ interface DumpCallback { + + /** + * Invoked when a net info has dumped. + * + * @param net A dumped net object. + */ + void onDump(Net net); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java new file mode 100644 index 0000000..64fef23 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/Net.java @@ -0,0 +1,64 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.net; + +/** + * A dumped net info class contains IPs, ports and uid. + * + * @author Megatron King + * @since 2018-12-01 22:33 + */ +public class Net { + + /** + * The identifier of a process's uid. + */ + public int uid; + + /** + * The local IP. + */ + public String localIp; + + /** + * The local port. + */ + public int localPort; + + /** + * The remote server IP. + */ + public String remoteIp; + + /** + * The remote server port. + */ + public int remotePort; + + /* package */ Net(int uid, String localIp, int localPort, String remoteIp, int remotePort) { + this.uid = uid; + this.localIp = localIp; + this.localPort = localPort; + this.remoteIp = remoteIp; + this.remotePort = remotePort; + } + + @Override + public String toString() { + return uid + " " + localIp + ":" + localPort + " -> " + remoteIp + ":" + remotePort; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java new file mode 100644 index 0000000..da1bf7e --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/NetDumper.java @@ -0,0 +1,149 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.net; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * A Dumper uses for dumping net info from /proc/net/. + * + * @author Megatron King + * @since 2018-12-01 22:36 + */ +/* package */ class NetDumper implements Runnable { + + private final String mArg; + private final String mLocalIp; + private final DumpCallback mCallback; + + private boolean mIsRunning; + + NetDumper(String arg, String localIp, DumpCallback callback) { + this.mArg = arg; + this.mLocalIp = localIp; + this.mCallback = callback; + } + + void startDump() { + mIsRunning = true; + UidDumper.THREAD_POOL_EXECUTOR.execute(this); + } + + void stopDump() { + mIsRunning = false; + } + + void pauseDump() { + synchronized (mArg) { + try { + mArg.wait(); + } catch (InterruptedException e) { + // do nothing + } + } + } + + void resumeDump() { + synchronized (mArg) { + mArg.notify(); + } + } + + @Override + public void run() { + while (mIsRunning) { + ProcessBuilder builder = new ProcessBuilder("cat", mArg); + InputStream is = null; + BufferedReader reader = null; + try { + Process process = builder.start(); + is = process.getInputStream(); + reader = new BufferedReader(new InputStreamReader(is)); + String line; + int index = 0; + while ((line = reader.readLine()) != null) { + index++; + if (index == 1) { + // Skip the table title + continue; + } + String[] columns = line.trim().split(" "); + if (columns.length < 8) { + // Uid is in the 8th of columns. + continue; + } + int uid = NetBareUtils.parseInt(columns[7], -1); + if (uid == -1 || uid == 0) { + continue; + } + String[] local = columns[1].split(":"); + if (local.length != 2 || local[0].length() < 8) { + continue; + } + String[] remote = columns[2].split(":"); + if (remote.length != 2 || remote[0].length() < 8) { + continue; + } + String localIp = parseIp(local[0]); + if (localIp == null || !localIp.equals(mLocalIp)) { + continue; + } + String remoteIp = parseIp(remote[0]); + if (remoteIp == null || remoteIp.equals("0.0.0.0") + || remoteIp.equals("255.255.255.255")) { + continue; + } + int localPort = parsePort(local[1]); + if (localPort == -1) { + continue; + } + int remotePort = parsePort(remote[1]); + if (remotePort == -1) { + continue; + } + mCallback.onDump(new Net(uid, localIp, localPort, remoteIp, remotePort)); + } + } catch (IOException e) { + NetBareLog.wtf(e); + } + NetBareUtils.closeQuietly(is); + NetBareUtils.closeQuietly(reader); + } + } + + private String parseIp(String ip) { + ip = ip.substring(ip.length() - 8); + int ip1 = NetBareUtils.parseInt(ip.substring(6, 8), 16, -1); + int ip2 = NetBareUtils.parseInt(ip.substring(4, 6), 16, -1); + int ip3 = NetBareUtils.parseInt(ip.substring(2, 4), 16, -1); + int ip4 = NetBareUtils.parseInt(ip.substring(0, 2), 16, -1); + if (ip1 < 0 || ip2 < 0 || ip3 < 0 || ip4 < 0) { + return null; + } + return ip1 + "." + ip2 + "." + ip3 + "." + ip4; + } + + private int parsePort(String port) { + return NetBareUtils.parseInt(port, 16, -1); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java new file mode 100644 index 0000000..9c8f6ed --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/Session.java @@ -0,0 +1,94 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.net; + +import com.github.megatronking.netbare.ip.Protocol; + +import java.util.UUID; + +/** + * This object represents a network session, it contains IPs, ports and IP packet details. + * + * @author Megatron King + * @since 2018-10-14 23:39 + */ +public final class Session { + + /** + * IP protocol. + */ + public final Protocol protocol; + + /** + * Local vpn port. + */ + public final short localPort; + + /** + * Remote server port. + */ + public final short remotePort; + + /** + * Remote server IP. + */ + public final int remoteIp; + + /** + * An unique id uses to identify this session. + */ + public String id; + + /** + * Session started time. + */ + public long time; + + /** + * Remote server host. + */ + public String host; + + /** + * The process id that the session belongs to. + */ + public int uid; + + /** + * Packet counts. + */ + public int packetIndex; + + /** + * The total size of the packets that sends to remote server. + */ + public int sendDataSize; + + /** + * The total size of the packets that received from remote server. + */ + public int receiveDataSize; + + /* package */ Session(Protocol protocol, short localPort, short remotePort, int remoteIp) { + this.protocol = protocol; + this.localPort = localPort; + this.remotePort = remotePort; + this.remoteIp = remoteIp; + this.id = UUID.randomUUID().toString(); + this.time = System.currentTimeMillis(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java new file mode 100644 index 0000000..52f8019 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/SessionProvider.java @@ -0,0 +1,94 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.net; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.github.megatronking.netbare.ip.Protocol; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A session provider that provides the session instance query services. + * + * @author Megatron King + * @since 2018-10-15 21:46 + */ +public final class SessionProvider { + + private static final int MAX_SESSION = 100; + + private final Map mSessions; + private final UidDumper mDumper; + + /** + * Constructs a session provider with a {@link UidDumper}. + * + * @param dumper Use to dump uid, can be null. + */ + public SessionProvider(UidDumper dumper) { + this.mSessions = new ConcurrentHashMap<>(MAX_SESSION); + this.mDumper = dumper; + } + + /** + * Query a session by local VPN port. + * + * @param localPort The local VPN port. + * @return The instance of {@link Session} if it exists, or null. + */ + @Nullable + public Session query(short localPort) { + Session session = mSessions.get(localPort); + if (mDumper != null && session != null && session.uid == 0) { + // Query uid again. + mDumper.request(session); + } + return session; + } + + /** + * Query or create a session by protocol, ports and remote server IP. + * + * @param protocol IP protocol. + * @param localPort Local VPN port. + * @param remotePort Remote server port. + * @param remoteIp Remote server IP. + * @return An instance of {@link Session}, if the instance not exists, will create a new one. + */ + @NonNull + public Session ensureQuery(Protocol protocol, short localPort, short remotePort, int remoteIp) { + Session session = mSessions.get(localPort); + if (session != null) { + if (session.protocol != protocol || session.localPort != localPort || + session.remotePort != remotePort || session.remoteIp != remoteIp) { + session = null; + } + } + if (session == null) { + session = new Session(protocol, localPort, remotePort, remoteIp); + mSessions.put(localPort, session); + // Dump uid from /proc/net/ + if (mDumper != null) { + mDumper.request(session); + } + } + return session; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java new file mode 100644 index 0000000..82b2091 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidDumper.java @@ -0,0 +1,184 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.net; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareConfig; +import com.github.megatronking.netbare.NetBareUtils; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A dumper analyzes /proc/net/ files to dump uid of the network session. This class may be a + * battery-killer, but can set {@link NetBareConfig.Builder#dumpUid} to false to close the dumper. + * + * @author Megatron King + * @since 2018-12-03 16:54 + */ +public final class UidDumper implements DumpCallback { + + private static final int NET_ALIVE_SECONDS = 60; + private static final int NET_CONCURRENCY_LEVEL = 6; + private static final int NET_MAX_SIZE = 100; + + private static final int SESSION_ALIVE_SECONDS = 30; + private static final int SESSION_CONCURRENCY_LEVEL = 8; + private static final int SESSION_MAX_SIZE = 100; + + private static final int CORE_POOL_SIZE = 4; + private static final int MAXIMUM_POOL_SIZE = 4; + private static final int KEEP_ALIVE_SECONDS = 3 * 60; + private static final int QUEUE_SIZE = 32; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(@NonNull Runnable r) { + return new Thread(r, "UidDumper #" + mCount.getAndIncrement()); + } + }; + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + /* package */ static final Executor THREAD_POOL_EXECUTOR; + + private final Cache mNetCaches; + private final Cache mWaitingSessions; + + private final UidProvider mUidProvider; + + private final NetDumper dumper1; + private final NetDumper dumper2; + private final NetDumper dumper3; + private final NetDumper dumper4; + + static { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( + CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, + new LinkedBlockingDeque(QUEUE_SIZE), sThreadFactory); + threadPoolExecutor.allowCoreThreadTimeOut(true); + THREAD_POOL_EXECUTOR = threadPoolExecutor; + } + + public UidDumper(String localIp, UidProvider provider) { + this.mUidProvider = provider; + this.mNetCaches = CacheBuilder.newBuilder() + .expireAfterAccess(NET_ALIVE_SECONDS, TimeUnit.SECONDS) + .concurrencyLevel(NET_CONCURRENCY_LEVEL) + .maximumSize(NET_MAX_SIZE) + .build(); + this.mWaitingSessions = CacheBuilder.newBuilder() + .expireAfterAccess(SESSION_ALIVE_SECONDS, TimeUnit.SECONDS) + .concurrencyLevel(SESSION_CONCURRENCY_LEVEL) + .maximumSize(SESSION_MAX_SIZE) + .build(); + + this.dumper1 = new NetDumper("/proc/net/tcp", localIp, this); + this.dumper2 = new NetDumper("/proc/net/tcp6", localIp, this); + this.dumper3 = new NetDumper("/proc/net/udp", localIp, this); + this.dumper4 = new NetDumper("/proc/net/udp6", localIp, this); + } + + public void startDump() { + dumper1.startDump(); + dumper2.startDump(); + dumper3.startDump(); + dumper4.startDump(); + } + + public void stopDump() { + dumper1.stopDump(); + dumper2.stopDump(); + dumper3.stopDump(); + dumper4.stopDump(); + } + + private void pauseDump() { + dumper1.pauseDump(); + dumper2.pauseDump(); + dumper3.pauseDump(); + dumper4.pauseDump(); + } + + private void resumeDump() { + dumper1.resumeDump(); + dumper2.resumeDump(); + dumper3.resumeDump(); + dumper4.resumeDump(); + } + + public void request(Session session) { + if (mUidProvider != null) { + int uid = mUidProvider.uid(session); + if (uid != UidProvider.UID_UNKNOWN) { + session.uid = uid; + return; + } + } + int port = NetBareUtils.convertPort(session.localPort); + Map caches = mNetCaches.asMap(); + if (caches.containsKey(port)) { + Net net = caches.get(port); + if (net != null) { + session.uid = net.uid; + } + } else { + // Find net by remote ip from cache + for (Net net : caches.values()) { + if (NetBareUtils.convertIp(net.remoteIp) == session.remoteIp) { + session.uid = net.uid; + return; + } + } + mWaitingSessions.put(port, session); + // resumeDump(); + } + } + + @Override + public void onDump(Net net) { + mNetCaches.put(net.localPort, net); + + if (mWaitingSessions.size() == 0) { + // If sleep the threads, some uid would be missed. But if keep the threads running, too + // much battery would be cost. + // pauseDump(); + return; + } + Map map = mWaitingSessions.asMap(); + for (int port : map.keySet()) { + if (net.localPort == port) { + Session session = map.get(port); + if (session != null) { + session.uid = net.uid; + } + mWaitingSessions.invalidate(port); + break; + } + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java b/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java new file mode 100644 index 0000000..080f062 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/net/UidProvider.java @@ -0,0 +1,21 @@ +package com.github.megatronking.netbare.net; + +/** + * This interface provides a known uid for a session. + * + * @author Megatron King + * @since 2019/1/27 21:31 + */ +public interface UidProvider { + + int UID_UNKNOWN = -1; + + /** + * Returns a known uid for this session, if the uid is unknown should return {@link #UID_UNKNOWN}. + * + * @param session Network session. + * @return A known uid or {@link #UID_UNKNOWN}. + */ + int uid(Session session); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java new file mode 100644 index 0000000..2035986 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/BaseProxyServer.java @@ -0,0 +1,73 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import com.github.megatronking.netbare.NetBareLog; + +import java.io.IOException; + +/** + * An abstract base class defined for proxy servers. The local proxy server runs a separated thread + * and loop to process packets. The sub class needs to impl {@link #process()} to handle the packets. + * + * @author Megatron King + * @since 2018-10-10 00:31 + */ +/* package */ abstract class BaseProxyServer extends ProxyServer implements Runnable { + + /** + * Waiting the specific protocol packets and trying to sent to real remote server. + * + * @throws IOException If an I/O error has occurred. + */ + protected abstract void process() throws IOException; + + private boolean mIsRunning; + + private final Thread mServerThread; + + /* package */ BaseProxyServer(String serverName) { + this.mServerThread = new Thread(this, serverName); + } + + @Override + void startServer() { + mIsRunning = true; + mServerThread.start(); + } + + @Override + void stopServer() { + mIsRunning = false; + mServerThread.interrupt(); + } + + @Override + public void run() { + while (mIsRunning) { + try { + process(); + } catch (IOException e) { + NetBareLog.e(e.getMessage()); + } + } + } + + /* package */ boolean isStopped() { + return !mIsRunning; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java new file mode 100644 index 0000000..5bf31b1 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/IcmpProxyServerForwarder.java @@ -0,0 +1,51 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.ip.IcmpHeader; +import com.github.megatronking.netbare.ip.IpHeader; + +import java.io.OutputStream; + +/** + * Forward the Internet Control Message Protocol (ICMP) to proxy server. + * + * @author Megatron King + * @since 2018-10-09 01:30 + */ +public final class IcmpProxyServerForwarder implements ProxyServerForwarder { + + @Override + public void prepare() { + // TODO + } + + @Override + public void forward(byte[] packet, int len, OutputStream output) { + IpHeader ipHeader = new IpHeader(packet, 0); + IcmpHeader icmpHeader = new IcmpHeader(ipHeader, packet, ipHeader.getHeaderLength()); + NetBareLog.v("ICMP type: " + icmpHeader.getType()); + NetBareLog.v("ICMP code: " + icmpHeader.getCode()); + // TODO transfer to proxy server + } + + @Override + public void release() { + // TODO + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java new file mode 100644 index 0000000..509f3d1 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServer.java @@ -0,0 +1,67 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +/** + * A local proxy server receives net packets from VPN and transfer them to the real remote server. + * Every local proxy server runs separated threads and handle specific IP protocols like TCP, UDP + * and so on. The server is managed by {@link ProxyServerForwarder}, use {@link #start()} to + * establish the server and {@link #stop()} to terminate it. + * + * @author Megatron King + * @since 2018-10-10 00:23 + */ +public abstract class ProxyServer { + + /** + * Establish the server and start receive packets. + */ + /* package */ abstract void startServer(); + + /** + * Terminate this server. + */ + /* package */ abstract void stopServer(); + + /** + * Returns the proxy server IP. + * + * @return The proxy server IP. + */ + /* package */ abstract int getIp(); + + /** + * Returns the proxy server port. + * + * @return The proxy server port. + */ + /* package */ abstract short getPort(); + + /** + * Establish the proxy server. + */ + public final void start() { + startServer(); + } + + /** + * Terminate the proxy server, release resources and close IOs. + */ + public final void stop() { + stopServer(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java new file mode 100644 index 0000000..ccbc09e --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/ProxyServerForwarder.java @@ -0,0 +1,47 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import java.io.OutputStream; + +/** + * An interface needs to be implement by proxy server forwarders. + * + * @author Megatron King + * @since 2018-10-09 01:24 + */ +public interface ProxyServerForwarder { + + /** + * Prepare the forwarder. + */ + void prepare(); + + /** + * Forward a packet to local proxy server. + * + * @param packet A data packet, the array length is MTU. + * @param len The actual data length in packet array. + * @param output An output stream connects VPN file descriptor. + */ + void forward(byte[] packet, int len, OutputStream output); + + /** + * Release the forwarder. + */ + void release(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java new file mode 100644 index 0000000..9565d53 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServer.java @@ -0,0 +1,216 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import android.net.VpnService; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.net.SessionProvider; +import com.github.megatronking.netbare.tunnel.ConnectionShutdownException; +import com.github.megatronking.netbare.tunnel.NioCallback; +import com.github.megatronking.netbare.tunnel.NioTunnel; +import com.github.megatronking.netbare.tunnel.TcpProxyTunnel; +import com.github.megatronking.netbare.tunnel.TcpRemoteTunnel; +import com.github.megatronking.netbare.tunnel.TcpTunnel; +import com.github.megatronking.netbare.tunnel.TcpVATunnel; + +import java.io.EOFException; +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; + +/** + * The TCP proxy server is a nio {@link ServerSocketChannel}, it listens the connections from + * {@link VpnService} and forwards request packets to real remote server. This server uses + * {@link TcpVATunnel} to bind {@link VirtualGateway} and {@link NioTunnel} together. Every TCP + * connection has two channels: {@link TcpProxyTunnel} and {@link TcpRemoteTunnel}. + * The {@link TcpProxyTunnel} is responsible for sending remote server response packets to VPN + * service, and the {@link TcpRemoteTunnel} is responsible for communicating with remote server. + * + * @author Megatron King + * @since 2018-10-11 17:35 + */ +/* package */ class TcpProxyServer extends BaseProxyServer implements Runnable { + + private final VpnService mVpnService; + + private final Selector mSelector; + private final ServerSocketChannel mServerSocketChannel; + + private int mIp; + private short mPort; + private int mMtu; + + private SessionProvider mSessionProvider; + + /* package */ TcpProxyServer(VpnService vpnService, String ip, int mtu) + throws IOException { + super("TcpProxyServer"); + this.mVpnService = vpnService; + + this.mSelector = Selector.open(); + this.mServerSocketChannel = ServerSocketChannel.open(); + this.mServerSocketChannel.configureBlocking(false); + this.mServerSocketChannel.socket().bind(new InetSocketAddress(0)); + this.mServerSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT); + + this.mIp = NetBareUtils.convertIp(ip); + this.mPort = (short) mServerSocketChannel.socket().getLocalPort(); + this.mMtu = mtu; + + NetBareLog.v("[TCP]proxy server: %s:%d", ip, NetBareUtils.convertPort(mPort)); + } + + void setSessionProvider(SessionProvider sessionProvider) { + this.mSessionProvider = sessionProvider; + } + + + @Override + int getIp() { + return mIp; + } + + @Override + short getPort() { + return mPort; + } + + @Override + public void run() { + NetBareLog.i("[TCP]Server starts running."); + super.run(); + NetBareUtils.closeQuietly(mSelector); + NetBareUtils.closeQuietly(mServerSocketChannel); + NetBareLog.i("[TCP]Server stops running."); + } + + @Override + protected void process() throws IOException { + int select = mSelector.select(); + if (select == 0) { + return; + } + Set selectedKeys = mSelector.selectedKeys(); + if (selectedKeys == null) { + return; + } + Iterator iterator = selectedKeys.iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + try { + if (key.isValid()) { + if (key.isAcceptable()) { + onAccept(); + } else { + Object attachment = key.attachment(); + if (attachment instanceof NioCallback) { + NioCallback callback = (NioCallback) attachment; + try { + if (key.isConnectable()) { + callback.onConnected(); + } else if (key.isReadable()) { + callback.onRead(); + } else if (key.isWritable()) { + callback.onWrite(); + } + } catch (IOException e) { + NioTunnel tunnel = callback.getTunnel(); + if (!tunnel.isClosed()) { + handleException(e); + } + callback.onClosed(); + } + } + } + } + } finally { + iterator.remove(); + } + + } + } + + private void onAccept() throws IOException { + SocketChannel clientChannel = mServerSocketChannel.accept(); + Socket clientSocket = clientChannel.socket(); + + // The client ip is the remote server ip + // The client port is the local port(it is the vpn port not the proxy server port) + String ip = clientSocket.getInetAddress().getHostAddress(); + int port = clientSocket.getPort(); + + // The session should have be saved before the tcp packets be forwarded to proxy server. So + // we can query it by client port. + Session session = mSessionProvider.query((short) port); + if (session == null) { + throw new IOException("No session saved with key: " + port); + } + + int remotePort = NetBareUtils.convertPort(session.remotePort); + + // Connect remote server and dispatch data. + TcpTunnel proxyTunnel = null; + TcpTunnel remoteTunnel = null; + try { + proxyTunnel = new TcpProxyTunnel(clientChannel, mSelector, remotePort); + remoteTunnel = new TcpRemoteTunnel(mVpnService, SocketChannel.open(), + mSelector, ip, remotePort); + TcpVATunnel gatewayTunnel = new TcpVATunnel(session, proxyTunnel, + remoteTunnel, mMtu); + gatewayTunnel.connect(new InetSocketAddress(ip, remotePort)); + } catch (IOException e){ + NetBareUtils.closeQuietly(proxyTunnel); + NetBareUtils.closeQuietly(remoteTunnel); + throw e; + } + } + + private void handleException(IOException e) { + if (e == null || e.getMessage() == null) { + return; + } + if (e instanceof SSLHandshakeException) { + // Client doesn't accept the MITM CA certificate. + NetBareLog.e(e.getMessage()); + } else if (e instanceof ConnectionShutdownException) { + // Connection exception, do not mind this. + NetBareLog.e(e.getMessage()); + } else if (e instanceof ConnectException) { + // Connection timeout + NetBareLog.e(e.getMessage()); + } else if (e instanceof SSLException && (e.getCause() instanceof EOFException)) { + // Connection shutdown manually + NetBareLog.e(e.getMessage()); + } else { + NetBareLog.wtf(e); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java new file mode 100644 index 0000000..d90f047 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/TcpProxyServerForwarder.java @@ -0,0 +1,135 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import android.net.VpnService; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.ip.IpHeader; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.ip.TcpHeader; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.net.SessionProvider; +import com.github.megatronking.netbare.net.UidDumper; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Handshake with local TCP proxy server and then forward packets to it. + * + * @author Megatron King + * @since 2018-10-09 01:30 + */ +public final class TcpProxyServerForwarder implements ProxyServerForwarder { + + private final SessionProvider mSessionProvider; + private final TcpProxyServer mProxyServer; + + public TcpProxyServerForwarder(VpnService vpnService, String ip, int mtu, + UidDumper dumper) throws IOException { + this.mSessionProvider = new SessionProvider(dumper); + this.mProxyServer = new TcpProxyServer(vpnService, ip, mtu); + this.mProxyServer.setSessionProvider(mSessionProvider); + } + + @Override + public void prepare() { + this.mProxyServer.start(); + } + + @Override + public void forward(byte[] packet, int len, OutputStream output) { + IpHeader ipHeader = new IpHeader(packet, 0); + TcpHeader tcpHeader = new TcpHeader(ipHeader, packet, ipHeader.getHeaderLength()); + + // Src IP & Port + int localIp = ipHeader.getSourceIp(); + short localPort = tcpHeader.getSourcePort(); + + // Dest IP & Port + int remoteIp = ipHeader.getDestinationIp(); + short remotePort = tcpHeader.getDestinationPort(); + + // TCP data size + int tcpDataSize = ipHeader.getDataLength() - tcpHeader.getHeaderLength(); + + NetBareLog.v("ip: %s:%d -> %s:%d", NetBareUtils.convertIp(localIp), + NetBareUtils.convertPort(localPort), NetBareUtils.convertIp(remoteIp), + NetBareUtils.convertPort(remotePort)); + NetBareLog.v("tcp: %s, size: %d", tcpHeader.toString(), tcpDataSize); + + // Tcp handshakes and proxy forward flow. + + // Client: 10.1.10.1:40988 + // Server: 182.254.116.117:80 + // Proxy Server: 10.1.10.1:38283 + + // 10.1.10.1:40988 -> 182.254.116.117:80 SYN + // Forward: 182.254.116.117:40988 -> 10.1.10.1:38283 SYN + + // 10.1.10.1:38283 -> 182.254.116.117:40988 SYN+ACK + // Forward: 182.254.116.117:80 -> 10.1.10.1:40988 SYN+ACK + + // 10.1.10.1:40988 -> 182.254.116.117:80 ACK + // Forward: 182.254.116.117:80 -> 10.1.10.1:38283 ACK + + if (localPort != mProxyServer.getPort()) { + // Client requests to server + Session session = mSessionProvider.ensureQuery(Protocol.TCP, localPort, remotePort, remoteIp); + session.packetIndex++; + + // Forward client request to proxy server. + ipHeader.setSourceIp(remoteIp); + ipHeader.setDestinationIp(mProxyServer.getIp()); + tcpHeader.setDestinationPort(mProxyServer.getPort()); + + ipHeader.updateChecksum(); + tcpHeader.updateChecksum(); + + session.sendDataSize += tcpDataSize; + } else { + // Proxy server responses forward client request. + Session session = mSessionProvider.query(remotePort); + if (session == null) { + NetBareLog.w("No session saved with key: " + remotePort); + return; + } + // Forward proxy server response to client. + ipHeader.setSourceIp(remoteIp); + ipHeader.setDestinationIp(mProxyServer.getIp()); + tcpHeader.setSourcePort(session.remotePort); + + ipHeader.updateChecksum(); + tcpHeader.updateChecksum(); + + session.receiveDataSize += tcpDataSize; + } + + try { + output.write(packet, 0, len); + } catch (IOException e) { + NetBareLog.e(e.getMessage()); + } + } + + @Override + public void release() { + this.mProxyServer.stop(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java new file mode 100644 index 0000000..faddeb4 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServer.java @@ -0,0 +1,180 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import android.net.VpnService; +import android.os.SystemClock; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.ip.IpHeader; +import com.github.megatronking.netbare.ip.UdpHeader; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.net.SessionProvider; +import com.github.megatronking.netbare.tunnel.NioCallback; +import com.github.megatronking.netbare.tunnel.NioTunnel; +import com.github.megatronking.netbare.tunnel.Tunnel; +import com.github.megatronking.netbare.tunnel.UdpRemoteTunnel; +import com.github.megatronking.netbare.tunnel.UdpVATunnel; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The UDP proxy server is a virtual server, every packet from {@link UdpProxyServerForwarder} is + * saw as a connection. It use {@link UdpVATunnel} to bind {@link VirtualGateway} and + * {@link NioTunnel} together. Not like TCP, UDP only use {@link UdpRemoteTunnel} to communicate with + * real remote server. + * + * @author Megatron King + * @since 2018-10-11 17:35 + */ +/* package */ class UdpProxyServer extends BaseProxyServer { + + private static final int SELECTOR_WAIT_TIME = 50; + + private final VpnService mVpnService; + + private int mMtu; + + private final Selector mSelector; + private final Map mTunnels; + + private SessionProvider mSessionProvider; + + /* package */ UdpProxyServer(VpnService vpnService, int mtu) throws IOException { + super("UdpProxyServer"); + this.mVpnService = vpnService; + + this.mMtu = mtu; + + this.mSelector = Selector.open(); + this.mTunnels = new ConcurrentHashMap<>(); + } + + @Override + int getIp() { + return 0; + } + + @Override + short getPort() { + return 0; + } + + void setSessionProvider(SessionProvider sessionProvider) { + this.mSessionProvider = sessionProvider; + } + + void send(UdpHeader header, OutputStream output) throws IOException { + short localPort = header.getSourcePort(); + UdpVATunnel tunnel = mTunnels.get(localPort); + try { + if (tunnel == null) { + Session session = mSessionProvider.query(localPort); + if (session == null) { + throw new IOException("No session saved with key: " + localPort); + } + + IpHeader ipHeader = header.getIpHeader(); + NioTunnel remoteTunnel = new UdpRemoteTunnel(mVpnService, DatagramChannel.open(), + mSelector, NetBareUtils.convertIp(session.remoteIp), session.remotePort); + tunnel = new UdpVATunnel(session, remoteTunnel, output, mMtu); + tunnel.connect(new InetSocketAddress(NetBareUtils.convertIp(ipHeader.getDestinationIp()), + NetBareUtils.convertPort(header.getDestinationPort()))); + mTunnels.put(header.getSourcePort(), tunnel); + } + tunnel.send(header); + } catch (IOException e) { + mTunnels.remove(localPort); + NetBareUtils.closeQuietly(tunnel); + throw e; + } + } + + @Override + public void run() { + NetBareLog.i("[UDP]Server starts running."); + super.run(); + NetBareUtils.closeQuietly(mSelector); + NetBareLog.i("[UDP]Server stops running."); + } + + @Override + protected void process() throws IOException { + int select = mSelector.select(); + if (select == 0) { + // Wait a short time to let the selector register or interest. + SystemClock.sleep(SELECTOR_WAIT_TIME); + return; + } + Set selectedKeys = mSelector.selectedKeys(); + if (selectedKeys == null) { + return; + } + Iterator iterator = selectedKeys.iterator(); + while (iterator.hasNext()) { + SelectionKey key = iterator.next(); + if (key.isValid()) { + Object attachment = key.attachment(); + if (attachment instanceof NioCallback) { + NioCallback callback = (NioCallback) attachment; + try { + if (key.isReadable()) { + callback.onRead(); + } else if (key.isWritable()) { + callback.onWrite(); + } else if (key.isConnectable()) { + callback.onConnected(); + } + } catch (IOException e) { + callback.onClosed(); + removeTunnel(callback.getTunnel()); + } + } + } + iterator.remove(); + } + } + + @Override + void stopServer() { + super.stopServer(); + for (UdpVATunnel tunnel : mTunnels.values()) { + NetBareUtils.closeQuietly(tunnel); + } + } + + private void removeTunnel(Tunnel tunnel) { + Map tunnels = new HashMap<>(mTunnels); + for (short key : tunnels.keySet()) { + if (tunnels.get(key).getRemoteChannel() == tunnel) { + mTunnels.remove(key); + } + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java new file mode 100644 index 0000000..d482ebb --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/proxy/UdpProxyServerForwarder.java @@ -0,0 +1,92 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.proxy; + +import android.net.VpnService; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.ip.IpHeader; +import com.github.megatronking.netbare.ip.Protocol; +import com.github.megatronking.netbare.ip.UdpHeader; +import com.github.megatronking.netbare.net.Session; +import com.github.megatronking.netbare.net.SessionProvider; +import com.github.megatronking.netbare.net.UidDumper; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Unlike TCP proxy server, UDP doesn't need handshake, we can forward packets to it directly. + * + * @author Megatron King + * @since 2018-10-09 01:30 + */ +public final class UdpProxyServerForwarder implements ProxyServerForwarder { + + private final SessionProvider mSessionProvider; + private final UdpProxyServer mProxyServer; + + public UdpProxyServerForwarder(VpnService vpnService, int mtu, UidDumper dumper) + throws IOException { + this.mSessionProvider = new SessionProvider(dumper); + this.mProxyServer = new UdpProxyServer(vpnService, mtu); + this.mProxyServer.setSessionProvider(mSessionProvider); + } + + @Override + public void prepare() { + this.mProxyServer.start(); + } + + @Override + public void forward(byte[] packet, int len, OutputStream output) { + IpHeader ipHeader = new IpHeader(packet, 0); + UdpHeader udpHeader = new UdpHeader(ipHeader, packet, ipHeader.getHeaderLength()); + + // Src IP & Port + int localIp = ipHeader.getSourceIp(); + short localPort = udpHeader.getSourcePort(); + + // Dest IP & Port + int remoteIp = ipHeader.getDestinationIp(); + short remotePort = udpHeader.getDestinationPort(); + + // UDP data size + int udpDataSize = ipHeader.getDataLength() - udpHeader.getHeaderLength(); + + NetBareLog.v("ip: %s:%d -> %s:%d", NetBareUtils.convertIp(localIp), + NetBareUtils.convertPort(localPort), NetBareUtils.convertIp(remoteIp), + NetBareUtils.convertPort(remotePort)); + NetBareLog.v("udp: %s, size: %d", udpHeader.toString(), udpDataSize); + + Session session = mSessionProvider.ensureQuery(Protocol.UDP, localPort, remotePort, remoteIp); + session.packetIndex++; + + try { + mProxyServer.send(udpHeader, output); + session.sendDataSize += udpDataSize; + } catch (IOException e) { + NetBareLog.e(e.getMessage()); + } + } + + @Override + public void release() { + this.mProxyServer.stop(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java new file mode 100644 index 0000000..982b24b --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateGenerator.java @@ -0,0 +1,253 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import com.github.megatronking.netbare.NetBareUtils; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.bc.BcX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Random; + +/** + * Generates self-signed certificate by {@link JKS}. + * + * @author Megatron King + * @since 2018-11-08 02:23 + */ +public final class CertificateGenerator { + + private static final String KEY_STORE_TYPE = "PKCS12"; + private static final String KEYGEN_ALGORITHM = "RSA"; + private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; + + private static final String PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME; + + private static final int ROOT_KEY_SIZE = 2048; + private static final int SERVER_KEY_SIZE = 1024; + + /** + * The signature algorithm starting with the message digest to use when signing certificates. + * On 64-bit systems this should be set to SHA512, on 32-bit systems this is SHA256. On 64-bit + * systems, SHA512 generally performs better than SHA256; see this question for details: + * http://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256 + */ + private static final String SIGNATURE_ALGORITHM = (is32BitJvm() ? "SHA256" : "SHA512") + + "WithRSAEncryption"; + + /** + * The milliseconds of 30 day. + */ + private static final long ONE_DAY = 30 * 86400000L; + + /** + * Current time minus 1 year, just in case software clock goes back due to time synchronization. + */ + private static final Date NOT_BEFORE = new Date(System.currentTimeMillis() - ONE_DAY * 365); + + /** + * The maximum possible value in X.509 specification: 9999-12-31 23:59:59, + * new Date(253402300799000L), but Apple iOS 8 fails with a certificate + * expiration date grater than Mon, 24 Jan 6084 02:07:59 GMT (issue #6). + * + * Hundred years in the future from starting the proxy should be enough. + */ + private static final Date NOT_AFTER = new Date(System.currentTimeMillis() + ONE_DAY * 365 * 10); + + /** + * Generate a root keystore by a given {@link JKS}. + * + * @param jks A java keystore object. + * @return A root {@link KeyStore}. + */ + public KeyStore generateRoot(JKS jks) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, + IOException, OperatorCreationException { + KeyPair keyPair = generateKeyPair(ROOT_KEY_SIZE); + + X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + nameBuilder.addRDN(BCStyle.CN, jks.commonName()); + nameBuilder.addRDN(BCStyle.O, jks.organization()); + nameBuilder.addRDN(BCStyle.OU, jks.organizationalUnitName()); + X500Name issuer = nameBuilder.build(); + + PublicKey pubKey = keyPair.getPublic(); + + X509v3CertificateBuilder generator = new JcaX509v3CertificateBuilder( + issuer, BigInteger.valueOf(randomSerial()), NOT_BEFORE, NOT_AFTER, issuer, pubKey); + generator.addExtension(Extension.subjectKeyIdentifier, false, + createSubjectKeyIdentifier(pubKey)); + generator.addExtension(Extension.basicConstraints, true, + new BasicConstraints(true)); + + KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.digitalSignature | + KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.cRLSign); + generator.addExtension(Extension.keyUsage, false, usage); + + ASN1EncodableVector purposes = new ASN1EncodableVector(); + purposes.add(KeyPurposeId.id_kp_serverAuth); + purposes.add(KeyPurposeId.id_kp_clientAuth); + purposes.add(KeyPurposeId.anyExtendedKeyUsage); + generator.addExtension(Extension.extendedKeyUsage, false, + new DERSequence(purposes)); + + X509Certificate cert = signCertificate(generator, keyPair.getPrivate()); + + KeyStore result = KeyStore.getInstance(KEY_STORE_TYPE); + result.load(null, null); + result.setKeyEntry(jks.alias(), keyPair.getPrivate(), jks.password(), + new Certificate[] { cert }); + return result; + } + + public KeyStore generateServer(String commonName, JKS jks, + Certificate caCert, PrivateKey caPrivKey) + throws NoSuchAlgorithmException, NoSuchProviderException, + IOException, OperatorCreationException, CertificateException, + InvalidKeyException, SignatureException, KeyStoreException { + + KeyPair keyPair = generateKeyPair(SERVER_KEY_SIZE); + + X500Name issuer = new X509CertificateHolder(caCert.getEncoded()).getSubject(); + BigInteger serial = BigInteger.valueOf(randomSerial()); + X500NameBuilder name = new X500NameBuilder(BCStyle.INSTANCE); + name.addRDN(BCStyle.CN, commonName); + name.addRDN(BCStyle.O, jks.certOrganisation()); + name.addRDN(BCStyle.OU, jks.certOrganizationalUnitName()); + X500Name subject = name.build(); + + X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuer, serial, NOT_BEFORE, + new Date(System.currentTimeMillis() + ONE_DAY), subject, keyPair.getPublic()); + builder.addExtension(Extension.subjectKeyIdentifier, false, + createSubjectKeyIdentifier(keyPair.getPublic())); + builder.addExtension(Extension.basicConstraints, false, + new BasicConstraints(false)); + builder.addExtension(Extension.subjectAlternativeName, false, + new DERSequence(new GeneralName(GeneralName.dNSName, commonName))); + + X509Certificate cert = signCertificate(builder, caPrivKey); + + cert.checkValidity(new Date()); + cert.verify(caCert.getPublicKey()); + + KeyStore result = KeyStore.getInstance(KeyStore.getDefaultType()); + result.load(null, null); + Certificate[] chain = { cert, caCert }; + result.setKeyEntry(jks.alias(), keyPair.getPrivate(), jks.password(), chain); + return result; + } + + public String keyStoreType() { + return KEY_STORE_TYPE; + } + + private KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException { + KeyPairGenerator generator = KeyPairGenerator.getInstance(KEYGEN_ALGORITHM); + SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM); + generator.initialize(keySize, secureRandom); + return generator.generateKeyPair(); + } + + private long randomSerial() { + final Random rnd = new Random(); + rnd.setSeed(System.currentTimeMillis()); + // prevent browser certificate caches, cause of doubled serial numbers + // using 48bit random number + long sl = ((long) rnd.nextInt()) << 32 | (rnd.nextInt() & 0xFFFFFFFFL); + // let reserve of 16 bit for increasing, serials have to be positive + sl = sl & 0x0000FFFFFFFFFFFFL; + return sl; + } + + private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) throws IOException { + ByteArrayInputStream bIn = new ByteArrayInputStream(key.getEncoded()); + ASN1InputStream is = null; + try { + is = new ASN1InputStream(bIn); + ASN1Sequence seq = (ASN1Sequence) is.readObject(); + SubjectPublicKeyInfo info = SubjectPublicKeyInfo.getInstance(seq); + return new BcX509ExtensionUtils().createSubjectKeyIdentifier(info); + } finally { + NetBareUtils.closeQuietly(is); + } + } + + private static X509Certificate signCertificate(X509v3CertificateBuilder certificateBuilder, + PrivateKey signedWithPrivateKey) throws OperatorCreationException, + CertificateException { + ContentSigner signer = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .setProvider(PROVIDER_NAME) + .build(signedWithPrivateKey); + return new JcaX509CertificateConverter() + .setProvider(PROVIDER_NAME) + .getCertificate(certificateBuilder.build(signer)); + } + + /** + * Uses the non-portable system property sun.arch.data.model to help + * determine if we are running on a 32-bit JVM. Since the majority of modern + * systems are 64 bits, this method "assumes" 64 bits and only returns true + * if sun.arch.data.model explicitly indicates a 32-bit JVM. + * + * @return true if we can determine definitively that this is a 32-bit JVM, + * otherwise false + */ + private static boolean is32BitJvm() { + Integer bits = Integer.getInteger("sun.arch.data.model"); + return bits != null && bits == 32; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java new file mode 100644 index 0000000..8196687 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/CertificateInstallActivity.java @@ -0,0 +1,82 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.security.KeyChain; +import androidx.annotation.Nullable; + +import com.github.megatronking.netbare.NetBareLog; + +import java.io.File; +import java.io.IOException; + +/** + * A translucent activity uses to install self-signed certificate. + * + * @author Megatron King + * @since 2018-11-10 21:18 + */ +public class CertificateInstallActivity extends Activity { + + private static final int REQUEST_CODE_INSTALL = 1; + public static final String EXTRA_ALIAS = "jks_alias"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle bundle = getIntent().getExtras(); + if (bundle == null) { + finish(); + return; + } + Intent intent = KeyChain.createInstallIntent(); + intent.putExtras(bundle); + startActivityForResult(intent, REQUEST_CODE_INSTALL); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CODE_INSTALL && resultCode == RESULT_OK) { + File jsk = new File(getCacheDir(), + getIntent().getStringExtra(EXTRA_ALIAS) + JKS.KEY_JKS_FILE_EXTENSION); + try { + if(!jsk.exists() && !jsk.createNewFile()) { + throw new IOException("Create jks file failed."); + } + } catch (IOException e) { + NetBareLog.wtf(e); + } + } + finish(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putAll(getIntent().getExtras()); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + getIntent().putExtras(savedInstanceState); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java new file mode 100755 index 0000000..a27e798 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/JKS.java @@ -0,0 +1,215 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import android.security.KeyChain; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; + +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.security.KeyStore; +import java.security.cert.Certificate; + +/** + * A java keystore to manage root certificate. + * + * @author Megatron King + * @since 2018-11-10 20:06 + */ +public class JKS { + private static final String JSK_ALIAS = "易开发抓包证书"; + private static volatile JKS jks; + public static final String KEY_STORE_FILE_EXTENSION = ".p12"; + public static final String KEY_PEM_FILE_EXTENSION = ".pem"; + public static final String KEY_JKS_FILE_EXTENSION = ".jks"; + private final File keystoreDir; + private final String alias; + private final char[] password; + private final String commonName; + private final String organization; + private final String organizationalUnitName; + private final String certOrganization; + private final String certOrganizationalUnitName; + + public JKS(@NonNull Context context, @NonNull String alias, @NonNull char[] password, + @NonNull String commonName, @NonNull String organization, + @NonNull String organizationalUnitName, @NonNull String certOrganization, + @NonNull String certOrganizationalUnitName) { + this.keystoreDir = context.getCacheDir(); + this.alias = alias; + this.password = password; + this.commonName = commonName; + this.organization = organization; + this.organizationalUnitName = organizationalUnitName; + this.certOrganization = certOrganization; + this.certOrganizationalUnitName = certOrganizationalUnitName; + createKeystore(); + } + + public static void init(Application application) { + if (jks == null) { + synchronized (JKS.class) { + if (jks == null) { + jks = new JKS(application, JSK_ALIAS, JSK_ALIAS.toCharArray(), JSK_ALIAS, JSK_ALIAS, JSK_ALIAS, JSK_ALIAS, JSK_ALIAS); + } + } + } + } + + public static JKS getJks() { + return jks; + } + + public static String getJskAlias() { + return JSK_ALIAS; + } + + String alias() { + return alias; + } + + char[] password() { + return password; + } + + String commonName() { + return commonName; + } + + String organization() { + return organization; + } + + String organizationalUnitName() { + return organizationalUnitName; + } + + String certOrganisation() { + return certOrganization; + } + + String certOrganizationalUnitName() { + return certOrganizationalUnitName; + } + + public boolean isInstalled() { + return aliasFile(KEY_STORE_FILE_EXTENSION).exists() && + aliasFile(KEY_PEM_FILE_EXTENSION).exists() && + aliasFile(KEY_JKS_FILE_EXTENSION).exists(); + } + + public File aliasFile(String fileExtension) { + return new File(keystoreDir, alias + fileExtension); + } + + private void createKeystore() { + if (aliasFile(KEY_STORE_FILE_EXTENSION).exists() && + aliasFile(KEY_PEM_FILE_EXTENSION).exists()) { + return; + } + + // Generate keystore in the async thread + new Thread(new Runnable() { + @Override + public void run() { + CertificateGenerator generator = new CertificateGenerator(); + KeyStore keystore; + OutputStream os = null; + Writer sw = null; + JcaPEMWriter pw = null; + try { + keystore = generator.generateRoot(JKS.this); + os = new FileOutputStream(aliasFile(KEY_STORE_FILE_EXTENSION)); + keystore.store(os, password()); + + Certificate cert = keystore.getCertificate(alias()); + sw = new FileWriter(aliasFile(KEY_PEM_FILE_EXTENSION)); + pw = new JcaPEMWriter(sw); + pw.writeObject(cert); + pw.flush(); + NetBareLog.i("Generate keystore succeed."); + } catch (Exception e) { + NetBareLog.e(e.getMessage()); + } finally { + NetBareUtils.closeQuietly(os); + NetBareUtils.closeQuietly(sw); + NetBareUtils.closeQuietly(pw); + } + } + }).start(); + } + + /** + * Whether the certificate with given alias has been installed. + * + * @param context Any context. + * @param alias Key store alias. + * @return True if the certificate has been installed. + */ + public static boolean isInstalled(Context context, String alias) { + return new File(context.getCacheDir(), + alias + KEY_JKS_FILE_EXTENSION).exists(); + } + + /** + * Install the self-signed root certificate. + * + * @param context Any context. + * @param name Key chain name. + * @param alias Key store alias. + * @throws IOException If an IO error has occurred. + */ + public static void install(Context context, String name, String alias) + throws IOException { + byte[] keychain; + FileInputStream is = null; + try { + is = new FileInputStream(new File(context.getCacheDir(), + alias + KEY_PEM_FILE_EXTENSION)); + keychain = new byte[is.available()]; + int len = is.read(keychain); + if (len != keychain.length) { + throw new IOException("Install JKS failed, len: " + len); + } + } finally { + NetBareUtils.closeQuietly(is); + } + + Intent intent = new Intent(context, CertificateInstallActivity.class); + intent.putExtra(KeyChain.EXTRA_CERTIFICATE, keychain); + intent.putExtra(KeyChain.EXTRA_NAME, name); + intent.putExtra(CertificateInstallActivity.EXTRA_ALIAS, alias); + if (!(context instanceof Activity)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + context.startActivity(intent); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java new file mode 100644 index 0000000..ff4f6a0 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLCodec.java @@ -0,0 +1,476 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import android.os.Build; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareLog; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; + +/** + * A base class for encrypting and decrypting SSL packets. Use {@link CodecCallback} to + * observe actions and receive output packets. + * + *

SSL handshake steps:

+ * + * client server message + * ====== ====== ======= + * wrap() ... ClientHello + * ... unwrap() ClientHello + * ... wrap() ServerHello/Certificate + * unwrap() ... ServerHello/Certificate + * wrap() ... ClientKeyExchange + * wrap() ... ChangeCipherSpec + * wrap() ... Finished + * ... unwrap() ClientKeyExchange + * ... unwrap() ChangeCipherSpec + * ... unwrap() Finished + * ... wrap() ChangeCipherSpec + * ... wrap() Finished + * unwrap() ... ChangeCipherSpec + * unwrap() ... Finished + * + * @author Megatron King + * @since 2018-11-15 17:46 + */ +public abstract class SSLCodec { + + /** + * Change cipher spec. + */ + public static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = + SSLUtils.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC; + + /** + * Alert. + */ + public static final int SSL_CONTENT_TYPE_ALERT = + SSLUtils.SSL_CONTENT_TYPE_ALERT; + + /** + * Handshake. + */ + public static final int SSL_CONTENT_TYPE_HANDSHAKE = + SSLUtils.SSL_CONTENT_TYPE_HANDSHAKE; + + /** + * Application data. + */ + public static final int SSL_CONTENT_TYPE_APPLICATION_DATA = + SSLUtils.SSL_CONTENT_TYPE_APPLICATION_DATA; + + /** + * HeartBeat Extension. + */ + public static final int SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT = + SSLUtils.SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT; + + /** + * Should larger than generated pem certificate file size. + */ + private static final int DEFAULT_BUFFER_SIZE = 20 * 1024; + + private SSLEngineFactory mSSLEngineFactory; + + private boolean mEngineClosed; + private boolean mHandshakeStarted; + private boolean mHandshakeFinished; + + private Queue mPlaintextBuffers; + + SSLCodec(SSLEngineFactory factory) { + this.mSSLEngineFactory = factory; + this.mPlaintextBuffers = new ConcurrentLinkedDeque<>(); + } + + /** + * Create an {@link SSLEngine} instance to encode and decode SSL packets. + * + * @param factory A factory produces {@link SSLEngine}. + * @return An instance of {@link SSLEngine}. + * @throws IOException If an I/O error has occurred. + */ + protected abstract SSLEngine createEngine(SSLEngineFactory factory) + throws IOException; + + /** + * Handshake with the client or server and try to decode a SSL encrypt packet. + * + * @param buffer The SSL encrypt packet. + * @param callback A callback to observe actions and receive output packets. + * @throws IOException If an I/O error has occurred. + */ + public void decode(ByteBuffer buffer, @NonNull CodecCallback callback) throws IOException { + int verifyResult = SSLUtils.verifyPacket(buffer); + if (!mHandshakeStarted) { + if (verifyResult == SSLUtils.PACKET_NOT_ENCRYPTED) { + callback.onDecrypt(buffer); + return; + } + } + if (verifyResult == SSLUtils.PACKET_NOT_ENOUGH) { + callback.onPending(buffer); + return; + } + decode(createEngine(mSSLEngineFactory), buffer, callback); + } + + /** + * Try to encrypt a plaintext packet. If SSL handshake has finished, then encode it, + * otherwise add it to queue and wait handshake finished. + * + * @param buffer The plaintext packet. + * @param callback A callback to observe actions and receive output packets. + * @throws IOException If an I/O error has occurred. + */ + public void encode(ByteBuffer buffer, @NonNull CodecCallback callback) throws IOException { + if (!buffer.hasRemaining()) { + return; + } + if (mHandshakeFinished) { + wrap(createEngine(mSSLEngineFactory), buffer, callback); + } else { + mPlaintextBuffers.offer(buffer); + } + } + + private void decode(SSLEngine engine, ByteBuffer input, CodecCallback callback) + throws IOException { + // Give up decrypt SSL packet. + if (engine == null) { + callback.onProcess(input); + return; + } + startDecode(engine, input, callback); + } + + private void startDecode(SSLEngine engine, ByteBuffer input, CodecCallback callback) + throws IOException { + if (mEngineClosed) { + return; + } + if (mHandshakeFinished) { + unwrap(engine, input, callback); + } else { + handshake(engine, input, callback); + } + // Start wrap plaintext to engine if possible. + if (mHandshakeFinished && !mPlaintextBuffers.isEmpty()) { + ByteBuffer plaintextBuffer; + while (!mPlaintextBuffers.isEmpty()) { + plaintextBuffer = mPlaintextBuffers.poll(); + if (plaintextBuffer != null && plaintextBuffer.hasRemaining()) { + wrap(engine, plaintextBuffer, callback); + } + } + } + } + + /* package */ void handshake(SSLEngine engine, ByteBuffer input, CodecCallback callback) + throws IOException { + if (!mHandshakeStarted) { + engine.beginHandshake(); + mHandshakeStarted = true; + } + SSLEngineResult.HandshakeStatus status = engine.getHandshakeStatus(); + while (!mHandshakeFinished) { + if (mEngineClosed) { + throw new IOException("Handshake failed: Engine is closed."); + } + if (status == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + // Should never happen + throw new IOException("Handshake failed: Invalid handshake status: " + status); + } else if (status == SSLEngineResult.HandshakeStatus.FINISHED) { + mHandshakeFinished = true; + NetBareLog.i("SSL handshake finished!"); + if (input.hasRemaining()) { + decode(engine, input, callback); + } + } else if (status == SSLEngineResult.HandshakeStatus.NEED_WRAP) { + status = handshakeWrap(engine, callback).getHandshakeStatus(); + } else if (status == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { + // Wait next encrypted buffer. + if (!input.hasRemaining()) { + break; + } + status = handshakeUnwrap(engine, input, callback).getHandshakeStatus(); + } else if (status == SSLEngineResult.HandshakeStatus.NEED_TASK) { + runDelegatedTasks(engine); + } + } + } + + private SSLEngineResult handshakeWrap(SSLEngine engine, CodecCallback callback) + throws IOException { + SSLEngineResult result; + SSLEngineResult.Status status; + ByteBuffer output = allocate(); + while (true) { + result = engineWrap(engine, allocate(0), output); + status = result.getStatus(); + output.flip(); + if (output.hasRemaining()) { + callback.onEncrypt(output); + } + if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { + output = allocate(engine.getSession().getApplicationBufferSize()); + } else { + if (status == SSLEngineResult.Status.CLOSED) { + mEngineClosed = true; + } + break; + } + } + return result; + } + + private SSLEngineResult handshakeUnwrap(SSLEngine engine, ByteBuffer input, + CodecCallback callback) throws IOException { + SSLEngineResult result; + SSLEngineResult.Status status; + ByteBuffer output = allocate(); + while (true) { + result = engineUnwrap(engine, input, output); + status = result.getStatus(); + output.flip(); + int producedSize = output.remaining(); + if (producedSize > 0) { + callback.onDecrypt(output); + } + if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { + int bufferSize = engine.getSession().getApplicationBufferSize() - producedSize; + if (bufferSize < 0) { + bufferSize = engine.getSession().getApplicationBufferSize(); + } + output = allocate(bufferSize); + } else if (status == SSLEngineResult.Status.BUFFER_UNDERFLOW) { + // Store the remaining packet and wait next encrypted buffer. + if (input.hasRemaining()) { + callback.onPending(ByteBuffer.wrap(input.array(), input.position(), + input.remaining())); + // Clear all data. + input.position(0); + input.limit(0); + } + break; + } else if (status == SSLEngineResult.Status.CLOSED) { + mEngineClosed = true; + break; + } else { + // It is status OK. + break; + } + } + return result; + } + + private void unwrap(SSLEngine engine, ByteBuffer input, CodecCallback callback) + throws IOException { + ByteBuffer output = null; + while (true) { + if (output == null) { + output = allocate(); + } + SSLEngineResult result = engineUnwrap(engine, input, output); + SSLEngineResult.Status status = result.getStatus(); + output.flip(); + int producedSize = output.remaining(); + if (producedSize > 0) { + callback.onDecrypt(output); + output = null; + } + if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { + int bufferSize = engine.getSession().getApplicationBufferSize() - producedSize; + if (bufferSize < 0) { + bufferSize = engine.getSession().getApplicationBufferSize(); + } + output = allocate(bufferSize); + } else if (status == SSLEngineResult.Status.BUFFER_UNDERFLOW) { + // Store the remaining packet and wait next encrypted buffer. + if (input.hasRemaining()) { + callback.onPending(ByteBuffer.wrap(input.array(), input.position(), + input.remaining())); + // Clear all data. + input.position(0); + input.limit(0); + } + break; + } else if (status == SSLEngineResult.Status.CLOSED) { + mEngineClosed = true; + break; + } else { + if (!input.hasRemaining()) { + break; + } + } + } + } + + private void wrap(SSLEngine engine, ByteBuffer input, CodecCallback callback) + throws IOException { + ByteBuffer output = allocate(); + while (true) { + SSLEngineResult result = engineWrap(engine, input, output); + SSLEngineResult.Status status = result.getStatus(); + output.flip(); + if (output.hasRemaining()) { + callback.onEncrypt(output); + } + if (status == SSLEngineResult.Status.BUFFER_OVERFLOW) { + output = allocate(engine.getSession().getApplicationBufferSize()); + } else { + if (status == SSLEngineResult.Status.CLOSED) { + mEngineClosed = true; + } + break; + } + } + if (!mEngineClosed && input.hasRemaining()) { + wrap(engine, input, callback); + } + } + + private SSLEngineResult engineWrap(SSLEngine engine, ByteBuffer input, ByteBuffer output) + throws SSLException { + return engine.wrap(input, output); + } + + private SSLEngineResult engineUnwrap(SSLEngine engine, ByteBuffer input, ByteBuffer output) + throws SSLException { + int position = input.position(); + SSLEngineResult result; + // Fixed issue #4 + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { + // In Android 8.1, the BUFFER_OVERFLOW in the unwrap method will throw an + // SSLException-EOFException. We catch this error and increase the output buffer + // capacity. + while (true) { + int inputRemaining = input.remaining(); + try { + result = engine.unwrap(input, output); + break; + } catch (SSLException e) { + if (!output.hasRemaining()) { + // Copy + ByteBuffer outputTemp = ByteBuffer.allocate(output.capacity() * 2); + output.flip(); + outputTemp.put(output); + output = outputTemp; + } else { + // java.io.EOFException: Read error is an Android 8.1 system bug, + // it will cause #4 and #11. We swallowed this exception and not throw. + if ((e.getCause() instanceof EOFException && inputRemaining == 31 && + input.remaining() == 0 && output.remaining() == output.capacity())) { + // Create a new SSLEngineResult. + result = new SSLEngineResult(SSLEngineResult.Status.OK, + SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, + inputRemaining, 0); + break; + } else { + throw e; + } + } + } + } + } else { + result = engine.unwrap(input, output); + } + + // This is a workaround for a bug in Android 5.0. Android 5.0 does not correctly update + // the SSLEngineResult.bytesConsumed() in some cases and just return 0. + // + // See: + // - https://android-review.googlesource.com/c/platform/external/conscrypt/+/122080 + // - https://github.com/netty/netty/issues/7758 + if (result.bytesConsumed() == 0) { + int consumed = input.position() - position; + if (consumed != result.bytesConsumed()) { + // Create a new SSLEngineResult with the correct bytesConsumed(). + result = new SSLEngineResult(result.getStatus(), result.getHandshakeStatus(), + consumed, result.bytesProduced()); + } + } + return result; + } + + private void runDelegatedTasks(SSLEngine engine) { + while (true) { + final Runnable task = engine.getDelegatedTask(); + if (task == null) { + break; + } + task.run(); + } + } + + private ByteBuffer allocate(int size) { + return ByteBuffer.allocate(size); + } + + private ByteBuffer allocate() { + return ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + } + + /** + * A callback to receive {@link SSLCodec} results. + */ + public interface CodecCallback { + + /** + * The packet is not a perfect SSL encrypted packet, should wait next packet and decode + * together. + * + * @param buffer The buffer should be pended in a queue. + */ + void onPending(ByteBuffer buffer); + + /** + * The packet is unable to decrypt or encrypt, should send them to tunnel immediately. + * + * @param buffer Packets should send to tunnel. + * @throws IOException If an I/O error has occurred. + */ + void onProcess(ByteBuffer buffer) throws IOException; + + /** + * Output an encrypted packet. + * + * @param buffer The encrypted packet. + * @throws IOException If an I/O error has occurred. + */ + void onEncrypt(ByteBuffer buffer) throws IOException; + + /** + * Output an decrypted packet, it is a plaintext. + * + * @param buffer The decrypted packet. + * @throws IOException If an I/O error has occurred. + */ + void onDecrypt(ByteBuffer buffer) throws IOException; + + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java new file mode 100644 index 0000000..7669f9b --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLEngineFactory.java @@ -0,0 +1,212 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareUtils; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import org.bouncycastle.operator.OperatorCreationException; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * A factory produces client and server MITM {@link SSLEngine}. + * + * @author Megatron King + * @since 2018-11-10 23:43 + */ +public final class SSLEngineFactory { + + private static final int ALIVE_MINUTES = 10; + private static final int CONCURRENCY_LEVEL = 16; + + /** + * Enforce TLS 1.2 if available, since it's not default up to Java 8. + *

+ * Java 7 disables TLS 1.1 and 1.2 for clients. From Java Cryptography Architecture Oracle Providers Documentation: + * Although SunJSSE in the Java SE 7 release supports TLS 1.1 and TLS 1.2, + * neither version is enabled by default for client connections. Some + * servers do not implement forward compatibility correctly and refuse to + * talk to TLS 1.1 or TLS 1.2 clients. For interoperability, SunJSSE does + * not enable TLS 1.1 or TLS 1.2 by default for client connections. + */ + private static final String SSL_CONTEXT_PROTOCOL = "TLSv1.2"; + + /** + * {@link SSLContext}: Every implementation of the Java platform is required + * to support the following standard SSLContext protocol: TLSv1 + */ + private static final String SSL_CONTEXT_FALLBACK_PROTOCOL = "TLSv1"; + + private final JKS mJKS; + private final Cache mServerSSLContexts; + private final CertificateGenerator mGenerator; + + private Certificate mCaCert; + private PrivateKey mCaPrivKey; + + /** + * Constructs the factory with a self-signed certificate. + * + * @param jks Java keystore of the self-signed certificate. + * @throws GeneralSecurityException If a generic security exception has occurred. + * @throws IOException If an I/O error has occurred. + */ + public SSLEngineFactory(@NonNull JKS jks) throws GeneralSecurityException, IOException { + this.mJKS = jks; + this.mServerSSLContexts = CacheBuilder.newBuilder() + .expireAfterAccess(ALIVE_MINUTES, TimeUnit.MINUTES) + .concurrencyLevel(CONCURRENCY_LEVEL) + .build(); + this.mGenerator = new CertificateGenerator(); + initializeSSLContext(); + } + + /** + * Create a MITM server {@link SSLEngine} with the remote server host. + * + * @param host The remote server host. + * @return A server {@link SSLEngine} instance. + * @throws ExecutionException If an execution error has occurred. + */ + public SSLEngine createServerEngine(@NonNull final String host) throws ExecutionException { + SSLContext ctx = mServerSSLContexts.get(host, new Callable() { + @Override + public SSLContext call() throws GeneralSecurityException, IOException, + OperatorCreationException { + return createServerContext(host); + } + }); + return ctx.createSSLEngine(); + } + + /** + * Create a client {@link SSLEngine} with the remote server IP and port. + * + * @param ip Remote server IP. + * @param port Remote server port. + * @return A client {@link SSLEngine} instance. + * @throws NoSuchAlgorithmException if no Provider supports a SSLContextSpi implementation for + * the specified protocol. + * @throws KeyStoreException If creates KeyManager fail. + * @throws KeyManagementException If creates SSLContext fail. + */ + public SSLEngine createClientEngine(@NonNull final String ip, int port) + throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + SSLContext sslContext = createSSLContext(); + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new KeyManagementException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + sslContext.init(null, new TrustManager[] { trustManagers[0] }, null); + SSLEngine engine = sslContext.createSSLEngine(ip, port); + List ciphers = new LinkedList<>(); + for (String each : engine.getEnabledCipherSuites()) { + if (!each.equals("TLS_DHE_RSA_WITH_AES_128_CBC_SHA") && + !each.equals("TLS_DHE_RSA_WITH_AES_256_CBC_SHA")) { + ciphers.add(each); + } + } + engine.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()])); + engine.setUseClientMode(true); + engine.setNeedClientAuth(false); + return engine; + } + + private void initializeSSLContext() throws GeneralSecurityException, IOException { + KeyStore ks = loadKeyStore(); + mCaCert = ks.getCertificate(mJKS.alias()); + mCaPrivKey = (PrivateKey) ks.getKey(mJKS.alias(), mJKS.password()); + } + + private KeyStore loadKeyStore() throws GeneralSecurityException, IOException { + KeyStore ks = KeyStore.getInstance(mGenerator.keyStoreType()); + FileInputStream is = null; + try { + is = new FileInputStream(mJKS.aliasFile(JKS.KEY_STORE_FILE_EXTENSION)); + ks.load(is, mJKS.password()); + } finally { + NetBareUtils.closeQuietly(is); + } + return ks; + } + + private SSLContext createServerContext(String host) throws GeneralSecurityException, + IOException, OperatorCreationException { + KeyStore ks = mGenerator.generateServer(host, mJKS, mCaCert, mCaPrivKey); + KeyManager[] keyManagers = getKeyManagers(ks); + return createServerContext(keyManagers); + } + + private SSLContext createServerContext(KeyManager[] keyManagers) throws NoSuchAlgorithmException, + KeyManagementException { + SSLContext result = createSSLContext(); + SecureRandom random = new SecureRandom(); + random.setSeed(System.currentTimeMillis() + 1); + result.init(keyManagers, null, random); + return result; + } + + private SSLContext createSSLContext() throws NoSuchAlgorithmException { + try { + return SSLContext.getInstance(SSL_CONTEXT_PROTOCOL); + } catch (NoSuchAlgorithmException e) { + return SSLContext.getInstance(SSL_CONTEXT_FALLBACK_PROTOCOL); + } + } + + private KeyManager[] getKeyManagers(KeyStore keyStore) throws NoSuchAlgorithmException, + UnrecoverableKeyException, KeyStoreException { + String keyManAlg = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyManAlg); + kmf.init(keyStore, mJKS.password()); + return kmf.getKeyManagers(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java new file mode 100644 index 0000000..1d42931 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLRequestCodec.java @@ -0,0 +1,75 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.gateway.Request; + +import java.util.concurrent.ExecutionException; + +import javax.net.ssl.SSLEngine; + +/** + * An implementation of {@link SSLCodec} to codec request SSL packets. This codec creates a MITM + * SSL server engine using {@link SSLEngineFactory}, it requires the remote server host. + * + * @author Megatron King + * @since 2018-11-15 23:23 + */ +public class SSLRequestCodec extends SSLCodec { + + private Request mRequest; + private SSLEngine mEngine; + + /** + * Constructs an instance of {@link SSLCodec} by a factory. + * + * @param factory A factory produces {@link SSLEngine}. + */ + public SSLRequestCodec(SSLEngineFactory factory) { + super(factory); + } + + /** + * Bind a {@link Request} to this codec. + * + * @param request A request has terminated remote server host. + */ + public void setRequest(Request request) { + this.mRequest = request; + } + + @Override + protected SSLEngine createEngine(SSLEngineFactory factory) { + if (mEngine == null) { + String host = mRequest.host(); + if (host == null) { + // Unable to get host. + NetBareLog.e("Failed to get SSL host."); + return null; + } + try { + mEngine = factory.createServerEngine(host); + mEngine.setUseClientMode(false); + mEngine.setNeedClientAuth(false); + } catch (ExecutionException e) { + NetBareLog.e("Failed to create server SSLEngine: " + e.getMessage()); + } + } + return mEngine; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java new file mode 100644 index 0000000..98990fe --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLResponseCodec.java @@ -0,0 +1,114 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.gateway.Request; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +/** + * An implementation of {@link SSLCodec} to codec response SSL packets. This codec creates a SSL + * client engine using {@link SSLEngineFactory}, it requires the remote server ip and host. + * Before encrypt, should call {@link #prepareHandshake()} to start SSL handshake. + * + * @author Megatron King + * @since 2018-11-16 01:30 + */ +public class SSLResponseCodec extends SSLCodec { + + private final SSLEngineFactory mSSLEngineFactory; + + private Request mRequest; + private SSLEngine mEngine; + + /** + * Constructs an instance of {@link SSLCodec} by a factory. + * + * @param factory A factory produces {@link SSLEngine}. + */ + public SSLResponseCodec(SSLEngineFactory factory) { + super(factory); + this.mSSLEngineFactory = factory; + } + + /** + * Bind a {@link Request} to this codec. + * + * @param request A request has terminated remote server ip and port. + */ + public void setRequest(Request request) { + this.mRequest = request; + } + + @Override + protected SSLEngine createEngine(SSLEngineFactory factory) { + if (mEngine == null) { + try { + String host = mRequest.host() != null ? mRequest.host() : mRequest.ip(); + mEngine = factory.createClientEngine(host, mRequest.port()); + mEngine.setUseClientMode(true); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + NetBareLog.e("Failed to create client SSLEngine: " + e.getMessage()); + } + } + return mEngine; + } + + /** + * Prepare and start SSL handshake with the remote server. + * + * @throws IOException If an I/O error has occurred. + */ + public void prepareHandshake() throws IOException { + if (mEngine != null) { + // The handshake was started. + return; + } + SSLEngine engine = createEngine(mSSLEngineFactory); + if (engine == null) { + throw new SSLException("Failed to create client SSLEngine."); + } + ByteBuffer input = ByteBuffer.allocate(0); + handshake(engine, input, new CodecCallback() { + @Override + public void onPending(ByteBuffer buffer) { + } + + @Override + public void onProcess(ByteBuffer buffer) { + } + + @Override + public void onEncrypt(ByteBuffer buffer) throws IOException { + // Send to remote server + mRequest.process(buffer); + } + + @Override + public void onDecrypt(ByteBuffer buffer) { + } + }); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java new file mode 100644 index 0000000..c21b621 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ssl/SSLUtils.java @@ -0,0 +1,251 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ssl; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.http.HttpProtocol; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A SSL utils class. + * + * @author Megatron King + * @since 2018-11-14 11:38 + */ +public final class SSLUtils { + + /** + * Change cipher spec. + */ + public static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20; + + /** + * Alert. + */ + public static final int SSL_CONTENT_TYPE_ALERT = 21; + + /** + * Handshake. + */ + public static final int SSL_CONTENT_TYPE_HANDSHAKE = 22; + + /** + * Application data. + */ + public static final int SSL_CONTENT_TYPE_APPLICATION_DATA = 23; + + /** + * HeartBeat Extension. + */ + public static final int SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT = 24; + + /** + * The length of the ssl record header (in bytes). + */ + private static final int SSL_RECORD_HEADER_LENGTH = 5; + + /** + * Packet length is not enough to determine. + */ + public static final int PACKET_NOT_ENOUGH = 1; + + /** + * It is a plaintext packet. + */ + public static final int PACKET_NOT_ENCRYPTED = 2; + + /** + * It is a valid SSL packet. + */ + public static final int PACKET_SSL = 3; + + /** + * Verify a packet to see whether it is a valid SSL packet. + * + * @param buffer Encrypted SSL packet. + * @return Verification result, one of {@link #PACKET_NOT_ENOUGH}, {@link #PACKET_NOT_ENCRYPTED}, + * and {@link #PACKET_SSL}. + */ + public static int verifyPacket(ByteBuffer buffer) { + final int position = buffer.position(); + // Get the packet length and wait until we get a packets worth of data to unwrap. + if (buffer.remaining() < SSL_RECORD_HEADER_LENGTH) { + NetBareLog.w("No enough ssl/tls packet length: " + buffer.remaining()); + return PACKET_NOT_ENOUGH; + } + int packetLength = 0; + // SSLv3 or TLS - Check ContentType + boolean tls; + switch (unsignedByte(buffer, position)) { + case SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC: + case SSL_CONTENT_TYPE_ALERT: + case SSL_CONTENT_TYPE_HANDSHAKE: + case SSL_CONTENT_TYPE_APPLICATION_DATA: + case SSL_CONTENT_TYPE_EXTENSION_HEARTBEAT: + tls = true; + break; + default: + // SSLv2 or bad data + tls = false; + } + if (tls) { + // SSLv3 or TLS - Check ProtocolVersion + int majorVersion = unsignedByte(buffer, position + 1); + if (majorVersion == 3) { + // SSLv3 or TLS + packetLength = unsignedShort(buffer, position + 3) + SSL_RECORD_HEADER_LENGTH; + if (packetLength <= SSL_RECORD_HEADER_LENGTH) { + // Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data) + tls = false; + } + } else { + // Neither SSLv3 or TLSv1 (i.e. SSLv2 or bad data) + tls = false; + } + } + if (!tls) { + // SSLv2 or bad data - Check the version + int headerLength = (unsignedByte(buffer, position) & 0x80) != 0 ? 2 : 3; + int majorVersion = unsignedByte(buffer, position + headerLength + 1); + if (majorVersion == 2 || majorVersion == 3) { + // SSLv2 + packetLength = headerLength == 2 ? + (buffer.getShort(position) & 0x7FFF) + 2 : (buffer.getShort(position) & 0x3FFF) + 3; + if (packetLength <= headerLength) { + NetBareLog.w("No enough ssl/tls packet length, packet: " + packetLength + + " header: " + headerLength); + // No enough data. + return PACKET_NOT_ENOUGH; + } + } else { + // Not encrypted + return PACKET_NOT_ENCRYPTED; + } + } + // Decode SSL data. + if (packetLength > buffer.remaining()) { + NetBareLog.w("No enough ssl/tls packet length, packet: " + packetLength + + " actual: " + buffer.remaining()); + // Wait until the whole packet can be read. + return PACKET_NOT_ENOUGH; + } + return PACKET_SSL; + } + + public static HttpProtocol[] parseClientHelloAlpn(ByteBuffer clienthelloMessage) { + byte[] buffer = clienthelloMessage.array(); + int offset = clienthelloMessage.position(); + int size = clienthelloMessage.remaining(); + int limit = offset + size; + // Client Hello + if (size <= 43 || buffer[offset] != 0x16) { + return null; + } + // Skip 43 byte header + offset += 43; + // Read sessionID + if (offset + 1 > limit) { + return null; + } + int sessionIDLength = buffer[offset++] & 0xFF; + offset += sessionIDLength; + + // Read cipher suites + if (offset + 2 > limit) { + return null; + } + + int cipherSuitesLength = readShort(buffer, offset) & 0xFFFF; + offset += 2; + offset += cipherSuitesLength; + + // Read Compression method. + if (offset + 1 > limit) { + return null; + } + int compressionMethodLength = buffer[offset++] & 0xFF; + offset += compressionMethodLength; + + // Read Extensions + if (offset + 2 > limit) { + return null; + } + int extensionsLength = readShort(buffer, offset) & 0xFFFF; + offset += 2; + + if (offset + extensionsLength > limit) { + return null; + } + + while (offset + 4 <= limit) { + int type = readShort(buffer, offset) & 0xFFFF; + offset += 2; + int length = readShort(buffer, offset) & 0xFFFF; + offset += 2; + // TYPE_APPLICATION_LAYER_PROTOCOL_NEGOTIATION = 16 + if (type == 16) { + int protocolCount = readShort(buffer, offset) & 0xFFFF; + offset += 2; + length -= 2; + if (offset + length > limit) { + return null; + } + List httpProtocols = new ArrayList<>(); + int read = 0; + while (read <= protocolCount) { + int protocolLength = buffer[offset + read]; + read += 1; + HttpProtocol protocol = HttpProtocol.parse(new String(buffer, offset + read, + protocolLength)); + if (protocol == HttpProtocol.HTTP_1_1 || protocol == HttpProtocol.HTTP_2) { + httpProtocols.add(protocol); + } + read += protocolLength; + } + if (httpProtocols.isEmpty()) { + return null; + } else { + HttpProtocol[] protocols = new HttpProtocol[httpProtocols.size()]; + for (int i = 0; i < protocols.length; i++) { + protocols[i] = httpProtocols.get(i); + } + return protocols; + } + } else { + offset += length; + } + + } + return null; + } + + private static int unsignedByte(ByteBuffer buffer, int index) { + return buffer.get(index) & 0x0FF; + } + + private static int unsignedShort(ByteBuffer buffer, int index) { + return buffer.getShort(index) & 0x0FFFF; + } + + private static short readShort(byte[] data, int offset) { + int r = ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF); + return (short) r; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java new file mode 100644 index 0000000..86a9687 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/ConnectionShutdownException.java @@ -0,0 +1,38 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import java.io.IOException; + +/** + * Thrown when the connection has shutdown due to irresistible reason. + * + * @author Megatron King + * @since 2018-12-20 18:50 + */ +public class ConnectionShutdownException extends IOException { + + /** + * Constructs an {@code ConnectionShutdownException} with the specified detail message. + * + * @param message The detail message (which is saved for later retrieval by the + * {@link #getMessage()} method). + */ + public ConnectionShutdownException(String message) { + super(message); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java new file mode 100644 index 0000000..784e793 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioCallback.java @@ -0,0 +1,61 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import java.io.IOException; + +/** + * A nio selector attachment callback for sending notification to {@link NioTunnel}. + * + * @author Megatron King + * @since 2018-10-15 23:15 + */ +public interface NioCallback { + + /** + * Invoked when the connection is connected with the terminal. + * + * @throws IOException If an I/O error has occurred. + */ + void onConnected() throws IOException; + + /** + * Invoked when the socket IO is readable. + * + * @throws IOException If an I/O error has occurred. + */ + void onRead() throws IOException; + + /** + * Invoked when the socket IO is writable. + * + * @throws IOException If an I/O error has occurred. + */ + void onWrite() throws IOException; + + /** + * Invoked when the socket IO is closed. + */ + void onClosed(); + + /** + * Returns the tunnel using nio attachment. + * + * @return A tunnel. + */ + NioTunnel getTunnel(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java new file mode 100644 index 0000000..189fbd0 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/NioTunnel.java @@ -0,0 +1,195 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.NetBareUtils; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.spi.AbstractSelectableChannel; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * An abstract base nio tunnel class uses nio operations, the sub class should provides IO + * operations, such as connect, read and write. + * + * @param An implementation class for selectable channels + * @param A socket protects by VPN service. + * + * @author Megatron King + * @since 2018-11-18 18:34 + */ +public abstract class NioTunnel implements Closeable, + NioCallback, Tunnel { + + /** + * Let the remote tunnel connects to remote server. + * + * @param address The remote server IP socket address. + * @throws IOException if an I/O error occurs. + */ + public abstract void connect(InetSocketAddress address) throws IOException; + + /** + * Returns the socket should be protected by VPN service. + * + * @return A socket. + */ + public abstract S socket(); + + /** + * Write the packet buffer to remote server. + * + * @param buffer A packet buffer. + * @return The wrote length. + * @throws IOException if an I/O error occurs. + */ + protected abstract int channelWrite(ByteBuffer buffer) throws IOException; + + /** + * Read data from remote server and put it into the given buffer. + * + * @param buffer A buffer to store data. + * @return The read length. + * @throws IOException if an I/O error occurs. + */ + protected abstract int channelRead(ByteBuffer buffer) throws IOException; + + private final T mChannel; + private final Selector mSelector; + private SelectionKey mSelectionKey; + + private Queue mPendingBuffers; + + private NioCallback mCallback; + private boolean mIsClosed; + + NioTunnel(T channel, Selector selector) { + this.mChannel = channel; + this.mSelector = selector; + this.mPendingBuffers = new ConcurrentLinkedDeque<>(); + } + + @Override + public void onConnected() throws IOException { + if (mCallback != null) { + mCallback.onConnected(); + } + } + + @Override + public void onRead() throws IOException { + if (mCallback != null) { + mCallback.onRead(); + } + } + + @Override + public void onWrite() throws IOException { + if (mCallback != null) { + mCallback.onWrite(); + } + // Write pending buffers. + while (!mPendingBuffers.isEmpty()) { + ByteBuffer buffer = mPendingBuffers.poll(); + int remaining = buffer.remaining(); + int sent = channelWrite(buffer); + if (sent < remaining) { + // Should wait next onWrite. + mPendingBuffers.offer(buffer); + return; + } + } + interestRead(); + } + + @Override + public void onClosed() { + if (mCallback != null) { + mCallback.onClosed(); + } + } + + @Override + public NioTunnel getTunnel() { + return this; + } + + @Override + public void close() { + mIsClosed = true; + mPendingBuffers.clear(); + NetBareUtils.closeQuietly(mChannel); + } + + @Override + public void write(ByteBuffer buffer) throws IOException { + if (mIsClosed) { + return; + } + if (!buffer.hasRemaining()) { + return; + } + mPendingBuffers.offer(buffer); + interestWrite(); + } + + public int read(ByteBuffer buffer) throws IOException { + buffer.clear(); + int len = channelRead(buffer); + if (len > 0) { + buffer.flip(); + } + return len; + } + + public boolean isClosed() { + return mIsClosed; + } + + /* package */ void setNioCallback(NioCallback callback) { + this.mCallback = callback; + } + + /* package */ void prepareRead() throws IOException { + if (mChannel.isBlocking()) { + mChannel.configureBlocking(false); + } + mSelector.wakeup(); + mSelectionKey = mChannel.register(mSelector, SelectionKey.OP_READ, this); + } + + private void interestWrite() { + if (mSelectionKey != null) { + mSelector.wakeup(); + mSelectionKey.interestOps(SelectionKey.OP_WRITE); + } + } + + private void interestRead() { + if (mSelectionKey != null) { + mSelector.wakeup(); + mSelectionKey.interestOps(SelectionKey.OP_READ); + } + } + + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java new file mode 100644 index 0000000..978f6dd --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpProxyTunnel.java @@ -0,0 +1,75 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; + +/** + * A TCP tunnel communicates with the VPN service. + * + * @author Megatron King + * @since 2018-11-21 01:40 + */ +public class TcpProxyTunnel extends TcpTunnel { + + private NetBareXLog mLog; + + public TcpProxyTunnel(SocketChannel socketChannel, Selector selector, int remotePort) { + super(socketChannel, selector); + Socket socket = socketChannel.socket(); + this.mLog = new NetBareXLog(Protocol.TCP, socket.getInetAddress().getHostAddress(), + remotePort); + } + + @Override + public void connect(InetSocketAddress address) { + // Nothing to connect + } + + @Override + public void onConnected() throws IOException { + mLog.i("Proxy tunnel is connected."); + super.onConnected(); + } + + @Override + public int read(ByteBuffer buffer) throws IOException { + int len = super.read(buffer); + mLog.i("Read from proxy: " + len); + return len; + } + + @Override + public void write(ByteBuffer buffer) throws IOException { + mLog.i("Write to proxy: " + buffer.remaining()); + super.write(buffer); + } + + @Override + public void close() { + mLog.i("Proxy tunnel is closed."); + super.close(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java new file mode 100644 index 0000000..5eb660f --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpRemoteTunnel.java @@ -0,0 +1,84 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import android.net.VpnService; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; + +/** + * A TCP tunnel communicates with the remote server. + * + * @author Megatron King + * @since 2018-11-21 01:41 + */ +public class TcpRemoteTunnel extends TcpTunnel { + + private final VpnService mVpnService; + + private NetBareXLog mLog; + + public TcpRemoteTunnel(VpnService vpnService, SocketChannel channel, Selector selector, + String remoteIp, int remotePort) { + super(channel, selector); + this.mVpnService = vpnService; + this.mLog = new NetBareXLog(Protocol.TCP, remoteIp, remotePort); + } + + @Override + public void connect(InetSocketAddress address) throws IOException { + if (mVpnService.protect(socket())) { + super.connect(address); + mLog.i("Connect to remote server %s", address); + } else { + throw new IOException("[TCP]Can not protect remote tunnel socket."); + } + } + + @Override + public void onConnected() throws IOException { + mLog.i("Remote tunnel is connected."); + super.onConnected(); + } + + @Override + public int read(ByteBuffer buffer) throws IOException { + int len = super.read(buffer); + mLog.i("Read from remote: " + len); + return len; + } + + @Override + public void write(ByteBuffer buffer) throws IOException { + mLog.i("Write to remote: " + buffer.remaining()); + super.write(buffer); + } + + @Override + public void close() { + mLog.i("Remote tunnel is closed."); + super.close(); + } + + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java new file mode 100644 index 0000000..84c38aa --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpTunnel.java @@ -0,0 +1,80 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.NetBareLog; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; + +/** + * A TCP protocol implementation based with {@link NioTunnel}. + * + * @author Megatron King + * @since 2018-11-21 00:36 + */ +public abstract class TcpTunnel extends NioTunnel { + + private final SocketChannel mSocketChannel; + private final Selector mSelector; + + TcpTunnel(SocketChannel socketChannel, Selector selector) { + super(socketChannel, selector); + this.mSocketChannel = socketChannel; + this.mSelector = selector; + } + + @Override + public void connect(InetSocketAddress address) throws IOException { + NetBareLog.i("TCP connects to: %s:%s", + address.getAddress().getHostAddress(), address.getPort()); + if (mSocketChannel.isBlocking()) { + mSocketChannel.configureBlocking(false); + } + mSocketChannel.register(mSelector, SelectionKey.OP_CONNECT, this); + mSocketChannel.connect(address); + } + + @Override + public void onConnected() throws IOException { + if (mSocketChannel.finishConnect()) { + super.onConnected(); + } else { + throw new IOException("[TCP]The tunnel socket is not connected."); + } + } + + @Override + public Socket socket() { + return mSocketChannel.socket(); + } + + @Override + protected int channelRead(ByteBuffer buffer) throws IOException { + return mSocketChannel.read(buffer); + } + + @Override + protected int channelWrite(ByteBuffer buffer) throws IOException { + return mSocketChannel.write(buffer); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java new file mode 100644 index 0000000..92545f6 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/TcpVATunnel.java @@ -0,0 +1,163 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.NetBareVirtualGateway; +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.net.Session; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +/** + * TCP protocol virtual gateway tunnel wraps {@link TcpProxyTunnel} and {@link TcpRemoteTunnel} as + * client and server. + * + * @author Megatron King + * @since 2018-11-18 00:19 + */ +public class TcpVATunnel extends VirtualGatewayTunnel { + + private final NioTunnel mRemoteTunnel; + private final NioTunnel mProxyTunnel; + private VirtualGateway mGateway; + + private final int mMtu; + + public TcpVATunnel(Session session, NioTunnel proxyServerTunnel, NioTunnel remoteServerTunnel, int mtu) { + this.mProxyTunnel = proxyServerTunnel; + this.mRemoteTunnel = remoteServerTunnel; + this.mGateway = new NetBareVirtualGateway(session, new Request(mRemoteTunnel), + new Response(mProxyTunnel)); + + this.mMtu = mtu; + + setCallbacks(); + } + + @Override + public VirtualGateway getGateway() { + return mGateway; + } + + @Override + public void connect(InetSocketAddress address) throws IOException { + mRemoteTunnel.connect(address); + } + + private void setCallbacks() { + mProxyTunnel.setNioCallback(new NioCallback() { + @Override + public void onConnected() { + // Nothing to do. + } + + @Override + public void onRead() throws IOException { + if (mProxyTunnel.isClosed()) { + mGateway.sendResponseFinished(); + return; + } + ByteBuffer buffer = ByteBuffer.allocate(mMtu); + int len; + try { + len = mProxyTunnel.read(buffer); + } catch (IOException e) { + throw new ConnectionShutdownException(e.getMessage()); + } + if (len < 0 || mRemoteTunnel.isClosed()) { + NetBareUtils.closeQuietly(mProxyTunnel); + mGateway.sendResponseFinished(); + return; + } + mGateway.sendRequest(buffer); + } + + @Override + public void onWrite() { + // Do nothing + } + + @Override + public void onClosed() { + close(); + } + + @Override + public NioTunnel getTunnel() { + return null; + } + }); + mRemoteTunnel.setNioCallback(new NioCallback() { + @Override + public void onConnected() throws IOException { + // Prepare to read data. + mProxyTunnel.prepareRead(); + mRemoteTunnel.prepareRead(); + } + + @Override + public void onRead() throws IOException { + if (mRemoteTunnel.isClosed()) { + mGateway.sendRequestFinished(); + return; + } + ByteBuffer buffer = ByteBuffer.allocate(mMtu); + int len; + try { + len = mRemoteTunnel.read(buffer); + } catch (IOException e) { + throw new ConnectionShutdownException(e.getMessage()); + } + if (len < 0 || mProxyTunnel.isClosed()) { + NetBareUtils.closeQuietly(mRemoteTunnel); + mGateway.sendRequestFinished(); + return; + } + mGateway.sendResponse(buffer); + } + + @Override + public void onWrite() { + // Do nothing + } + + @Override + public void onClosed() { + close(); + } + + @Override + public NioTunnel getTunnel() { + return null; + } + + }); + } + + @Override + public void close() { + NetBareUtils.closeQuietly(mProxyTunnel); + NetBareUtils.closeQuietly(mRemoteTunnel); + mGateway.sendRequestFinished(); + mGateway.sendResponseFinished(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java new file mode 100644 index 0000000..cb6dd63 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/Tunnel.java @@ -0,0 +1,37 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A tunnel connects a terminal that can be wrote to. + * + * @author Megatron King + * @since 2018-10-25 17:57 + */ +public interface Tunnel { + + /** + * Write a packet buffer to the terminal. + * + * @param buffer A packet buffer. + * @throws IOException If an I/O error has occurred. + */ + void write(ByteBuffer buffer) throws IOException; + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java new file mode 100644 index 0000000..47359ac --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpRemoteTunnel.java @@ -0,0 +1,76 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import android.net.VpnService; + +import com.github.megatronking.netbare.NetBareXLog; +import com.github.megatronking.netbare.ip.Protocol; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.Selector; + +/** + * A UDP tunnel communicates with the remote server. + * + * @author Megatron King + * @since 2018-12-02 19:49 + */ +public class UdpRemoteTunnel extends UdpTunnel { + + private final VpnService mVpnService; + private NetBareXLog mLog; + + public UdpRemoteTunnel(VpnService vpnService, DatagramChannel channel, Selector selector, + String remoteIp, short remotePort) { + super(channel, selector); + this.mVpnService = vpnService; + this.mLog = new NetBareXLog(Protocol.UDP, remoteIp, remotePort); + } + + @Override + public void connect(InetSocketAddress address) throws IOException { + if (mVpnService.protect(socket())) { + super.connect(address); + mLog.i("Connect to remote server %s", address); + } else { + throw new IOException("[UDP]Can not protect remote tunnel socket."); + } + } + + @Override + public int read(ByteBuffer buffer) throws IOException { + int len = super.read(buffer); + mLog.i("Read from remote: " + len); + return len; + } + + @Override + public void write(ByteBuffer buffer) throws IOException { + mLog.i("Write to remote: " + buffer.remaining()); + super.write(buffer); + } + + @Override + public void close() { + mLog.i("Remote tunnel is closed."); + super.close(); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java new file mode 100644 index 0000000..9d6fc81 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpTunnel.java @@ -0,0 +1,68 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.NetBareLog; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.Selector; + +/** + * A UDP protocol implementation based with {@link NioTunnel}. + * + * @author Megatron King + * @since 2018-12-02 19:24 + */ +public abstract class UdpTunnel extends NioTunnel { + + private final DatagramChannel mDatagramChannel; + + UdpTunnel(DatagramChannel datagramChannel, Selector selector) { + super(datagramChannel, selector); + this.mDatagramChannel = datagramChannel; + } + + @Override + public void connect(InetSocketAddress address) throws IOException { + NetBareLog.i("UDP connects to: %s:%s", + address.getAddress().getHostAddress(), address.getPort()); + if (mDatagramChannel.isBlocking()) { + mDatagramChannel.configureBlocking(false); + } + mDatagramChannel.connect(address); + prepareRead(); + } + + @Override + public DatagramSocket socket() { + return mDatagramChannel.socket(); + } + + @Override + protected int channelWrite(ByteBuffer buffer) throws IOException { + return mDatagramChannel.write(buffer); + } + + @Override + protected int channelRead(ByteBuffer buffer) throws IOException { + return mDatagramChannel.read(buffer); + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java new file mode 100644 index 0000000..05b5f67 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/UdpVATunnel.java @@ -0,0 +1,197 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.NetBareLog; +import com.github.megatronking.netbare.NetBareUtils; +import com.github.megatronking.netbare.NetBareVirtualGateway; +import com.github.megatronking.netbare.gateway.Request; +import com.github.megatronking.netbare.gateway.Response; +import com.github.megatronking.netbare.gateway.VirtualGateway; +import com.github.megatronking.netbare.ip.IpHeader; +import com.github.megatronking.netbare.ip.UdpHeader; +import com.github.megatronking.netbare.net.Session; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +/** + * UDP protocol virtual gateway tunnel wraps {@link UdpRemoteTunnel} and itself as client and + * server. + * + * @author Megatron King + * @since 2018-11-25 20:16 + */ +public class UdpVATunnel extends VirtualGatewayTunnel implements NioCallback, + Tunnel { + + private final NioTunnel mRemoteTunnel; + private final OutputStream mOutput; + + private final int mMtu; + + private Session mSession; + private VirtualGateway mGateway; + + private UdpHeader mTemplateHeader; + + public UdpVATunnel(Session session, NioTunnel tunnel, OutputStream output, int mtu) { + this.mRemoteTunnel = tunnel; + this.mOutput = output; + this.mMtu = mtu; + + this.mSession = session; + this.mGateway = new NetBareVirtualGateway(session, + new Request(mRemoteTunnel), new Response(this)); + + this.mRemoteTunnel.setNioCallback(this); + } + + @Override + public void connect(InetSocketAddress address) throws IOException { + mRemoteTunnel.connect(address); + } + + @Override + public VirtualGateway getGateway() { + return mGateway; + } + + @Override + public void onConnected() { + } + + @Override + public void onRead() throws IOException { + if (mRemoteTunnel.isClosed()) { + mGateway.sendRequestFinished(); + mGateway.sendResponseFinished(); + return; + } + ByteBuffer buffer = ByteBuffer.allocate(mMtu); + int len; + try { + len = mRemoteTunnel.read(buffer); + } catch (IOException e) { + throw new ConnectionShutdownException(e.getMessage()); + } + if (len < 0) { + close(); + return; + } + mGateway.sendResponse(buffer); + } + + @Override + public void onWrite() { + } + + @Override + public void onClosed() { + close(); + } + + @Override + public NioTunnel getTunnel() { + return null; + } + + @Override + public void close() { + NetBareUtils.closeQuietly(mRemoteTunnel); + mGateway.sendRequestFinished(); + mGateway.sendResponseFinished(); + } + + public void send(UdpHeader header) { + if (mRemoteTunnel.isClosed()) { + return; + } + // Clone a template by the send data. + if (mTemplateHeader == null) { + mTemplateHeader = createTemplate(header); + } + + try { + mGateway.sendRequest(header.data()); + } catch (IOException e) { + NetBareLog.e(e.getMessage()); + close(); + } + } + + @Override + public void write(ByteBuffer buffer) throws IOException { + // Write to vpn. + UdpHeader header = mTemplateHeader.copy(); + ByteBuffer headerBuffer = header.buffer(); + int headLength = header.getIpHeader().getHeaderLength() + header.getHeaderLength(); + byte[] packet = new byte[headLength + buffer.remaining()]; + headerBuffer.get(packet, 0, headLength); + buffer.get(packet, headLength, packet.length - headLength); + + IpHeader ipHeader = new IpHeader(packet, 0); + ipHeader.setTotalLength((short) packet.length); + + UdpHeader udpHeader = new UdpHeader(ipHeader, packet, ipHeader.getHeaderLength()); + udpHeader.setTotalLength((short) (packet.length - ipHeader.getHeaderLength())); + + ipHeader.updateChecksum(); + udpHeader.updateChecksum(); + + mOutput.write(packet, 0, packet.length); + + mSession.receiveDataSize += packet.length; + } + + public NioTunnel getRemoteChannel() { + return mRemoteTunnel; + } + + private UdpHeader createTemplate(UdpHeader header) { + UdpHeader templateUdp = header.copy(); + IpHeader templateIp = templateUdp.getIpHeader(); + // Swap ip + int sourceIp = templateIp.getSourceIp(); + int destinationIp = templateIp.getDestinationIp(); + templateIp.setSourceIp(destinationIp); + templateIp.setDestinationIp(sourceIp); + // Swap port + short sourcePort = templateUdp.getSourcePort(); + short destinationPort = templateUdp.getDestinationPort(); + templateUdp.setDestinationPort(sourcePort); + templateUdp.setSourcePort(destinationPort); + return templateUdp; + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java new file mode 100644 index 0000000..07db766 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/tunnel/VirtualGatewayTunnel.java @@ -0,0 +1,47 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.tunnel; + +import com.github.megatronking.netbare.gateway.VirtualGateway; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; + +/** + * A tunnel uses {@link VirtualGateway} to intercept and filter net packets. + * + * @author Megatron King + * @since 2018-11-21 09:00 + */ +public abstract class VirtualGatewayTunnel implements Closeable { + + /** + * Connects to the remote server by the given server address. + * + * @param address The server IP socket address. + * @throws IOException If an I/O error has occurred. + */ + public abstract void connect(InetSocketAddress address) throws IOException; + + /** + * Returns the {@link VirtualGateway} this tunnel created. + * + * @return The {@link VirtualGateway} instance. + */ + public abstract VirtualGateway getGateway(); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java new file mode 100644 index 0000000..3b76f4b --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketCallback.java @@ -0,0 +1,66 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.ws; + +import java.io.IOException; + +/** + * A callback to be invoked when the message is decoded. + * + * @author Megatron King + * @since 2019/1/18 23:56 + */ +public interface WebSocketCallback { + + /** + * Invoked when a text message is decoded. + * + * @param text A text content. + * @throws IOException If an I/O error has occurred. + */ + void onReadMessage(String text) throws IOException; + + /** + * Invoked when a binary message is decoded. + * + * @param binary A binary content. + * @throws IOException If an I/O error has occurred. + */ + void onReadMessage(byte[] binary) throws IOException; + + /** + * Invoked when a ping message is decoded. + * + * @param ping The ping message content. + */ + void onReadPing(byte[] ping); + + /** + * Invoked when a pong message is decoded. + * + * @param pong The pong message content. + */ + void onReadPong(byte[] pong); + + /** + * Invoked when the control frame is closed. + * + * @param code Status code. + * @param reason Close reason. + */ + void onReadClose(int code, String reason); + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java new file mode 100644 index 0000000..aaf7773 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketProtocol.java @@ -0,0 +1,145 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.ws; + +/** + * Protocol see: http://tools.ietf.org/html/rfc6455 + * + * @author Megatron King + * @since 2019/1/18 23:27 + */ +/* package */ final class WebSocketProtocol { + + /** + * Byte 0 flag for whether this is the final fragment in a message. + */ + static final int B0_FLAG_FIN = 0b10000000; + + /** + * Byte 0 reserved flag 1. Must be 0 unless negotiated otherwise. + */ + static final int B0_FLAG_RSV1 = 0b01000000; + + /** + * Byte 0 reserved flag 2. Must be 0 unless negotiated otherwise. + */ + static final int B0_FLAG_RSV2 = 0b00100000; + + /** + * Byte 0 reserved flag 3. Must be 0 unless negotiated otherwise. + */ + static final int B0_FLAG_RSV3 = 0b00010000; + + /** + * Byte 0 mask for the frame opcode. + */ + static final int B0_MASK_OPCODE = 0b00001111; + + /** + * Flag in the opcode which indicates a control frame. + */ + static final int OPCODE_FLAG_CONTROL = 0b00001000; + + /** + * Byte 1 flag for whether the payload data is masked.

If this flag is set, the next four + * bytes represent the mask key. These bytes appear after any additional bytes specified by {@link + * #B1_MASK_LENGTH}. + */ + static final int B1_FLAG_MASK = 0b10000000; + /** + * Byte 1 mask for the payload length.

If this value is {@link #PAYLOAD_SHORT}, the next two + * bytes represent the length. If this value is {@link #PAYLOAD_LONG}, the next eight bytes + * represent the length. + */ + static final int B1_MASK_LENGTH = 0b01111111; + + static final int OPCODE_CONTINUATION = 0x0; + static final int OPCODE_TEXT = 0x1; + static final int OPCODE_BINARY = 0x2; + + static final int OPCODE_CONTROL_CLOSE = 0x8; + static final int OPCODE_CONTROL_PING = 0x9; + static final int OPCODE_CONTROL_PONG = 0xa; + + /** + * Maximum length of frame payload. Larger payloads, if supported by the frame type, can use the + * special values {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}. + */ + static final long PAYLOAD_BYTE_MAX = 125L; + + /** + * Maximum length of close message in bytes. + */ + static final long CLOSE_MESSAGE_MAX = PAYLOAD_BYTE_MAX - 2; + + /** + * Value for {@link #B1_MASK_LENGTH} which indicates the next two bytes are the unsigned length. + */ + static final int PAYLOAD_SHORT = 126; + + /** + * Maximum length of a frame payload to be denoted as {@link #PAYLOAD_SHORT}. + */ + static final long PAYLOAD_SHORT_MAX = 0xffffL; + + /** + * Value for {@link #B1_MASK_LENGTH} which indicates the next eight bytes are the unsigned + * length. + */ + static final int PAYLOAD_LONG = 127; + + /** + * Used when an unchecked exception was thrown in a listener. + */ + static final int CLOSE_CLIENT_GOING_AWAY = 1001; + + /** + * Used when an empty close frame was received (i.e., without a status code). + */ + static final int CLOSE_NO_STATUS_CODE = 1005; + + static String closeCodeExceptionMessage(int code) { + if (code < 1000 || code >= 5000) { + return "Code must be in range [1000,5000): " + code; + } else if ((code >= 1004 && code <= 1006) || (code >= 1012 && code <= 2999)) { + return "Code " + code + " is reserved and may not be used."; + } else { + return null; + } + } + + static void toggleMask(byte[] data, byte[] key) { + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (data[i] ^ key[i % key.length]); + } + } + +} diff --git a/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java new file mode 100644 index 0000000..fa85971 --- /dev/null +++ b/netbare-core/src/main/java/com/github/megatronking/netbare/ws/WebSocketReader.java @@ -0,0 +1,313 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.megatronking.netbare.ws; + +import com.github.megatronking.netbare.NetBareUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static java.lang.Integer.toHexString; + +/** + * A web socket frame reader. + * + * @author Megatron King + * @since 2019/1/18 23:52 + */ +public class WebSocketReader { + + private final InputStream mInput; + private final boolean mClient; + private final WebSocketCallback mCallback; + + private final byte[] mMaskKey; + + private boolean mClosed; + + private int mOpcode; + private long mFrameLength; + private boolean mFinalFrame; + private boolean mControlFrame; + + private List mMessageSegments; + + public WebSocketReader(InputStream input, boolean client, WebSocketCallback callback) { + this.mInput = input; + this.mClient = client; + this.mCallback = callback; + + // Masks are only a concern for server writers. + this.mMaskKey = client ? null : new byte[4]; + + this.mMessageSegments = new ArrayList<>(1); + } + + /** + * Process the next protocol frame. + */ + public void processNextFrame() throws IOException { + readHeader(); + if (mControlFrame) { + readControlFrame(); + } else { + readMessageFrame(); + } + } + + /** + * Close the input stream. + */ + public void close() { + mClosed = true; + NetBareUtils.closeQuietly(mInput); + } + + private void readHeader() throws IOException { + if (mClosed) { + throw new IOException("The stream is closed."); + } + + // Each frame starts with two bytes of data. + // + // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + // +-+-+-+-+-------+ +-+-------------+ + // |F|R|R|R| OP | |M| LENGTH | + // |I|S|S|S| CODE | |A| | + // |N|V|V|V| | |S| | + // | |1|2|3| | |K| | + // +-+-+-+-+-------+ +-+-------------+ + + // Read first byte + int b0 = mInput.read() & 0xff; + + mOpcode = b0 & WebSocketProtocol.B0_MASK_OPCODE; + mFinalFrame = (b0 & WebSocketProtocol.B0_FLAG_FIN) != 0; + mControlFrame = (b0 & WebSocketProtocol.OPCODE_FLAG_CONTROL) != 0; + + // Control frames must be final frames (cannot contain continuations). + if (mControlFrame && !mFinalFrame) { + throw new ProtocolException("Control frames must be final."); + } + + boolean reservedFlag1 = (b0 & WebSocketProtocol.B0_FLAG_RSV1) != 0; + boolean reservedFlag2 = (b0 & WebSocketProtocol.B0_FLAG_RSV2) != 0; + boolean reservedFlag3 = (b0 & WebSocketProtocol.B0_FLAG_RSV3) != 0; + if (reservedFlag1 || reservedFlag2 || reservedFlag3) { + // Reserved flags are for extensions which we currently do not support. + throw new ProtocolException("Reserved flags are unsupported."); + } + + int b1 = mInput.read() & 0xff; + + boolean isMasked = (b1 & WebSocketProtocol.B1_FLAG_MASK) != 0; + if (isMasked == mClient) { + // Masked payloads must be read on the server. Unmasked payloads must be read on the client. + throw new ProtocolException(mClient + ? "Server-sent frames must not be masked." + : "Client-sent frames must be masked."); + } + + // Get frame length, optionally reading from follow-up bytes if indicated by special values. + mFrameLength = b1 & WebSocketProtocol.B1_MASK_LENGTH; + if (mFrameLength == WebSocketProtocol.PAYLOAD_SHORT) { + mFrameLength = readShort() & 0xffffL; // Value is unsigned. + } else if (mFrameLength == WebSocketProtocol.PAYLOAD_LONG) { + mFrameLength = readLong(); + if (mFrameLength < 0) { + throw new ProtocolException( + "Frame length 0x" + Long.toHexString(mFrameLength) + " > 0x7FFFFFFFFFFFFFFF"); + } + } + + if (mControlFrame && mFrameLength > WebSocketProtocol.PAYLOAD_BYTE_MAX) { + throw new ProtocolException("Control frame must be less than " + + WebSocketProtocol.PAYLOAD_BYTE_MAX + "B."); + } + + if (isMasked) { + // Read the masking key as bytes so that they can be used directly for unmasking. + readFully(mMaskKey); + } + + } + + private void readControlFrame() throws IOException { + if (mFrameLength >= Integer.MAX_VALUE) { + throw new IOException("Not support a frame length > " + Integer.MAX_VALUE); + } + WebSocketCallback callback = mCallback; + if (callback == null) { + return; + } + ByteBuffer byteBuffer; + if (mFrameLength > 0) { + byte[] frame = new byte[(int) mFrameLength]; + readFully(frame); + if (!mClient) { + WebSocketProtocol.toggleMask(frame, mMaskKey); + } + byteBuffer = ByteBuffer.wrap(frame); + } else { + byteBuffer = ByteBuffer.allocate(0); + } + switch (mOpcode) { + case WebSocketProtocol.OPCODE_CONTROL_PING: + callback.onReadPing(readFully(byteBuffer)); + break; + case WebSocketProtocol.OPCODE_CONTROL_PONG: + callback.onReadPong(readFully(byteBuffer)); + break; + case WebSocketProtocol.OPCODE_CONTROL_CLOSE: + int code = WebSocketProtocol.CLOSE_NO_STATUS_CODE; + String reason = ""; + long bufferSize = byteBuffer.remaining(); + if (bufferSize == 1) { + throw new ProtocolException("Malformed close payload length of 1."); + } else if (bufferSize != 0) { + code = byteBuffer.getShort(); + reason = new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.remaining()); + String codeExceptionMessage = WebSocketProtocol.closeCodeExceptionMessage(code); + if (codeExceptionMessage != null) { + throw new ProtocolException(codeExceptionMessage); + } + } + callback.onReadClose(code, reason); + break; + default: + throw new ProtocolException("Unknown control opcode: " + toHexString(mOpcode)); + } + } + + private void readMessageFrame() throws IOException { + int opcode = this.mOpcode; + if (opcode != WebSocketProtocol.OPCODE_TEXT && opcode != WebSocketProtocol.OPCODE_BINARY) { + throw new ProtocolException("Unknown opcode: " + toHexString(opcode)); + } + + readMessage(); + + WebSocketCallback callback = mCallback; + if (callback == null) { + return; + } + if (mMessageSegments.isEmpty()) { + throw new ProtocolException("Message frame segment is empty!"); + } + int total = 0; + for (byte[] segment : mMessageSegments) { + total+= segment.length; + } + ByteBuffer byteBuffer = ByteBuffer.allocate(total); + for (byte[] segment : mMessageSegments) { + byteBuffer.put(segment); + } + byteBuffer.flip(); + mMessageSegments.clear(); + if (opcode == WebSocketProtocol.OPCODE_TEXT) { + callback.onReadMessage(new String(byteBuffer.array())); + } else { + callback.onReadMessage(byteBuffer.array()); + } + } + + private void readMessage() throws IOException { + while (true) { + if (mClosed) { + throw new IOException("The stream is closed."); + } + if (mFrameLength <= 0) { + return; + } + if (mFrameLength >= Integer.MAX_VALUE) { + throw new IOException("Not support a frame length > " + Integer.MAX_VALUE); + } + byte[] frame = new byte[(int) mFrameLength]; + readFully(frame); + if (!mClient) { + WebSocketProtocol.toggleMask(frame, mMaskKey); + } + mMessageSegments.add(frame); + + if (mFinalFrame) { + break; // We are exhausted and have no continuations. + } + + readUntilNonControlFrame(); + if (mOpcode != WebSocketProtocol.OPCODE_CONTINUATION) { + throw new ProtocolException("Expected continuation opcode. Got: " + toHexString(mOpcode)); + } + } + } + + private void readUntilNonControlFrame() throws IOException { + while (!mClosed) { + readHeader(); + if (!mControlFrame) { + break; + } + readControlFrame(); + } + } + + private short readShort() throws IOException { + return (short) ((mInput.read() & 0xFF) << 8 + | (mInput.read() & 0xFF)); + } + + private long readLong() throws IOException { + return (mInput.read() & 0xFFL) << 56 + | (mInput.read() & 0xFFL) << 48 + | (mInput.read() & 0xFFL) << 40 + | (mInput.read() & 0xFFL) << 32 + | (mInput.read() & 0xFFL) << 24 + | (mInput.read() & 0xFFL) << 16 + | (mInput.read() & 0xFFL) << 8 + | (mInput.read() & 0xFFL); + } + + private void readFully(byte[] bytes) throws IOException { + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) mInput.read(); + } + } + + private byte[] readFully(ByteBuffer byteBuffer) { + byte[] data = new byte[byteBuffer.remaining()]; + byteBuffer.get(data); + return data; + } + +} diff --git a/netbare-core/src/main/res/values/styles.xml b/netbare-core/src/main/res/values/styles.xml new file mode 100644 index 0000000..fdd919c --- /dev/null +++ b/netbare-core/src/main/res/values/styles.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/netbare-injector/.gitignore b/netbare-injector/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/netbare-injector/.gitignore @@ -0,0 +1 @@ +/build diff --git a/netbare-injector/build.gradle b/netbare-injector/build.gradle new file mode 100644 index 0000000..3b52679 --- /dev/null +++ b/netbare-injector/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation project(':netbare-core') + implementation 'com.android.support:appcompat-v7:28.0.0' +} \ No newline at end of file diff --git a/netbare-injector/src/main/AndroidManifest.xml b/netbare-injector/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4ad7016 --- /dev/null +++ b/netbare-injector/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java new file mode 100644 index 0000000..af59cae --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/Cookie.java @@ -0,0 +1,581 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.text.TextUtils; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An RFC 6265 Cookie. + * + * @author Megatron King + * @since 2018/12/25 21:56 + */ +public final class Cookie { + + private static final Pattern YEAR_PATTERN + = Pattern.compile("(\\d{2,4})[^\\d]*"); + private static final Pattern MONTH_PATTERN + = Pattern.compile("(?i)(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*"); + private static final Pattern DAY_OF_MONTH_PATTERN + = Pattern.compile("(\\d{1,2})[^\\d]*"); + private static final Pattern TIME_PATTERN + = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})[^\\d]*"); + + private static final TimeZone UTC = TimeZone.getTimeZone("GMT"); + + private static final long MAX_DATE = 253402300799999L; + + /** + * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such + * cookies are on the fast path. + */ + private static final ThreadLocal STANDARD_DATE_FORMAT = + new ThreadLocal() { + @Override + protected DateFormat initialValue() { + // Date format specified by RFC 7231 section 7.1.1.1. + DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); + rfc1123.setLenient(false); + rfc1123.setTimeZone(UTC); + return rfc1123; + } + }; + + public final String name; + public final String value; + public final long expiresAt; + public final String domain; + public final String path; + public final String priority; + public final boolean secure; + public final boolean httpOnly; + + /** + * True if 'expires' or 'max-age' is present. + */ + public final boolean persistent; + + /** + * True unless 'domain' is present. + */ + public final boolean hostOnly; + + private Cookie(String name, String value, long expiresAt, String domain, String path, + boolean secure, boolean httpOnly, boolean hostOnly, boolean persistent, + String priority) { + this.name = name; + this.value = value; + this.expiresAt = expiresAt; + this.domain = domain; + this.path = path; + this.secure = secure; + this.httpOnly = httpOnly; + this.persistent = persistent; + this.hostOnly = hostOnly; + this.priority = priority; + } + + private Cookie(Builder builder) { + this.name = builder.name; + this.value = builder.value; + this.expiresAt = builder.expiresAt; + this.domain = builder.domain; + this.path = builder.path; + this.priority = builder.priority; + this.secure = builder.secure; + this.httpOnly = builder.httpOnly; + this.persistent = builder.persistent; + this.hostOnly = builder.hostOnly; + } + + public String expiresAt() { + if (!persistent || expiresAt == Long.MIN_VALUE) { + // A session date. + return "1969-12-31T23:59:59.000Z"; + } + DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + format.setLenient(false); + format.setTimeZone(UTC); + return format.format(new Date(expiresAt)); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(name); + result.append('='); + result.append(value); + if (persistent) { + if (expiresAt == Long.MIN_VALUE) { + result.append("; max-age=0"); + } else { + result.append("; expires=").append(STANDARD_DATE_FORMAT.get().format(new Date(expiresAt))); + } + } + if (!hostOnly) { + result.append("; domain="); + result.append(domain); + } + result.append("; path=").append(path); + + if (secure) { + result.append("; secure"); + } + if (httpOnly) { + result.append("; httponly"); + } + if (priority != null) { + result.append("; Priority=").append(priority); + } + return result.toString(); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof Cookie)) { + return false; + } + Cookie that = (Cookie) other; + return that.name.equals(name) + && that.value.equals(value) + && that.domain.equals(domain) + && that.path.equals(path) + && that.expiresAt == expiresAt + && that.secure == secure + && that.httpOnly == httpOnly + && that.persistent == persistent + && that.hostOnly == hostOnly + && TextUtils.equals(that.priority, priority); + } + + @Override + public int hashCode() { + int hash = 17; + hash = 31 * hash + name.hashCode(); + hash = 31 * hash + value.hashCode(); + hash = 31 * hash + domain.hashCode(); + hash = 31 * hash + path.hashCode(); + if (priority != null) { + hash = 31 * hash + priority.hashCode(); + } + hash = 31 * hash + (int) (expiresAt ^ (expiresAt >>> 32)); + hash = 31 * hash + (secure ? 0 : 1); + hash = 31 * hash + (httpOnly ? 0 : 1); + hash = 31 * hash + (persistent ? 0 : 1); + hash = 31 * hash + (hostOnly ? 0 : 1); + return hash; + } + + /** + * Builds a cookie. The {@linkplain #name(String)} }, {@linkplain #value(String)}, + * and {@linkplain #domain(String)} values must all be set before calling {@link #build}. + */ + public static final class Builder { + + private String name; + private String value; + private long expiresAt = MAX_DATE; + private String domain; + private String path = "/"; + private boolean secure; + private boolean httpOnly; + private boolean persistent; + private boolean hostOnly; + private String priority; + + /** + * Set the cookie name, must not null. + * + * @param name Cookie name. + * @return The same builder. + */ + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + /** + * Set the cookie value, must not null. + * + * @param value Cookie value. + * @return The same builder. + */ + public Builder value(@NonNull String value) { + this.value = value; + return this; + } + + /** + * Set the cookie expires time. + * + * @param expiresAt When the cookie expired. + * @return The same builder. + */ + public Builder expiresAt(long expiresAt) { + if (expiresAt <= 0) { + expiresAt = Long.MIN_VALUE; + } + if (expiresAt > MAX_DATE) { + expiresAt = MAX_DATE; + } + this.expiresAt = expiresAt; + this.persistent = true; + return this; + } + + /** + * Set the domain pattern for this cookie. The cookie will match {@code domain} and all of + * its subdomains. + * + * @param domain The domain pattern. + * @return The same builder. + */ + public Builder domain(String domain) { + return domain(domain, false); + } + + /** + * Set the host-only domain for this cookie. The cookie will match {@code domain} but none + * of its subdomains. + * + * @param domain The host-only domain pattern. + * @return The same builder. + */ + public Builder hostOnlyDomain(String domain) { + return domain(domain, true); + } + + private Builder domain(String domain, boolean hostOnly) { + this.domain = domain; + this.hostOnly = hostOnly; + return this; + } + + /** + * Set the path this cookie, must start with '/'. + * + * @param path The path, must start with '/'. + * @return The same builder. + */ + public Builder path(String path) { + this.path = path; + return this; + } + + /** + * Set the cookie is secure. + * + * @return The same builder. + */ + public Builder secure() { + this.secure = true; + return this; + } + + /** + * Set the cookie is http only. + * + * @return The same builder. + */ + public Builder httpOnly() { + this.httpOnly = true; + return this; + } + + /** + * Set the priority for this cookie, it is a chromium extension. + * + * @param priority The priority, such as HIGH. + * @return The same builder. + */ + public Builder priority(String priority) { + this.priority = priority; + return this; + } + + /** + * Build a new cookie instance. + * + * @return A Cookie. + */ + public Cookie build() { + return new Cookie(this); + } + + } + + @Nullable + public static Cookie parseSetCookie(String host, String setCookie) { + int pos = 0; + int limit = setCookie.length(); + int cookiePairEnd = delimiterOffset(setCookie, pos, limit, ';'); + + int pairEqualsSign = delimiterOffset(setCookie, pos, cookiePairEnd, '='); + if (pairEqualsSign == cookiePairEnd) { + return null; + } + + String cookieName = trimSubstring(setCookie, pos, pairEqualsSign); + if (cookieName.isEmpty() || indexOfControlOrNonAscii(cookieName) != -1) { + return null; + } + + String cookieValue = trimSubstring(setCookie, pairEqualsSign + 1, cookiePairEnd); + if (indexOfControlOrNonAscii(cookieValue) != -1) { + return null; + } + + long expiresAt = MAX_DATE; + String domain = null; + String path = null; + long deltaSeconds = -1L; + boolean secureOnly = false; + boolean httpOnly = false; + boolean hostOnly = true; + boolean persistent = false; + String priority = null; + + pos = cookiePairEnd + 1; + while (pos < limit) { + int attributePairEnd = delimiterOffset(setCookie, pos, limit, ';'); + + int attributeEqualsSign = delimiterOffset(setCookie, pos, attributePairEnd, '='); + String attributeName = trimSubstring(setCookie, pos, attributeEqualsSign); + String attributeValue = attributeEqualsSign < attributePairEnd + ? trimSubstring(setCookie, attributeEqualsSign + 1, attributePairEnd) + : ""; + + if (attributeName.equalsIgnoreCase("expires")) { + try { + expiresAt = parseExpires(attributeValue, attributeValue.length()); + persistent = true; + } catch (IllegalArgumentException e) { + // Ignore this attribute, it isn't recognizable as a date. + } + } else if (attributeName.equalsIgnoreCase("max-age")) { + try { + deltaSeconds = parseMaxAge(attributeValue); + persistent = true; + } catch (NumberFormatException e) { + // Ignore this attribute, it isn't recognizable as a max age. + } + } else if (attributeName.equalsIgnoreCase("domain")) { + domain = attributeValue; + } else if (attributeName.equalsIgnoreCase("path")) { + path = attributeValue; + } else if (attributeName.equalsIgnoreCase("secure")) { + secureOnly = true; + } else if (attributeName.equalsIgnoreCase("httponly")) { + httpOnly = true; + } else if (attributeName.equalsIgnoreCase("priority")) { + priority = attributeValue; + } + + pos = attributePairEnd + 1; + } + + if (TextUtils.isEmpty(domain)) { + domain = host; + } + + // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two + // attributes are declared in the cookie string. + if (deltaSeconds == Long.MIN_VALUE) { + expiresAt = Long.MIN_VALUE; + } else if (deltaSeconds != -1L) { + long deltaMilliseconds = deltaSeconds <= (Long.MAX_VALUE / 1000) ? deltaSeconds * 1000 + : Long.MAX_VALUE; + long currentTimeMillis = System.currentTimeMillis(); + expiresAt = currentTimeMillis + deltaMilliseconds; + if (expiresAt > MAX_DATE) { + // Handle overflow. + expiresAt = MAX_DATE; + } + } + return new Cookie(cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly, + hostOnly, persistent, priority); + } + + private static int delimiterOffset(String input, int pos, int limit, char delimiter) { + for (int i = pos; i < limit; i++) { + if (input.charAt(i) == delimiter) { + return i; + } + } + return limit; + } + + private static String trimSubstring(String string, int pos, int limit) { + int start = skipLeadingAsciiWhitespace(string, pos, limit); + int end = skipTrailingAsciiWhitespace(string, start, limit); + return string.substring(start, end); + } + + private static int skipLeadingAsciiWhitespace(String input, int pos, int limit) { + for (int i = pos; i < limit; i++) { + switch (input.charAt(i)) { + case '\t': + case '\n': + case '\f': + case '\r': + case ' ': + continue; + default: + return i; + } + } + return limit; + } + + private static int skipTrailingAsciiWhitespace(String input, int pos, int limit) { + for (int i = limit - 1; i >= pos; i--) { + switch (input.charAt(i)) { + case '\t': + case '\n': + case '\f': + case '\r': + case ' ': + continue; + default: + return i + 1; + } + } + return pos; + } + + private static int indexOfControlOrNonAscii(String input) { + for (int i = 0, length = input.length(); i < length; i++) { + char c = input.charAt(i); + if (c <= '\u001f' || c >= '\u007f') { + return i; + } + } + return -1; + } + + private static long parseExpires(String s, int limit) { + int pos = dateCharacterOffset(s, 0, limit, false); + + int hour = -1; + int minute = -1; + int second = -1; + int dayOfMonth = -1; + int month = -1; + int year = -1; + Matcher matcher = TIME_PATTERN.matcher(s); + + while (pos < limit) { + int end = dateCharacterOffset(s, pos + 1, limit, true); + matcher.region(pos, end); + + if (hour == -1 && matcher.usePattern(TIME_PATTERN).matches()) { + hour = Integer.parseInt(matcher.group(1)); + minute = Integer.parseInt(matcher.group(2)); + second = Integer.parseInt(matcher.group(3)); + } else if (dayOfMonth == -1 && matcher.usePattern(DAY_OF_MONTH_PATTERN).matches()) { + dayOfMonth = Integer.parseInt(matcher.group(1)); + } else if (month == -1 && matcher.usePattern(MONTH_PATTERN).matches()) { + String monthString = matcher.group(1).toLowerCase(Locale.US); + month = MONTH_PATTERN.pattern().indexOf(monthString) / 4; // Sneaky! jan=1, dec=12. + } else if (year == -1 && matcher.usePattern(YEAR_PATTERN).matches()) { + year = Integer.parseInt(matcher.group(1)); + } + + pos = dateCharacterOffset(s, end + 1, limit, false); + } + + // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015. + if (year >= 70 && year <= 99) { + year += 1900; + } + if (year >= 0 && year <= 69) { + year += 2000; + } + + // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap + // seconds are not supported by this syntax. + if (year < 1601) { + throw new IllegalArgumentException(); + } + if (month == -1) { + throw new IllegalArgumentException(); + } + if (dayOfMonth < 1 || dayOfMonth > 31) { + throw new IllegalArgumentException(); + } + if (hour < 0 || hour > 23) { + throw new IllegalArgumentException(); + } + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException(); + } + if (second < 0 || second > 59) { + throw new IllegalArgumentException(); + } + + Calendar calendar = new GregorianCalendar(UTC); + calendar.setLenient(false); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTimeInMillis(); + } + + private static int dateCharacterOffset(String input, int pos, int limit, boolean invert) { + for (int i = pos; i < limit; i++) { + int c = input.charAt(i); + boolean dateCharacter = (c < ' ' && c != '\t') || (c >= '\u007f') + || (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c == ':'); + if (dateCharacter == !invert) { + return i; + } + } + return limit; + } + + private static long parseMaxAge(String s) { + try { + long parsed = Long.parseLong(s); + return parsed <= 0L ? Long.MIN_VALUE : parsed; + } catch (NumberFormatException e) { + // Check if the value is an integer (positive or negative) that's too big for a long. + if (s.matches("-?\\d+")) { + return s.startsWith("-") ? Long.MIN_VALUE : Long.MAX_VALUE; + } + throw e; + } + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java new file mode 100644 index 0000000..e4a9472 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpBody.java @@ -0,0 +1,27 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.stream.Stream; + +/** + * An abstract HTTP body type object, it declares that this object represents the HTTP body. + * + * @author Megatron King + * @since 2018-12-15 21:09 + */ +public abstract class HttpBody implements Stream { +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java new file mode 100644 index 0000000..7be08c0 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpHeaderPart.java @@ -0,0 +1,204 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.github.megatronking.netbare.utils.CaseInsensitiveLinkedMap; +import com.github.megatronking.netbare.stream.Stream; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * A base class of http header part, it contains the request/status line and headers. + * + * @author Megatron King + * @since 2018-12-11 23:30 + */ +/* package */ abstract class HttpHeaderPart implements Stream { + + private final HttpProtocol protocol; + private final Uri uri; + private final Map> headers; + + HttpHeaderPart(@NonNull HttpProtocol protocol, @NonNull Uri uri, + @NonNull Map> headers) { + this.protocol = protocol; + this.uri = uri; + this.headers = headers; + } + + /** + * Get the http request uri. + * + * @return A {@link Uri} object. + */ + @NonNull + public Uri uri() { + return this.uri; + } + + /** + * Get the http protocol. + * + * @return The http protocol. + */ + @NonNull + public HttpProtocol protocol() { + return this.protocol; + } + + /** + * Get a collection of http headers from the header part. + * + * @return A map collection. + */ + @NonNull + public Map> headers() { + return this.headers; + } + + /** + * Get a list of header values by name. + * + * @param name The name of a header. + * @return a list of header values. + */ + @Nullable + public List headers(@NonNull String name) { + return this.headers.get(name); + } + + /** + * Get the last value corresponding to the specified name, or null. + * + * @param name The name of a header. + * @return The value of the specified header. + */ + @Nullable + public String header(@NonNull String name) { + List headers = headers(name); + return headers != null ? headers.get(headers.size() - 1) : null; + } + + /** + * Create a new builder for this header, the new builder will do a shallow copy of this header + * part. + * + * @return A Builder to generate a header part instance. + */ + protected abstract Builder newBuilder(); + + static abstract class Builder { + + HttpProtocol protocol; + Uri uri; + Map> headers; + + Builder(HttpProtocol protocol, Uri uri, Map> headers) { + this.protocol = protocol; + this.uri = uri; + this.headers = headers; + } + + Builder(HttpHeaderPart httpHeader) { + this.protocol = httpHeader.protocol; + this.uri = httpHeader.uri; + this.headers = new CaseInsensitiveLinkedMap<>(httpHeader.headers); + } + + Builder removeHeader(@NonNull String name) { + this.headers.remove(name); + return this; + } + + Builder removeHeader(@NonNull String name, int index) { + List header = headers.get(name); + if (header != null) { + if (header.size() > index) { + header.remove(index); + } + } + if (header == null || header.isEmpty()) { + headers.remove(name); + } + return this; + } + + Builder replaceHeader(@NonNull String name, @NonNull String value) { + List header = headers.get(name); + if (header == null) { + header = new ArrayList<>(1); + headers.put(name, header); + } else { + header.clear(); + } + header.add(value); + return this; + } + + Builder updateHeader(@NonNull String name, @NonNull String value, int index) { + List header = headers.get(name); + if (header == null) { + header = new ArrayList<>(1); + headers.put(name, header); + } + if (header.size() > index) { + header.remove(index); + header.add(index, value); + } + return this; + } + + Builder updateOrAddHeader(@NonNull String name, @NonNull String value, int index) { + List header = headers.get(name); + if (header == null) { + header = new ArrayList<>(1); + headers.put(name, header); + } + if (header.size() > index) { + header.remove(index); + header.add(index, value); + } else { + header.add(value); + } + return this; + } + + Builder addHeader(@NonNull String name, @NonNull String value) { + List header = headers.get(name); + if (header == null) { + header = new ArrayList<>(1); + headers.put(name, header); + } + header.add(value); + return this; + } + + Builder removeHeaders() { + headers.clear(); + return this; + } + + abstract HttpHeaderPart build(); + + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java new file mode 100644 index 0000000..7f3f954 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpInjectInterceptor.java @@ -0,0 +1,136 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import android.net.Uri; +import android.os.Process; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.injector.HttpInjector; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A HTTP inject interceptor holds a {@link HttpInjector}. + * + * @author Megatron King + * @since 2018-12-29 22:25 + */ +public final class HttpInjectInterceptor extends HttpIndexInterceptor { + + private boolean mShouldInjectRequest; + private boolean mShouldInjectResponse; + + private HttpInjector mHttpInjector; + + private HttpInjectInterceptor(HttpInjector httpInjector) { + this.mHttpInjector = httpInjector; + } + + @Override + protected final void intercept(@NonNull final HttpRequestChain chain, + @NonNull ByteBuffer buffer, int index) throws IOException { + if (chain.request().uid() == Process.myUid()) { + chain.process(buffer); + return; + } + if (index == 0) { + mShouldInjectRequest = mHttpInjector.sniffRequest(chain.request()); + } + if (!mShouldInjectRequest) { + chain.process(buffer); + return; + } + if (index == 0) { + mHttpInjector.onRequestInject(buildHeader(chain.request()), new HttpRequestInjectorCallback(chain)); + } else { + mHttpInjector.onRequestInject(chain.request(), new HttpRawBody(buffer), new HttpRequestInjectorCallback(chain)); + } + } + + @Override + protected final void intercept(@NonNull final HttpResponseChain chain, + @NonNull ByteBuffer buffer, int index) throws IOException { + if (chain.response().uid() == Process.myUid()) { + chain.process(buffer); + return; + } + if (index == 0) { + mShouldInjectResponse = mHttpInjector.sniffResponse(chain.response()); + } + if (!mShouldInjectResponse) { + chain.process(buffer); + return; + } + if (index == 0) { + mHttpInjector.onResponseInject(buildHeader(chain.response()), + new HttpResponseInjectorCallback(chain)); + } else { + mHttpInjector.onResponseInject(chain.response(), new HttpRawBody(buffer), + new HttpResponseInjectorCallback(chain)); + } + } + + @Override + protected void onRequestFinished(@NonNull HttpRequest request) { + super.onRequestFinished(request); + if (mShouldInjectRequest) { + mHttpInjector.onRequestFinished(request); + } + mShouldInjectRequest = false; + } + + @Override + protected void onResponseFinished(@NonNull HttpResponse response) { + super.onResponseFinished(response); + if (mShouldInjectResponse) { + mHttpInjector.onResponseFinished(response); + } + mShouldInjectResponse = false; + } + + private HttpRequestHeaderPart buildHeader(HttpRequest request) { + return new HttpRequestHeaderPart.Builder(request.httpProtocol(), Uri.parse(request.url()), + request.requestHeaders(), request.method()) + .build(); + } + + private HttpResponseHeaderPart buildHeader(HttpResponse response) { + return new HttpResponseHeaderPart.Builder(response.httpProtocol(), Uri.parse(response.url()), + response.responseHeaders(), response.code(), response.message()) + .build(); + } + + /** + * A factory produces {@link HttpInjectInterceptor} instance. + * + * @param httpInjector A HTTP injector. + * @return An instance of {@link HttpInjectInterceptor}. + */ + public static HttpInterceptorFactory createFactory(final HttpInjector httpInjector) { + return new HttpInterceptorFactory() { + + @NonNull + @Override + public HttpInterceptor create() { + return new HttpInjectInterceptor(httpInjector); + } + + }; + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java new file mode 100644 index 0000000..f3fcb87 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRawBody.java @@ -0,0 +1,45 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * HTTP body object contains a raw buffer. + * + * @author Megatron King + * @since 2018-12-15 21:15 + */ +public class HttpRawBody extends HttpBody { + + private ByteBuffer mRawBuffer; + + public HttpRawBody(ByteBuffer buffer) { + ByteBuffer rawBuffer = ByteBuffer.allocate(buffer.remaining()); + rawBuffer.put(buffer.array(), buffer.position(), buffer.remaining()); + rawBuffer.flip(); + this.mRawBuffer = rawBuffer; + } + + @NonNull + @Override + public ByteBuffer toBuffer() { + return mRawBuffer; + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java new file mode 100644 index 0000000..afafc94 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestHeaderPart.java @@ -0,0 +1,233 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import android.net.Uri; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareUtils; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +/** + * The http request header part, it contains the request line and request headers. + * + * @author Megatron King + * @since 2018-12-11 23:59 + */ +public final class HttpRequestHeaderPart extends HttpHeaderPart { + + private final HttpMethod method; + + HttpRequestHeaderPart(HttpProtocol protocol, HttpMethod method, Uri uri, + Map> headers) { + super(protocol, uri, headers); + this.method = method; + } + + /** + * Get the request method from the header part. + * + * @return The request method. + */ + public HttpMethod method() { + return this.method; + } + + /** + * Get the request path from the header part. + * + * @return The request path. + */ + public String path() { + String path = uri().getPath(); + int pathIndex = uri().toString().indexOf(path); + return uri().toString().substring(pathIndex); + } + + @Override + public Builder newBuilder() { + return new Builder(this); + } + + /** + * Builder class for {@link HttpRequestHeaderPart}. + */ + public static class Builder extends HttpHeaderPart.Builder { + + private HttpMethod method; + + Builder(HttpProtocol protocol, Uri uri, Map> headers, + HttpMethod method) { + super(protocol, uri, headers); + this.method = method; + } + + Builder(HttpRequestHeaderPart requestHeader) { + super(requestHeader); + this.method = requestHeader.method; + } + + /** + * Set a new http request method. + * + * @param method The http request method. + * @return The same builder. + */ + public Builder method(@NonNull HttpMethod method) { + this.method = method; + return this; + } + + /** + * Set a new http request uri. + * + * @param uri The http request uri. + * @return The same builder. + */ + public Builder uri(@NonNull Uri uri) { + this.uri = uri; + return this; + } + + /** + * Add a http header. + * + * @param name The name of the header. + * @param value The value of the header. + * @return The same builder. + */ + @Override + public Builder addHeader(@NonNull String name, @NonNull String value) { + return (Builder) super.addHeader(name, value); + } + + /** + * Replace the http header by name. If the name has multiple values, the values will be all + * removed. + * + * @param name The name of the header. + * @param value The value of the header. + * @return The same builder. + */ + @Override + public Builder replaceHeader(@NonNull String name, @NonNull String value) { + return (Builder) super.replaceHeader(name, value); + } + + /** + * Update the http header by name and index. If the index of values not exists, then do + * nothing. + * + * @param name The name of the header. + * @param value The value of the header. + * @param index The value index in list. + * @return The same builder. + * + * @see #updateOrAddHeader(String, String, int) + */ + @Override + public Builder updateHeader(@NonNull String name, @NonNull String value, int index) { + return (Builder) super.updateHeader(name, value, index); + } + + /** + * Update the http header by name and index. If the index of values not exists, then append + * a new header. + * + * @param name The name of the header. + * @param value The value of the header. + * @param index The value index in list. + * @return The same builder. + * + * @see #updateHeader(String, String, int) + */ + @Override + public Builder updateOrAddHeader(@NonNull String name, @NonNull String value, int index) { + return (Builder) super.updateOrAddHeader(name, value, index); + } + + /** + * Remove the http header by name. If the name has multiple values, the values will be all + * removed. + * + * @param name The name of the header. + * @return The same builder. + */ + @Override + public Builder removeHeader(@NonNull String name) { + return (Builder) super.removeHeader(name); + } + + /** + * Remove the http header by name and index. If the index of values not exists, then do + * nothing. + * + * @param name The name of the header. + * @return The same builder. + */ + @Override + public Builder removeHeader(@NonNull String name, int index) { + return (Builder) super.removeHeader(name, index); + } + + /** + * Remove all http headers. + * + * @return The same builder. + */ + @Override + public Builder removeHeaders() { + return (Builder) super.removeHeaders(); + } + + /** + * Build a {@link HttpRequestHeaderPart} instance. + * + * @return The instance. + */ + @Override + public HttpRequestHeaderPart build() { + return new HttpRequestHeaderPart(protocol, method, uri, headers); + } + + } + + @NonNull + @Override + public ByteBuffer toBuffer() { + StringBuilder builder = new StringBuilder(); + builder.append(method.name()); + builder.append(" "); + builder.append(path()); + builder.append(" "); + builder.append(protocol().toString()); + builder.append(NetBareUtils.LINE_END); + for (Map.Entry> entry : headers().entrySet()) { + for (String value : entry.getValue()) { + builder.append(entry.getKey()); + builder.append(": "); + builder.append(value); + builder.append(NetBareUtils.LINE_END); + } + } + builder.append(NetBareUtils.LINE_END); + return ByteBuffer.wrap(builder.toString().getBytes()); + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java new file mode 100644 index 0000000..acec681 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpRequestInjectorCallback.java @@ -0,0 +1,59 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.stream.Stream; +import com.github.megatronking.netbare.injector.InjectorCallback; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.LinkedHashMap; + +/** + * A HTTP request packets injector callback. + * + * @author Megatron King + * @since 2018-12-15 21:55 + */ +public class HttpRequestInjectorCallback implements InjectorCallback { + + private HttpRequestChain mChain; + + /** + * Constructs a {@link InjectorCallback} for HTTP request. + * + * @param chain A {@link HttpInterceptor} chain. + */ + public HttpRequestInjectorCallback(HttpRequestChain chain) { + this.mChain = chain; + } + + @Override + public void onFinished(Stream stream) throws IOException { + ByteBuffer byteBuffer = stream.toBuffer(); + if (stream instanceof HttpRequestHeaderPart) { + HttpRequestHeaderPart header = (HttpRequestHeaderPart) stream; + HttpSession session = mChain.request().session(); + session.method = header.method(); + session.protocol = header.protocol(); + session.path = header.path(); + session.requestHeaders = new LinkedHashMap<>(header.headers()); + session.reqBodyOffset = byteBuffer.remaining(); + } + mChain.process(byteBuffer); + } + +} \ No newline at end of file diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java new file mode 100644 index 0000000..47cd257 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseHeaderPart.java @@ -0,0 +1,239 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import android.net.Uri; +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareUtils; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +/** + * The http response header part, it contains the status line and response headers. + * + * @author Megatron King + * @since 2018-12-11 23:43 + */ +public final class HttpResponseHeaderPart extends HttpHeaderPart { + + private final int code; + private final String message; + + HttpResponseHeaderPart(int code, String message, HttpProtocol protocol, Uri uri, + Map> headers) { + super(protocol, uri, headers); + this.code = code; + this.message = message; + } + + /** + * Get the response code from the header part. + * + * @return The response code. + */ + public int code() { + return this.code; + } + + /** + * Get the response message from the header part. + * + * @return The response message. + */ + public String message() { + return this.message; + } + + @Override + public Builder newBuilder() { + return new Builder(this); + } + + /** + * Builder class for {@link HttpResponseHeaderPart}. + */ + public static class Builder extends HttpHeaderPart.Builder { + + private int code; + private String message; + + Builder(HttpProtocol protocol, Uri uri, Map> headers, + int code, String message) { + super(protocol, uri, headers); + this.code = code; + this.message = message; + } + + Builder(HttpResponseHeaderPart responseHeader) { + super(responseHeader); + this.code = responseHeader.code; + this.message = responseHeader.message; + } + + /** + * Set a new http response code. + * + * @param code The http response code. + * @return The same builder. + */ + public Builder code(int code) { + this.code = code; + return this; + } + + /** + * Set a new http response message, the message should match with the response code. + * + * @param message The http response message. + * @return The same builder. + */ + public Builder message(@NonNull String message) { + if (message.contains(NetBareUtils.LINE_END)) { + throw new IllegalArgumentException("The message contains line end symbol."); + } + this.message = message; + return this; + } + + /** + * Add a http header. + * + * @param name The name of the header. + * @param value The value of the header. + * @return The same builder. + */ + @Override + public Builder addHeader(@NonNull String name, @NonNull String value) { + return (Builder) super.addHeader(name, value); + } + + /** + * Replace the http header by name. If the name has multiple values, the values will be all + * removed. + * + * @param name The name of the header. + * @param value The value of the header. + * @return The same builder. + */ + @Override + public Builder replaceHeader(@NonNull String name, @NonNull String value) { + return (Builder) super.replaceHeader(name, value); + } + + /** + * Update the http header by name and index. If the index of values not exists, then do + * nothing. + * + * @param name The name of the header. + * @param value The value of the header. + * @param index The value index in list. + * @return The same builder. + * + * @see #updateOrAddHeader(String, String, int) + */ + @Override + public Builder updateHeader(@NonNull String name, @NonNull String value, int index) { + return (Builder) super.updateHeader(name, value, index); + } + + /** + * Update the http header by name and index. If the index of values not exists, then append + * a new header. + * + * @param name The name of the header. + * @param value The value of the header. + * @param index The value index in list. + * @return The same builder. + * + * @see #updateHeader(String, String, int) + */ + @Override + public Builder updateOrAddHeader(@NonNull String name, @NonNull String value, int index) { + return (Builder) super.updateOrAddHeader(name, value, index); + } + + /** + * Remove the http header by name. If the name has multiple values, the values will be all + * removed. + * + * @param name The name of the header. + * @return The same builder. + */ + @Override + public Builder removeHeader(@NonNull String name) { + return (Builder) super.removeHeader(name); + } + + /** + * Remove the http header by name and index. If the index of values not exists, then do + * nothing. + * + * @param name The name of the header. + * @return The same builder. + */ + @Override + public Builder removeHeader(@NonNull String name, int index) { + return (Builder) super.removeHeader(name, index); + } + + /** + * Remove all http headers. + * + * @return The same builder. + */ + @Override + public Builder removeHeaders() { + return (Builder) super.removeHeaders(); + } + + /** + * Build a {@link HttpResponseHeaderPart} instance. + * + * @return The instance. + */ + @Override + public HttpResponseHeaderPart build() { + return new HttpResponseHeaderPart(code, message, protocol, uri, headers); + } + + } + + @NonNull + @Override + public ByteBuffer toBuffer() { + StringBuilder builder = new StringBuilder(); + builder.append(protocol().toString()); + builder.append(" "); + builder.append(code()); + builder.append(" "); + builder.append(message()); + builder.append(NetBareUtils.LINE_END); + for (Map.Entry> entry : headers().entrySet()) { + for (String value : entry.getValue()) { + builder.append(entry.getKey()); + builder.append(": "); + builder.append(value); + builder.append(NetBareUtils.LINE_END); + } + } + builder.append(NetBareUtils.LINE_END); + return ByteBuffer.wrap(builder.toString().getBytes()); + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java new file mode 100644 index 0000000..7001a65 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/http/HttpResponseInjectorCallback.java @@ -0,0 +1,58 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.http; + +import com.github.megatronking.netbare.stream.Stream; +import com.github.megatronking.netbare.injector.InjectorCallback; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.LinkedHashMap; + +/** + * A HTTP response packets injector callback. + * + * @author Megatron King + * @since 2018-12-15 21:55 + */ +public class HttpResponseInjectorCallback implements InjectorCallback { + + private HttpResponseChain mChain; + + /** + * Constructs a {@link InjectorCallback} for HTTP response. + * + * @param chain A {@link HttpInterceptor} chain. + */ + public HttpResponseInjectorCallback(HttpResponseChain chain) { + this.mChain = chain; + } + + @Override + public void onFinished(Stream stream) throws IOException { + ByteBuffer byteBuffer = stream.toBuffer(); + if (stream instanceof HttpResponseHeaderPart) { + HttpResponseHeaderPart header = (HttpResponseHeaderPart) stream; + HttpSession session = mChain.response().session(); + session.code = header.code(); + session.message = header.message(); + session.responseHeaders = new LinkedHashMap<>(header.headers()); + session.resBodyOffset = byteBuffer.remaining(); + } + mChain.process(byteBuffer); + } + +} \ No newline at end of file diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java new file mode 100644 index 0000000..bdadf25 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/BlockedHttpInjector.java @@ -0,0 +1,63 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.injector; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.http.HttpBody; +import com.github.megatronking.netbare.http.HttpRequest; +import com.github.megatronking.netbare.http.HttpRequestHeaderPart; +import com.github.megatronking.netbare.http.HttpResponse; +import com.github.megatronking.netbare.http.HttpResponseHeaderPart; + +/** + * An injector can block http request and response. Add your custom rules in + * {@link #sniffRequest(HttpRequest)} or {@link #sniffResponse(HttpResponse)} to filter the request. + * + * @author Megatron King + * @since 2019/1/2 20:50 + */ +public abstract class BlockedHttpInjector implements HttpInjector { + + @Override + public final void onRequestInject(@NonNull HttpRequestHeaderPart header, + @NonNull InjectorCallback callback) { + } + + @Override + public final void onResponseInject(@NonNull HttpResponseHeaderPart header, + @NonNull InjectorCallback callback) { + } + + @Override + public final void onRequestInject(@NonNull HttpRequest request, @NonNull HttpBody body, + @NonNull InjectorCallback callback) { + } + + @Override + public final void onResponseInject(@NonNull HttpResponse response, @NonNull HttpBody body, + @NonNull InjectorCallback callback) { + } + + @Override + public void onRequestFinished(@NonNull HttpRequest request) { + } + + @Override + public void onResponseFinished(@NonNull HttpResponse response) { + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java new file mode 100644 index 0000000..ef13bdd --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/HttpInjector.java @@ -0,0 +1,115 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.injector; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.http.HttpBody; +import com.github.megatronking.netbare.http.HttpRequest; +import com.github.megatronking.netbare.http.HttpRequestHeaderPart; +import com.github.megatronking.netbare.http.HttpResponse; +import com.github.megatronking.netbare.http.HttpResponseHeaderPart; +import com.github.megatronking.netbare.stream.Stream; + +import java.io.IOException; + +/** + * An injector for HTTP protocol packets, you can block and modify HTTP headers and bodies in this + * injector. + *

+ * Remember do not hold those methods for a long time. + *

+ * + * @author Megatron King + * @since 2018-12-15 21:50 + */ +public interface HttpInjector { + + /** + * Determine should the injector apply to this request. + * + * @param request Http request session. + * @return True if do injection to this request. + */ + boolean sniffRequest(@NonNull HttpRequest request); + + /** + * Determine should the injector apply to this response. + * + * @param response Http response session. + * @return True if do injection to this response. + */ + boolean sniffResponse(@NonNull HttpResponse response); + + /** + * Inject the http request header part, call {@link InjectorCallback#onFinished(Stream)} after + * the injection. + * + * @param header Http header part. + * @param callback A injection finish callback. + * @throws IOException If an I/O error has occurred. + */ + void onRequestInject(@NonNull HttpRequestHeaderPart header, @NonNull InjectorCallback callback) + throws IOException; + + /** + * Inject the http response header part, call {@link InjectorCallback#onFinished(Stream)} after + * the injection. + * + * @param header Http header part. + * @param callback A injection finish callback. + * @throws IOException If an I/O error has occurred. + */ + void onResponseInject(@NonNull HttpResponseHeaderPart header, @NonNull InjectorCallback callback) + throws IOException; + + /** + * Inject the http request body part, call {@link InjectorCallback#onFinished(Stream)} after + * the injection. + * + * @param body Http body part. + * @param callback A injection finish callback. + * @throws IOException If an I/O error has occurred. + */ + void onRequestInject(@NonNull HttpRequest request, @NonNull HttpBody body, + @NonNull InjectorCallback callback) throws IOException; + + /** + * Inject the http response body part, call {@link InjectorCallback#onFinished(Stream)} after + * the injection. + * + * @param body Http body part. + * @param callback A injection finish callback. + * @throws IOException If an I/O error has occurred. + */ + void onResponseInject(@NonNull HttpResponse response, @NonNull HttpBody body, + @NonNull InjectorCallback callback) throws IOException; + + /** + * Notify the injector that the request is finished. + * + * @param request Http request session. + */ + void onRequestFinished(@NonNull HttpRequest request); + + /** + * Notify the injector that the response is finished. + * + * @param response Http response session. + */ + void onResponseFinished(@NonNull HttpResponse response); + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java new file mode 100644 index 0000000..ec3d68c --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/InjectorCallback.java @@ -0,0 +1,38 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.injector; + +import com.github.megatronking.netbare.stream.Stream; + +import java.io.IOException; + +/** + * A callback invoked when the injector finished the injection. + * + * @author Megatron King + * @since 2018-12-15 21:55 + */ +public interface InjectorCallback { + + /** + * Invoked when the injector finished the injection. + * + * @param stream The injected stream. + * @throws IOException If an I/O error has occurred. + */ + void onFinished(Stream stream) throws IOException; + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java new file mode 100644 index 0000000..669e776 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/injector/SimpleHttpInjector.java @@ -0,0 +1,78 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.injector; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.http.HttpBody; +import com.github.megatronking.netbare.http.HttpRequest; +import com.github.megatronking.netbare.http.HttpRequestHeaderPart; +import com.github.megatronking.netbare.http.HttpResponse; +import com.github.megatronking.netbare.http.HttpResponseHeaderPart; + +import java.io.IOException; + +/** + * Convenience class for simple injectors and allows extending only methods that are necessary. + * + * @author Megatron King + * @since 2019/1/2 20:55 + */ +public abstract class SimpleHttpInjector implements HttpInjector { + + @Override + public boolean sniffRequest(@NonNull HttpRequest request) { + return false; + } + + @Override + public boolean sniffResponse(@NonNull HttpResponse response) { + return false; + } + + @Override + public void onRequestInject(@NonNull HttpRequestHeaderPart header, + @NonNull InjectorCallback callback) throws IOException { + callback.onFinished(header); + } + + @Override + public void onResponseInject(@NonNull HttpResponseHeaderPart header, + @NonNull InjectorCallback callback) throws IOException { + callback.onFinished(header); + } + + @Override + public void onRequestInject(@NonNull HttpRequest request, @NonNull HttpBody body, + @NonNull InjectorCallback callback) throws IOException { + callback.onFinished(body); + } + + @Override + public void onResponseInject(@NonNull HttpResponse response, @NonNull HttpBody body, + @NonNull InjectorCallback callback) throws IOException { + callback.onFinished(body); + } + + @Override + public void onRequestFinished(@NonNull HttpRequest request) { + } + + @Override + public void onResponseFinished(@NonNull HttpResponse response) { + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java new file mode 100644 index 0000000..cd85cc1 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/io/ByteBufferInputStream.java @@ -0,0 +1,39 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.io; + +import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; + +/** + * A {@link ByteArrayInputStream} constructs by a {@link ByteBuffer}. + * + * @author Megatron King + * @since 2018-12-23 15:24 + */ +public class ByteBufferInputStream extends ByteArrayInputStream { + + /** + * Constructs a new {@link ByteArrayInputStream} by a {@link ByteBuffer}. + * + * @param buffer A buffer has remaining data. + */ + public ByteBufferInputStream(ByteBuffer buffer) { + super(buffer.array(), buffer.position(), buffer.remaining()); + } + + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java new file mode 100644 index 0000000..8008c5e --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/io/HttpBodyInputStream.java @@ -0,0 +1,39 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.io; + +import com.github.megatronking.netbare.http.HttpBody; + +import java.io.ByteArrayInputStream; + +/** + * A {@link ByteArrayInputStream} constructs by a {@link HttpBody}. + * + * @author Megatron King + * @since 2018-12-23 15:24 + */ +public class HttpBodyInputStream extends ByteBufferInputStream { + + /** + * Constructs a new {@link ByteArrayInputStream} by a {@link HttpBody}. + * + * @param body A HTTP body stream. + */ + public HttpBodyInputStream(HttpBody body) { + super(body.toBuffer()); + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java new file mode 100644 index 0000000..7e29037 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/BufferStream.java @@ -0,0 +1,47 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.stream; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * A {@link ByteBuffer} stream data, the buffer will be output with nothing changes. + * + * @author Megatron King + * @since 2018-12-23 11:15 + */ +public class BufferStream implements Stream { + + private ByteBuffer mBuffer; + + /** + * Constructs a {@link ByteStream} by a {@link ByteBuffer}. + * + * @param buffer A byte buffer. + */ + public BufferStream(@NonNull ByteBuffer buffer) { + this.mBuffer = buffer; + } + + @NonNull + @Override + public ByteBuffer toBuffer() { + return mBuffer; + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java new file mode 100644 index 0000000..bfa1916 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/ByteStream.java @@ -0,0 +1,62 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.stream; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * A stream creates by a byte array. + * + * @author Megatron King + * @since 2018-12-23 11:15 + */ +public class ByteStream implements Stream { + + private final ByteBuffer mBuffer; + + /** + * Constructs a new stream by a byte array. + * + * @param bytes The bytes to be wrapped into {@link ByteBuffer}. + */ + public ByteStream(byte[] bytes) { + this(bytes, 0, bytes.length); + } + + /** + * Constructs a new stream by a byte array. + * + * @param bytes The bytes to be wrapped into {@link ByteBuffer}. + * @param offset The index of the first byte in array. + * @param length The number of bytes. + */ + public ByteStream(byte[] bytes, int offset, int length) { + // Do not use wrap. + ByteBuffer buffer = ByteBuffer.allocate(length); + buffer.put(bytes, offset, length); + buffer.flip(); + this.mBuffer = buffer; + } + + @NonNull + @Override + public ByteBuffer toBuffer() { + return mBuffer; + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java new file mode 100644 index 0000000..97c9a60 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/Stream.java @@ -0,0 +1,38 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.stream; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * Stream is special data type using in NetBare. + * + * @author Megatron King + * @since 2018-12-15 21:05 + */ +public interface Stream { + + /** + * Converts the stream data type to {@link ByteBuffer}. + * + * @return A {@link ByteBuffer}. + */ + @NonNull + ByteBuffer toBuffer(); + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java new file mode 100644 index 0000000..6e07e84 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/StringStream.java @@ -0,0 +1,39 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.stream; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * A stream creates by a string. + * + * @author Megatron King + * @since 2018-12-23 11:30 + */ +public class StringStream extends ByteStream { + + /** + * Constructs a new stream by a string. + * + * @param string The string to be wrapped into {@link ByteBuffer}. + */ + public StringStream(@NonNull String string) { + super(string.getBytes()); + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java new file mode 100644 index 0000000..fa984b8 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/stream/TinyFileStream.java @@ -0,0 +1,78 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.stream; + +import androidx.annotation.NonNull; + +import com.github.megatronking.netbare.NetBareUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * A tiny file stream, the file will be loaded into memory, the file size should be less than + * {@link #MAX_FILE_LENGTH}. + * + * @author Megatron King + * @since 2018-12-23 11:39 + */ +public class TinyFileStream implements Stream { + + /** + * The max file size of this stream supports. + */ + public static final int MAX_FILE_LENGTH = 64 * 1024; + + private final File mFile; + + /** + * Constructs a stream by a tiny file. + * + * @param file A tiny file. + * @throws IOException + */ + public TinyFileStream(@NonNull File file) throws IOException { + if (file.length() > MAX_FILE_LENGTH) { + throw new IOException("Only support file size < " + MAX_FILE_LENGTH); + } + this.mFile = file; + } + + @NonNull + @Override + public ByteBuffer toBuffer() { + ByteBuffer byteBuffer; + FileInputStream fis = null; + FileChannel channel = null; + try { + fis = new FileInputStream(mFile); + channel = fis.getChannel(); + byteBuffer = ByteBuffer.allocate(fis.available()); + channel.read(byteBuffer); + byteBuffer.flip(); + } catch (IOException e) { + byteBuffer = ByteBuffer.allocate(0); + } finally { + NetBareUtils.closeQuietly(channel); + NetBareUtils.closeQuietly(fis); + } + return byteBuffer; + } + +} diff --git a/netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java b/netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java new file mode 100644 index 0000000..7935711 --- /dev/null +++ b/netbare-injector/src/main/java/com/github/megatronking/netbare/utils/CaseInsensitiveLinkedMap.java @@ -0,0 +1,362 @@ +/* NetBare - An android network capture and injection library. + * Copyright (C) 2018-2019 Megatron King + * Copyright (C) 2018-2019 GuoShi + * + * NetBare is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * NetBare is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with NetBare. + * If not, see . + */ +package com.github.megatronking.netbare.utils; + +import androidx.annotation.NonNull; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * {@link CaseInsensitiveLinkedMap} is a {@link LinkedHashMap} from String keys to + * values which is case-insensitive and case-preserving with respect to the keys in the map. + * + * This class is not necessarily thread safe. + * + * @author Megatron King + * @since 2018-12-21 23:55 + */ +public class CaseInsensitiveLinkedMap extends AbstractMap { + + private final Map map; + + private static final class KeySet extends AbstractSet { + + private static final class KeySetIterator implements Iterator { + + private Iterator iterator; + + private KeySetIterator(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public String next() { + return this.iterator.next().toString(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + } + + private final Set keySet; + + private KeySet(Set keySet) { + this.keySet = keySet; + } + + /** + * Not supported for sets returned by Map.keySet. + */ + @Override + public boolean add(String o) { + throw new UnsupportedOperationException("Map.keySet must return a Set which does not support add"); + } + + /** + * Not supported for sets returned by Map.keySet. + */ + @Override + public boolean addAll(@NonNull Collection c) { + throw new UnsupportedOperationException("Map.keySet must return a Set which does not support addAll"); + } + + @Override + public void clear() { + this.keySet.clear(); + } + + @Override + public boolean contains(Object o) { + return o instanceof String && this.keySet.contains(CaseInsensitiveKey.objectToKey(o)); + } + + @Override + public Iterator iterator() { + return new KeySetIterator(this.keySet.iterator()); + } + + @Override + public boolean remove(Object o) { + // The following can throw ClassCastException which conforms to the method specification. + return this.keySet.remove(CaseInsensitiveKey.objectToKey(o)); + } + + @Override + public int size() { + return this.keySet.size(); + } + + } + + private static final class EntrySet extends AbstractSet> { + + private static final class MapEntry implements Entry { + + private final Entry entry; + + private MapEntry(Entry entry) { + this.entry = entry; + } + + @Override + public String getKey() { + return this.entry.getKey().toString(); + } + + @Override + public V getValue() { + return this.entry.getValue(); + } + + @Override + public V setValue(V value) { + return this.entry.setValue(value); + } + + private Entry getEntry() { + return this.entry; + } + } + + private static final class EntrySetIterator implements Iterator> { + + private final Iterator> iterator; + + private EntrySetIterator(Iterator> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public Entry next() { + return new MapEntry<>(this.iterator.next()); + } + + @Override + public void remove() { + this.iterator.remove(); + } + + } + + private final Set> entrySet; + + private final CaseInsensitiveLinkedMap map; + + private EntrySet(Set> entrySet, CaseInsensitiveLinkedMap map) { + this.entrySet = entrySet; + this.map = map; + } + + /** + * Not supported for sets returned by Map.entrySet. + */ + @Override + public boolean add(Entry o) { + throw new UnsupportedOperationException("Map.entrySet must return a Set which does not support add"); + } + + /** + * Not supported for sets returned by Map.entrySet. + */ + @Override + public boolean addAll(@NonNull Collection> c) { + throw new UnsupportedOperationException("Map.entrySet must return a Set which does not support addAll"); + } + + @Override + public void clear() { + this.entrySet.clear(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean contains(Object o) { + if (o instanceof Entry) { + Entry e = (Entry) o; + V value = this.map.get(e.getKey()); + return value.equals(e.getValue()); + } + return false; + } + + @Override + public Iterator> iterator() { + return new EntrySetIterator<>(this.entrySet.iterator()); + } + + @SuppressWarnings("unchecked") + @Override + public boolean remove(Object o) { + return this.entrySet.remove(((MapEntry) o).getEntry()); + } + + @Override + public int size() { + return this.entrySet.size(); + } + + } + + static final class CaseInsensitiveKey { + + private final String key; + + private CaseInsensitiveKey(String key) { + this.key = key; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + key.toLowerCase().hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CaseInsensitiveKey other = (CaseInsensitiveKey) obj; + return key == null ? other.key == null : key.equalsIgnoreCase(other.key); + } + + @Override + public String toString() { + return key; + } + + /** + * Convert the given key Object to a {@link CaseInsensitiveKey}. + *

+ * Pre-condition: key instanceof String + * + * @param key the key to be converted + * @return the CaseInsensitiveKey corresponding to the given key + */ + public static CaseInsensitiveKey objectToKey(Object key) { + return new CaseInsensitiveKey((String) key); + } + + } + + public CaseInsensitiveLinkedMap(int initialCapacity) { + this.map = new LinkedHashMap<>(initialCapacity); + } + + public CaseInsensitiveLinkedMap() { + this.map = new LinkedHashMap<>(); + } + + @SuppressWarnings("unchecked") + public CaseInsensitiveLinkedMap(Map map) { + this.map = new LinkedHashMap<>(map.size()); + if (map instanceof CaseInsensitiveLinkedMap) { + this.map.putAll(((CaseInsensitiveLinkedMap) map).map); + } else { + for (Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + } + + public CaseInsensitiveLinkedMap(CaseInsensitiveLinkedMap map) { + this.map = new LinkedHashMap<>(map.size()); + this.map.putAll(map.map); + } + + @Override + public void clear() { + this.map.clear(); + } + + @Override + public boolean containsKey(Object key) { + return key instanceof String && this.map.containsKey(CaseInsensitiveKey.objectToKey(key)); + } + + @Override + public boolean containsValue(Object value) { + return this.map.containsValue(value); + } + + @Override + @NonNull + public Set> entrySet() { + return new EntrySet<>(this.map.entrySet(), this); + } + + @Override + public V get(Object key) { + return key instanceof String ? this.map.get(CaseInsensitiveKey.objectToKey(key)) : null; + } + + @Override + @NonNull + public Set keySet() { + return new KeySet(this.map.keySet()); + } + + @Override + public V put(String key, V value) { + if (key == null) { + throw new NullPointerException("CaseInsensitiveLinkedMap does not permit null keys"); + } + return this.map.put(CaseInsensitiveKey.objectToKey(key), value); + } + + @Override + public V remove(Object key) { + return key instanceof String ? this.map.remove(CaseInsensitiveKey.objectToKey(key)) : null; + } + + @Override + public int size() { + return this.map.size(); + } + + @Override + @NonNull + public Collection values() { + return this.map.values(); + } + +} diff --git a/netbare-sample/build.gradle b/netbare-sample/build.gradle new file mode 100644 index 0000000..26d99d8 --- /dev/null +++ b/netbare-sample/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.github.megatronking.netbare.sample" + minSdkVersion 21 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.11' + + implementation 'com.google.code.gson:gson:2.8.2' + implementation project(':basewidgetimport') + +} +repositories { + mavenCentral() +} diff --git a/netbare-sample/proguard-rules.pro b/netbare-sample/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/netbare-sample/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/netbare-sample/src/androidTest/java/com/github/megatronking/netbare/ExampleInstrumentedTest.java b/netbare-sample/src/androidTest/java/com/github/megatronking/netbare/ExampleInstrumentedTest.java new file mode 100644 index 0000000..bdab28e --- /dev/null +++ b/netbare-sample/src/androidTest/java/com/github/megatronking/netbare/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.github.megatronking.netbare; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.github.megatronking.netbare", appContext.getPackageName()); + } +} diff --git a/netbare-sample/src/main/AndroidManifest.xml b/netbare-sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8f56281 --- /dev/null +++ b/netbare-sample/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt new file mode 100644 index 0000000..b5dd397 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/App.kt @@ -0,0 +1,28 @@ +package com.github.megatronking.netbare.sample + +import android.app.Application +import com.github.megatronking.netbare.NetBare +import com.github.megatronking.netbare.ssl.JKS +import com.wrbug.developerhelper.basewidgetimport.BaseWidget + +class App : Application() { + + companion object { + + private lateinit var sInstance: App + + fun getInstance(): App { + return sInstance + } + } + + override fun onCreate() { + super.onCreate() + sInstance = this + // 创建自签证书 + BaseWidget.init(this) + } + + + +} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt new file mode 100644 index 0000000..99cd666 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/AppService.kt @@ -0,0 +1,54 @@ +package com.github.megatronking.netbare.sample + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.os.Build +import androidx.core.app.NotificationCompat +import com.github.megatronking.netbare.NetBareService + +class AppService : NetBareService() { + + companion object { + + private const val CHANNEL_ID = "com.github.megatronking.netbare.sample.NOTIFICATION_CHANNEL_ID" + + } + + override fun onCreate() { + super.onCreate() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) { + notificationManager.createNotificationChannel(NotificationChannel(CHANNEL_ID, + getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW)) + } + } + } + + override fun notificationId(): Int { + return 100 + } + + override fun createNotification(): Notification { + val intent = Intent(this, MainActivity::class.java) + intent.addCategory(Intent.CATEGORY_LAUNCHER) + intent.action = Intent.ACTION_MAIN + val pendingIntent = PendingIntent.getActivity(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT) + return NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.app_name)) + .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)) + .setOngoing(true) + .setContentIntent(pendingIntent) + .build() + } + +} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt new file mode 100644 index 0000000..9db4969 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/BaiduLogoInjector.kt @@ -0,0 +1,55 @@ +package com.github.megatronking.netbare.sample + +import android.util.Log +import com.github.megatronking.netbare.http.HttpBody +import com.github.megatronking.netbare.http.HttpResponse +import com.github.megatronking.netbare.http.HttpResponseHeaderPart +import com.github.megatronking.netbare.injector.InjectorCallback +import com.github.megatronking.netbare.injector.SimpleHttpInjector +import com.github.megatronking.netbare.stream.BufferStream +import java.nio.ByteBuffer + +/** + * 注入器范例1:替换百度首页logo + * + * 启动NetBare服务后,用浏览器App打开百度首页,Logo将会被替换成此sample项目raw目录下的图片。 + * 注意:如果浏览器有图片缓存,记得先把缓存清理掉! + * + * @author Megatron King + * @since 2018/12/30 00:18 + */ +class BaiduLogoInjector : SimpleHttpInjector() { + + companion object { + const val TAG = "BaiduLogoInjector" + } + + override fun sniffResponse(response: HttpResponse): Boolean { + // 请求url匹配时才进行注入 + val shouldInject = "https://m.baidu.com/static/index/plus/plus_logo.png".equals( + response.url()) + if (shouldInject) { + Log.i(TAG, "Start Miss. Du logo injection!") + } + return shouldInject + } + + override fun onResponseInject(header: HttpResponseHeaderPart, callback: InjectorCallback) { + // 响应体大小变化,一定要先更新header中的content-length + val newHeader = header.newBuilder() + .replaceHeader("content-length", "10764") + .build() + callback.onFinished(newHeader) + Log.i(TAG, "Inject header completed!") + } + + override fun onResponseInject(response: HttpResponse, body: HttpBody, callback: InjectorCallback) { + // 替换图片请求响应体 + val injectIOStream = App.getInstance().resources.openRawResource(R.raw.baidu_inject_logo) + val injectStream = BufferStream(ByteBuffer.wrap(injectIOStream.readBytes())) + injectIOStream.close() + callback.onFinished(injectStream) + Log.i(TAG, "Inject body completed!") + } + +} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt new file mode 100644 index 0000000..699ce30 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/HttpUrlPrintInterceptor.kt @@ -0,0 +1,43 @@ +package com.github.megatronking.netbare.sample + +import android.util.Log +import com.github.megatronking.netbare.http.* +import java.nio.ByteBuffer +import java.nio.CharBuffer +import java.nio.charset.Charset +import java.nio.charset.CharsetDecoder + +/** + * 拦截器反例1:打印请求url + * + * @author Megatron King + * @since 2019/1/2 22:05 + */ +class HttpUrlPrintInterceptor : HttpIndexInterceptor() { + + companion object { + const val TAG = "URL" + + fun createFactory(): HttpInterceptorFactory { + return HttpInterceptorFactory { HttpUrlPrintInterceptor() } + } + } + + override fun intercept(chain: HttpRequestChain, buffer: ByteBuffer, index: Int) { + if (index == 0) { + // 一个请求可能会有多个数据包,故此方法会多次触发,这里只在收到第一个包的时候打印 + Log.i(TAG, "Request: " + chain.request().url()) + } + // 调用process将数据发射给下一个拦截器,否则数据将不会发给服务器 + chain.process(buffer) + } + + override fun intercept(chain: HttpResponseChain, buffer: ByteBuffer, index: Int) { + chain.process(buffer) + } + + override fun onResponseFinished(response: HttpResponse) { + super.onResponseFinished(response) + } + +} diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt new file mode 100644 index 0000000..5b439a3 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/MainActivity.kt @@ -0,0 +1,107 @@ +package com.github.megatronking.netbare.sample + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity +import com.github.megatronking.netbare.NetBare +import com.github.megatronking.netbare.NetBareConfig +import com.github.megatronking.netbare.NetBareListener +import com.github.megatronking.netbare.http.HttpInjectInterceptor +import com.github.megatronking.netbare.http.HttpInterceptorFactory +import com.github.megatronking.netbare.http.HttpVirtualGatewayFactory +import com.github.megatronking.netbare.ssl.JKS +import java.io.IOException + +class MainActivity : AppCompatActivity(), NetBareListener { + + companion object { + private const val REQUEST_CODE_PREPARE = 1 + } + + private lateinit var mNetBare: NetBare + + private lateinit var mActionButton: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + mNetBare = NetBare.get() + + mActionButton = findViewById(R.id.action) + mActionButton.setOnClickListener { + if (mNetBare.isActive) { + mNetBare.stop() + } else { + prepareNetBare() + } + } + + // 监听NetBare服务的启动和停止 + mNetBare.registerNetBareListener(this) + } + + override fun onDestroy() { + super.onDestroy() + mNetBare.unregisterNetBareListener(this) + mNetBare.stop() + } + + override fun onServiceStarted() { + mActionButton.setText(R.string.stop_netbare) + } + + override fun onServiceStopped() { + mActionButton.setText(R.string.start_netbare) + mActionButton.invalidate() + } + + private fun prepareNetBare() { + // 安装自签证书 + if (!JKS.isInstalled(this, JKS.getJskAlias())) { + try { + JKS.install(this, JKS.getJskAlias(), JKS.getJskAlias()) + } catch (e: IOException) { + // 安装失败 + } + return + } + // 配置VPN + val intent = NetBare.get().prepare() + if (intent != null) { + startActivityForResult(intent, REQUEST_CODE_PREPARE) + return + } + // 启动NetBare服务 + mNetBare.start( + NetBareConfig.defaultHttpConfig( + JKS.getJks(), + interceptorFactories() + ) + ) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_PREPARE) { + prepareNetBare() + } + } + + private fun interceptorFactories(): List { + // 拦截器范例1:打印请求url + val interceptor1 = HttpUrlPrintInterceptor.createFactory() + // 注入器范例1:替换百度首页logo + val injector1 = HttpInjectInterceptor.createFactory(BaiduLogoInjector()) + // 注入器范例2:修改发朋友圈定位 + val injector2 = HttpInjectInterceptor.createFactory(WechatLocationInjector()) + val injector3 = HttpInjectInterceptor.createFactory(PresellLocationInjector()) + // 可以添加其它的拦截器,注入器 + // ... + return listOf(interceptor1, injector1, injector2, injector3) + } + + +} diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt new file mode 100644 index 0000000..0c15243 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/PresellLocationInjector.kt @@ -0,0 +1,115 @@ +package com.github.megatronking.netbare.sample + +import android.util.Log +import com.github.megatronking.netbare.NetBareUtils +import com.github.megatronking.netbare.http.HttpBody +import com.github.megatronking.netbare.http.HttpResponse +import com.github.megatronking.netbare.http.HttpResponseHeaderPart +import com.github.megatronking.netbare.injector.InjectorCallback +import com.github.megatronking.netbare.injector.SimpleHttpInjector +import com.github.megatronking.netbare.io.HttpBodyInputStream +import com.github.megatronking.netbare.stream.ByteStream +import com.google.gson.JsonParser +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStreamReader +import java.io.Reader +import java.nio.ByteBuffer +import java.nio.CharBuffer +import java.nio.charset.Charset +import java.nio.charset.CharsetDecoder +import java.util.* +import java.util.zip.DeflaterOutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.InflaterInputStream + +/** + * 注入器范例2:修改发朋友圈定位 + * + * 启动NetBare服务后,打开朋友圈发状态->所在位置,会发现POI都变成被修改地点附近的。 + * + * @author Sundy + * @since 2019/1/2 22:17 + */ +class PresellLocationInjector : SimpleHttpInjector() { + + companion object { + const val TAG = "WechatLocationInjector" + } + + private var mHoldResponseHeader: HttpResponseHeaderPart? = null + + override fun sniffResponse(response: HttpResponse): Boolean { + // 请求url匹配时才进行注入 + val shouldInject = response.url().startsWith("http://boss-apk.10.1.133.14.xip.io/") + if (shouldInject) { + Log.i(TAG, "Start wechat location injection!") + } + return shouldInject + } + + override fun onResponseInject(header: HttpResponseHeaderPart, callback: InjectorCallback) { + // 由于响应体大小不确定,这里先hold住header(需要在后面修改content-length) + mHoldResponseHeader = header + } + + override fun onResponseInject(response: HttpResponse, body: HttpBody, callback: InjectorCallback) { + if (mHoldResponseHeader == null) { + // 一般不会发生 + return + } + var his: HttpBodyInputStream? = null + try { +// his = HttpBodyInputStream(body) +// var bytes: ByteArray = ByteArray(his.available()) +// his.read(bytes) +// val str = String(bytes) + val str = String(uncompress(body.toBuffer().array())!!) + Log.i(TAG, str) + } finally { + NetBareUtils.closeQuietly(his) + } + mHoldResponseHeader = null + super.onResponseInject(response, body, callback) + } + + fun getString(buffer: ByteBuffer?): String { + var charset: Charset? = null + var decoder: CharsetDecoder? = null + var charBuffer: CharBuffer? = null + try { + charset = Charset.forName("UTF-8") + decoder = charset!!.newDecoder() + // charBuffer = decoder.decode(buffer);//用这个的话,只能输出来一次结果,第二次显示为空 + charBuffer = decoder!!.decode(buffer?.asReadOnlyBuffer()) + return charBuffer!!.toString() + } catch (ex: Exception) { + ex.printStackTrace() + return "" + } + + } + + fun uncompress(bytes: ByteArray?): ByteArray? { + if (bytes == null || bytes.isEmpty()) { + return null + } + val out = ByteArrayOutputStream() + val `in` = ByteArrayInputStream(bytes) + try { + val ungzip = GZIPInputStream(`in`) + val buffer = ByteArray(256) + var n: Int = ungzip.read(buffer) + while (n >= 0) { + out.write(buffer, 0, n) + n = ungzip.read(buffer) + } + } catch (e: Exception) { + e.printStackTrace() + } + + return out.toByteArray() + } + + +} diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt new file mode 100644 index 0000000..a5ba225 --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/TestHttpIntercepter.kt @@ -0,0 +1,32 @@ +package com.github.megatronking.netbare.sample + +import com.github.megatronking.netbare.http.* +import java.nio.ByteBuffer + +class TestHttpIntercepter : HttpInterceptor() { + + override fun intercept(chain: HttpRequestChain, buffer: ByteBuffer) { + // 对Http请求包进行自定义处理 + // 将Http请求发射出去,交给下个拦截器或者发给服务器 + chain.process(buffer) + } + + override fun intercept(chain: HttpResponseChain, buffer: ByteBuffer) { + // 对Http响应包进行自定义处理 + // 将Http响应发射出去,交给下个拦截器或者发给客户端 + chain.process(buffer) + } + + override fun onRequestFinished(request: HttpRequest) { + // Http请求包已全部发送完成 + } + + override fun onResponseFinished(response: HttpResponse) { + val url = response.url() + if (url==null) { + + } + // Http响应包已全部发送完成 + } + +} \ No newline at end of file diff --git a/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt new file mode 100644 index 0000000..14924ff --- /dev/null +++ b/netbare-sample/src/main/kotlin/com/github/megatronking/netbare/sample/WechatLocationInjector.kt @@ -0,0 +1,101 @@ +package com.github.megatronking.netbare.sample + +import android.util.Log +import com.github.megatronking.netbare.NetBareUtils +import com.github.megatronking.netbare.http.HttpBody +import com.github.megatronking.netbare.http.HttpResponse +import com.github.megatronking.netbare.http.HttpResponseHeaderPart +import com.github.megatronking.netbare.injector.InjectorCallback +import com.github.megatronking.netbare.injector.SimpleHttpInjector +import com.github.megatronking.netbare.io.HttpBodyInputStream +import com.github.megatronking.netbare.stream.ByteStream +import com.google.gson.JsonParser +import java.io.ByteArrayOutputStream +import java.io.InputStreamReader +import java.io.Reader +import java.util.zip.DeflaterOutputStream +import java.util.zip.InflaterInputStream + +/** + * 注入器范例2:修改发朋友圈定位 + * + * 启动NetBare服务后,打开朋友圈发状态->所在位置,会发现POI都变成被修改地点附近的。 + * + * @author Megatron King + * @since 2019/1/2 22:17 + */ +class WechatLocationInjector : SimpleHttpInjector() { + + companion object { + const val TAG = "WechatLocationInjector" + } + + private var mHoldResponseHeader: HttpResponseHeaderPart? = null + + override fun sniffResponse(response: HttpResponse): Boolean { + // 请求url匹配时才进行注入 + val shouldInject = response.url().startsWith("https://lbs.map.qq.com/loc") + if (shouldInject) { + Log.i(TAG, "Start wechat location injection!") + } + return shouldInject + } + + override fun onResponseInject(header: HttpResponseHeaderPart, callback: InjectorCallback) { + // 由于响应体大小不确定,这里先hold住header(需要在后面修改content-length) + mHoldResponseHeader = header + } + + override fun onResponseInject(response: HttpResponse, body: HttpBody, callback: InjectorCallback) { + if (mHoldResponseHeader == null) { + // 一般不会发生 + return + } + var his: HttpBodyInputStream? = null + var reader: Reader? = null + var fos: DeflaterOutputStream? = null + var bos: ByteArrayOutputStream? = null + try { + his = HttpBodyInputStream(body) + // 数据使用了zlib编码,这里需要先解码 + reader = InputStreamReader(InflaterInputStream(his)) + val element = JsonParser().parse(reader) + if (element == null || !element.isJsonObject) { + return + } + val locationNode = element.asJsonObject.get("location") + if (locationNode == null || !locationNode.isJsonObject) { + return + } + // 替换经纬度,这里是珠峰的经纬度,装逼很厉害的地方 + val location = locationNode.asJsonObject + location.addProperty("latitude", 27.99136f) + location.addProperty("longitude", 86.88915f) + val injectedBody = element.toString() + // 重新使用zlib编码 + bos = ByteArrayOutputStream() + fos = DeflaterOutputStream(bos) + fos.write(injectedBody.toByteArray()) + fos.finish() + fos.flush() + val injectBodyData = bos.toByteArray() + // 更新header的content-length + val injectHeader = mHoldResponseHeader!! + .newBuilder() + .replaceHeader("Content-Length", injectBodyData.size.toString()) + .build() + // 先将header发射出去 + callback.onFinished(injectHeader) + // 再将响应体发射出去 + callback.onFinished(ByteStream(injectBodyData)) + Log.i(TAG, "Inject wechat location completed!") + } finally { + NetBareUtils.closeQuietly(his) + NetBareUtils.closeQuietly(reader) + NetBareUtils.closeQuietly(fos) + NetBareUtils.closeQuietly(bos) + } + mHoldResponseHeader = null + } + +} diff --git a/netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/netbare-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/netbare-sample/src/main/res/drawable/ic_launcher_background.xml b/netbare-sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/netbare-sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/netbare-sample/src/main/res/layout/activity_main.xml b/netbare-sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a0ec0ea --- /dev/null +++ b/netbare-sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + +