Files
2017-01-24 22:59:26 +08:00

166 lines
7.6 KiB
Scala

package com.github.shadowsocks.plugin
import java.io.{File, FileNotFoundException, FileOutputStream, IOException}
import android.content.pm.{PackageManager, Signature}
import android.content.{BroadcastReceiver, ContentResolver, Intent}
import android.net.Uri
import android.os.Bundle
import android.util.{Base64, Log}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.CloseUtils.autoClose
import com.github.shadowsocks.utils.{Commandline, IOUtils}
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer
/**
* @author Mygod
*/
object PluginManager {
/**
* Trusted signatures by the app. Third-party fork should add their public key to their fork if the developer wishes
* to publish or has published plugins for this app. You can obtain your public key by executing:
*
* $ keytool -export -alias key-alias -keystore /path/to/keystore.jks -rfc
*
* If you don't plan to publish any plugin but is developing/has developed some, it's not necessary to add your
* public key yet since it will also automatically trust packages signed by the same signatures, e.g. debug keys.
*/
lazy val trustedSignatures: Set[Signature] =
app.getPackageManager.getPackageInfo(app.getPackageName, PackageManager.GET_SIGNATURES).signatures.toSet +
new Signature(Base64.decode( // @Mygod
"""
|MIIDWzCCAkOgAwIBAgIEUzfv8DANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJD
|TjEOMAwGA1UECBMFTXlnb2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdv
|ZDEOMAwGA1UECxMFTXlnb2QxDjAMBgNVBAMTBU15Z29kMCAXDTE0MDUwMjA5MjQx
|OVoYDzMwMTMwOTAyMDkyNDE5WjBdMQswCQYDVQQGEwJDTjEOMAwGA1UECBMFTXln
|b2QxDjAMBgNVBAcTBU15Z29kMQ4wDAYDVQQKEwVNeWdvZDEOMAwGA1UECxMFTXln
|b2QxDjAMBgNVBAMTBU15Z29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|AQEAjm5ikHoP3w6zavvZU5bRo6Birz41JL/nZidpdww21q/G9APA+IiJMUeeocy0
|L7/QY8MQZABVwNq79LXYWJBcmmFXM9xBPgDqQP4uh9JsvazCI9bvDiMn92mz9HiS
|Sg9V4KGg0AcY0r230KIFo7hz+2QBp1gwAAE97myBfA3pi3IzJM2kWsh4LWkKQMfL
|M6KDhpb4mdDQnHlgi4JWe3SYbLtpB6whnTqjHaOzvyiLspx1tmrb0KVxssry9KoX
|YQzl56scfE/QJX0jJ5qYmNAYRCb4PibMuNSGB2NObDabSOMAdT4JLueOcHZ/x9tw
|agGQ9UdymVZYzf8uqc+29ppKdQIDAQABoyEwHzAdBgNVHQ4EFgQUBK4uJ0cqmnho
|6I72VmOVQMvVCXowDQYJKoZIhvcNAQELBQADggEBABZQ3yNESQdgNJg+NRIcpF9l
|YSKZvrBZ51gyrC7/2ZKMpRIyXruUOIrjuTR5eaONs1E4HI/uA3xG1eeW2pjPxDnO
|zgM4t7EPH6QbzibihoHw1MAB/mzECzY8r11PBhDQlst0a2hp+zUNR8CLbpmPPqTY
|RSo6EooQ7+NBejOXysqIF1q0BJs8Y5s/CaTOmgbL7uPCkzArB6SS/hzXgDk5gw6v
|wkGeOtzcj1DlbUTvt1s5GlnwBTGUmkbLx+YUje+n+IBgMbohLUDYBtUHylRVgMsc
|1WS67kDqeJiiQZvrxvyW6CZZ/MIGI+uAkkj3DqJpaZirkwPgvpcOIrjZy0uFvQM=
""", Base64.DEFAULT)) +
new Signature(Base64.decode( // @madeye
"""
|MIICQzCCAaygAwIBAgIETV9OhjANBgkqhkiG9w0BAQUFADBmMQswCQYDVQQGEwJjbjERMA8GA1UE
|CBMIU2hhbmdoYWkxDzANBgNVBAcTBlB1ZG9uZzEUMBIGA1UEChMLRnVkYW4gVW5pdi4xDDAKBgNV
|BAsTA1BQSTEPMA0GA1UEAxMGTWF4IEx2MB4XDTExMDIxOTA1MDA1NFoXDTM2MDIxMzA1MDA1NFow
|ZjELMAkGA1UEBhMCY24xETAPBgNVBAgTCFNoYW5naGFpMQ8wDQYDVQQHEwZQdWRvbmcxFDASBgNV
|BAoTC0Z1ZGFuIFVuaXYuMQwwCgYDVQQLEwNQUEkxDzANBgNVBAMTBk1heCBMdjCBnzANBgkqhkiG
|9w0BAQEFAAOBjQAwgYkCgYEAq6lA8LqdeEI+es9SDX85aIcx8LoL3cc//iRRi+2mFIWvzvZ+bLKr
|4Wd0rhu/iU7OeMm2GvySFyw/GdMh1bqh5nNPLiRxAlZxpaZxLOdRcxuvh5Nc5yzjM+QBv8ECmuvu
|AOvvT3UDmA0AMQjZqSCmxWIxc/cClZ/0DubreBo2st0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQAQ
|Iqonxpwk2ay+Dm5RhFfZyG9SatM/JNFx2OdErU16WzuK1ItotXGVJaxCZv3u/tTwM5aaMACGED5n
|AvHaDGCWynY74oDAopM4liF/yLe1wmZDu6Zo/7fXrH+T03LBgj2fcIkUfN1AA4dvnBo8XWAm9VrI
|1iNuLIssdhDz3IL9Yg==
""", Base64.DEFAULT))
private var receiver: BroadcastReceiver = _
private var cachedPlugins: Map[String, Plugin] = _
def fetchPlugins(): Map[String, Plugin] = {
if (receiver == null) receiver = app.listenForPackageChanges(synchronized(cachedPlugins = null))
synchronized(if (cachedPlugins == null) {
val pm = app.getPackageManager
cachedPlugins = (
pm.queryIntentContentProviders(new Intent(PluginContract.ACTION_NATIVE_PLUGIN), PackageManager.GET_META_DATA)
.map(new NativePlugin(_, pm)) :+
NoPlugin
).map(plugin => plugin.id -> plugin).toMap
})
cachedPlugins
}
private def buildUri(id: String) = new Uri.Builder()
.scheme(PluginContract.SCHEME)
.authority(PluginContract.AUTHORITY)
.path('/' + id)
.build()
def buildIntent(id: String, action: String): Intent = new Intent(action, buildUri(id))
// the following parts are meant to be used by :bg
@throws[Throwable]
def init(options: PluginOptions): String = {
if (options.id.isEmpty) return null
var throwable: Throwable = null
try initNative(options) match {
case null =>
case path => return path
} catch {
case t: Throwable =>
t.printStackTrace()
if (throwable == null) throwable = t
}
// add other plugin types here
throw if (throwable == null) new FileNotFoundException(
app.getString(com.github.shadowsocks.R.string.plugin_unknown, options.id)) else throwable
}
@throws[IOException]
@throws[IndexOutOfBoundsException]
@throws[AssertionError]
private def initNative(options: PluginOptions): String = {
val providers = app.getPackageManager.queryIntentContentProviders(
new Intent(PluginContract.ACTION_NATIVE_PLUGIN, buildUri(options.id)), 0)
assert(providers.length == 1)
val uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providers(0).providerInfo.authority)
.build()
val cr = app.getContentResolver
try initNativeFast(cr, options, uri) catch {
case t: Throwable =>
t.printStackTrace()
Log.w("PluginManager", "Initializing native plugin fast mode failed. Falling back to slow mode.")
initNativeSlow(cr, options, uri)
}
}
private def initNativeFast(cr: ContentResolver, options: PluginOptions, uri: Uri): String = {
val out = new Bundle()
out.putString(PluginContract.EXTRA_OPTIONS, options.id)
val result = cr.call(uri, PluginContract.METHOD_GET_EXECUTABLE, null, out).getString(PluginContract.EXTRA_ENTRY)
assert(new File(result).canExecute)
result
}
private def initNativeSlow(cr: ContentResolver, options: PluginOptions, uri: Uri): String =
cr.query(uri, Array(PluginContract.COLUMN_PATH, PluginContract.COLUMN_MODE), null, null, null) match {
case null => null
case cursor =>
var initialized = false
val pluginDir = new File(app.getFilesDir, "plugin")
def entryNotFound() = throw new IndexOutOfBoundsException("Plugin entry binary not found")
if (!cursor.moveToFirst()) entryNotFound()
IOUtils.deleteRecursively(pluginDir)
if (!pluginDir.mkdirs()) throw new FileNotFoundException("Unable to create plugin directory")
val pluginDirPath = pluginDir.getAbsolutePath + '/'
val list = new ListBuffer[String]
do {
val path = cursor.getString(0)
val file = new File(pluginDir, path)
assert(file.getAbsolutePath.startsWith(pluginDirPath))
autoClose(cr.openInputStream(uri.buildUpon().path(path).build()))(in =>
autoClose(new FileOutputStream(file))(out => IOUtils.copy(in, out)))
list += Commandline.toString(Array("chmod", cursor.getString(1), file.getAbsolutePath))
if (path == options.id) initialized = true
} while (cursor.moveToNext())
if (!initialized) entryNotFound()
Shell.SH.run(list)
new File(pluginDir, options.id).getAbsolutePath
}
}