Commit e8b59537 authored by Max Lv's avatar Max Lv Committed by GitHub

Merge pull request #1364 from Mygod/url-rules

Rewrite network ACLs
parents d7183fe1 b8d7ade4
...@@ -224,5 +224,6 @@ ...@@ -224,5 +224,6 @@
<string-array name="acl_rule_templates"> <string-array name="acl_rule_templates">
<item>@string/acl_rule_templates_generic</item> <item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item> <item>@string/acl_rule_templates_domain</item>
<item>URL</item>
</string-array> </string-array>
</resources> </resources>
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
<string name="custom_rules">Custom rules</string> <string name="custom_rules">Custom rules</string>
<string name="action_selection">Selection…</string> <string name="action_selection">Selection…</string>
<string name="action_add_rule">Add rule(s)…</string> <string name="action_add_rule">Add rule(s)…</string>
<string name="acl_rule_templates_generic">URL, Subnet or Hostname PCRE pattern</string> <string name="acl_rule_templates_generic">Subnet or Hostname PCRE pattern</string>
<string name="acl_rule_templates_domain">Domain name and all its subdomain names</string> <string name="acl_rule_templates_domain">Domain name and all its subdomain names</string>
<string name="edit_rule">Edit rule</string> <string name="edit_rule">Edit rule</string>
......
...@@ -150,8 +150,7 @@ trait BaseService extends Service { ...@@ -150,8 +150,7 @@ trait BaseService extends Service {
profile.method = proxy(3).trim profile.method = proxy(3).trim
} }
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules if (profile.route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES), true)
plugin = new PluginConfiguration(profile.plugin).selectedOptions plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin) pluginPath = PluginManager.init(plugin)
......
...@@ -63,7 +63,10 @@ class ShadowsocksNatService extends BaseService { ...@@ -63,7 +63,10 @@ class ShadowsocksNatService extends BaseService {
if (profile.route != Acl.ALL) { if (profile.route != Acl.ALL) {
cmd += "--acl" cmd += "--acl"
cmd += Acl.getFile(profile.route).getAbsolutePath cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
} }
sslocalProcess = new GuardedProcess(cmd: _*).start() sslocalProcess = new GuardedProcess(cmd: _*).start()
......
...@@ -161,7 +161,10 @@ class ShadowsocksVpnService extends VpnService with BaseService { ...@@ -161,7 +161,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL) { if (profile.route != Acl.ALL) {
cmd += "--acl" cmd += "--acl"
cmd += Acl.getFile(profile.route).getAbsolutePath cmd += Acl.getFile(profile.route match {
case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
case route => route
}).getAbsolutePath
} }
if (TcpFastOpen.sendEnabled) cmd += "--fast-open" if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
......
package com.github.shadowsocks.acl package com.github.shadowsocks.acl
import java.io.{File, FileNotFoundException, IOException} import java.io.{File, FileNotFoundException}
import java.net.URL
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils import com.github.shadowsocks.utils.IOUtils
import com.j256.ormlite.field.DatabaseField import com.j256.ormlite.field.DatabaseField
...@@ -17,21 +19,44 @@ import scala.io.Source ...@@ -17,21 +19,44 @@ import scala.io.Source
* @author Mygod * @author Mygod
*/ */
class Acl { class Acl {
import Acl._
@DatabaseField(generatedId = true) @DatabaseField(generatedId = true)
var id: Int = _ var id: Int = _
val bypassHostnames = new mutable.SortedList[String]()
val hostnames = new mutable.SortedList[String]() val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]() val subnets = new mutable.SortedList[Subnet]()
val urls = new mutable.SortedList[String]() val urls = new mutable.SortedList[URL]()(urlOrdering)
@DatabaseField @DatabaseField
var bypass: Boolean = _ var bypass: Boolean = _
def isUrl(url: String): Boolean = url.startsWith("http://") || url.startsWith("https://") def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
def getSubnetsString: String = subnets.mkString("\n")
def getUrlsString: String = urls.mkString("\n")
def setBypassHostnamesString(value: String) {
bypassHostnames.clear()
bypassHostnames ++= value.split("\n")
}
def setProxyHostnamesString(value: String) {
proxyHostnames.clear()
proxyHostnames ++= value.split("\n")
}
def setSubnetsString(value: String) {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
def setUrlsString(value: String) {
urls.clear()
urls ++= value.split("\n").map(new URL(_))
}
def fromAcl(other: Acl): Acl = { def fromAcl(other: Acl): Acl = {
hostnames.clear() bypassHostnames.clear()
hostnames ++= other.hostnames bypassHostnames ++= other.bypassHostnames
proxyHostnames.clear()
proxyHostnames ++= other.proxyHostnames
subnets.clear() subnets.clear()
subnets ++= other.subnets subnets ++= other.subnets
urls.clear() urls.clear()
...@@ -39,78 +64,83 @@ class Acl { ...@@ -39,78 +64,83 @@ class Acl {
bypass = other.bypass bypass = other.bypass
this this
} }
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
def fromSource(value: Source, defaultBypass: Boolean = true): Acl = { bypassHostnames.clear()
subnets.clear() proxyHostnames.clear()
this.subnets.clear()
urls.clear() urls.clear()
bypass = defaultBypass bypass = defaultBypass
var in_urls = false lazy val bypassSubnets = new mutable.SortedList[Subnet]()
for (line <- value.getLines()) (line.trim.indexOf('#') match { lazy val proxySubnets = new mutable.SortedList[Subnet]()
case 0 => { var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
line.indexOf("NETWORK_ACL_BEGIN") match { var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
case -1 => for (line <- value.getLines()) (line.indexOf('#') match {
case index => in_urls = true case -1 => line
} case index =>
line.indexOf("NETWORK_ACL_END") match { line.substring(index + 1) match {
case -1 => case networkAclParser(url) => urls.add(new URL(url))
case index => in_urls = false case _ => // ignore
} }
"" // ignore any comment lines line.substring(0, index) // trim comments
}
case index => if (!in_urls) line else ""
}).trim match { }).trim match {
// Ignore all section controls
case "[outbound_block_list]" => case "[outbound_block_list]" =>
hostnames = null
subnets = null
case "[black_list]" | "[bypass_list]" => case "[black_list]" | "[bypass_list]" =>
hostnames = bypassHostnames
subnets = bypassSubnets
case "[white_list]" | "[proxy_list]" => case "[white_list]" | "[proxy_list]" =>
case "[reject_all]" | "[bypass_all]" => hostnames = proxyHostnames
case "[accept_all]" | "[proxy_all]" => subnets = proxySubnets
case "[reject_all]" | "[bypass_all]" => bypass = true
case "[accept_all]" | "[proxy_all]" => bypass = false
case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch { case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
case _: IllegalArgumentException => if (isUrl(input)) { case _: IllegalArgumentException => hostnames += input
urls += input
} else {
hostnames += input
}
} }
case _ => case _ =>
} }
this.subnets ++= (if (bypass) proxySubnets else bypassSubnets)
this this
} }
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id))) final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
def getAclString(network: Boolean): String = { def flatten(depth: Int): Acl = {
val result = new StringBuilder() if (depth > 0) for (url <- urls) {
if (urls.nonEmpty) { val child = new Acl().fromSource(Source.fromURL(url), bypass).flatten(depth - 1)
result.append(urls.mkString("\n")) if (bypass != child.bypass) {
if (network) { Log.w(TAG, "Imported network ACL has a conflicting mode set. This will probably not work as intended. URL: %s"
result.append("\n#NETWORK_ACL_BEGIN\n") .format(url))
try { child.subnets.clear() // subnets for the different mode are discarded
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString)) child.bypass = bypass
} catch {
case e: IOException => // ignore
}
result.append("\n#NETWORK_ACL_END\n")
} }
result.append("\n") bypassHostnames ++= child.bypassHostnames
proxyHostnames ++= child.proxyHostnames
subnets ++= child.subnets
} }
if (result.isEmpty) { urls.clear()
result.append("[bypass_all]\n") this
}
override def toString: String = {
val result = new StringBuilder()
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
val (bypassList, proxyList) =
if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
if (bypassList.nonEmpty) {
result.append("[bypass_list]\n")
result.append(bypassList.mkString("\n"))
result.append('\n')
} }
val list = subnets.toStream.map(_.toString) #::: hostnames.toStream if (proxyList.nonEmpty) {
if (list.nonEmpty) {
result.append("[proxy_list]\n") result.append("[proxy_list]\n")
result.append(list.mkString("\n")) result.append(proxyList.mkString("\n"))
result.append('\n') result.append('\n')
} }
result.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
result.toString result.toString
} }
override def toString: String = {
getAclString(false)
}
def isValidCustomRules: Boolean = !hostnames.isEmpty
// Don't change: dummy fields for OrmLite interaction // Don't change: dummy fields for OrmLite interaction
// noinspection ScalaUnusedSymbol // noinspection ScalaUnusedSymbol
...@@ -125,6 +155,7 @@ class Acl { ...@@ -125,6 +155,7 @@ class Acl {
} }
object Acl { object Acl {
final val TAG = "Acl"
final val ALL = "all" final val ALL = "all"
final val BYPASS_LAN = "bypass-lan" final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china" final val BYPASS_CHN = "bypass-china"
...@@ -132,6 +163,10 @@ object Acl { ...@@ -132,6 +163,10 @@ object Acl {
final val GFWLIST = "gfwlist" final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list" final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules" final val CUSTOM_RULES = "custom-rules"
final val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
private val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".r
private val urlOrdering: Ordering[URL] = Ordering.by(url => (url.getHost, url.getPort, url.getFile, url.getProtocol))
def getFile(id: String) = new File(app.getFilesDir, id + ".acl") def getFile(id: String) = new File(app.getFilesDir, id + ".acl")
def customRules: Acl = { def customRules: Acl = {
...@@ -139,9 +174,9 @@ object Acl { ...@@ -139,9 +174,9 @@ object Acl {
try acl.fromId(CUSTOM_RULES) catch { try acl.fromId(CUSTOM_RULES) catch {
case _: FileNotFoundException => case _: FileNotFoundException =>
} }
acl.bypass = true
acl.bypassHostnames.clear() // everything is bypassed
acl acl
} }
def save(id: String, acl: Acl, network: Boolean = false): Unit = { def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString)
IOUtils.writeString(getFile(id), acl.getAclString(network))
}
} }
package com.github.shadowsocks.acl package com.github.shadowsocks.acl
import java.net.IDN import java.net.{IDN, URL}
import java.util.Locale import java.util.Locale
import android.content.{ClipData, ClipboardManager, Context, DialogInterface} import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
...@@ -26,13 +26,15 @@ import scala.io.Source ...@@ -26,13 +26,15 @@ import scala.io.Source
* @author Mygod * @author Mygod
*/ */
object CustomRulesFragment { object CustomRulesFragment {
private final val GENERIC = 0 private final val TEMPLATE_GENERIC = 0
private final val DOMAIN = 1 private final val TEMPLATE_DOMAIN = 1
private final val TEMPLATE_URL = 2
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
private final val TEMPLATE_DOMAIN = "(^|\\.)%s$" private final val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS" private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES" private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
private final val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
} }
class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener { class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
...@@ -44,18 +46,26 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -44,18 +46,26 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case _ => false case _ => false
} }
private def createAclRuleDialog(text: CharSequence = "") = { private def createAclRuleDialog(item: AnyRef = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null) val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById[Spinner](R.id.template_selector) val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById[EditText](R.id.content) val editText = view.findViewById[EditText](R.id.content)
PATTERN_DOMAIN.findFirstMatchIn(text) match { item match {
case Some(m) => case hostname: String => PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
templateSelector.setSelection(DOMAIN) case Some(m) =>
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."), templateSelector.setSelection(TEMPLATE_DOMAIN)
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES)) editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
case None => IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
templateSelector.setSelection(GENERIC) case None =>
editText.setText(text) templateSelector.setSelection(TEMPLATE_GENERIC)
editText.setText(hostname)
}
case url: URL =>
templateSelector.setSelection(TEMPLATE_URL)
editText.setText(url.toString)
case i =>
templateSelector.setSelection(TEMPLATE_GENERIC)
editText.setText(i.toString)
} }
(templateSelector, editText, new AlertDialog.Builder(getActivity) (templateSelector, editText, new AlertDialog.Builder(getActivity)
.setTitle(R.string.edit_rule) .setTitle(R.string.edit_rule)
...@@ -85,9 +95,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -85,9 +95,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
text.setText(subnet.toString) text.setText(subnet.toString)
itemView.setSelected(selectedItems.contains(subnet)) itemView.setSelected(selectedItems.contains(subnet))
} }
def bind(url: URL) {
item = url
text.setText(url.toString)
itemView.setSelected(selectedItems.contains(url))
}
def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else { def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString) val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog dialog
.setNeutralButton(R.string.delete, ((_, _) => { .setNeutralButton(R.string.delete, ((_, _) => {
adapter.remove(item) adapter.remove(item)
...@@ -125,88 +140,86 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -125,88 +140,86 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
}) })
} }
def getItemCount: Int = acl.subnets.size + acl.hostnames.size + acl.urls.size def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.size
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = { def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
val j = i - acl.urls.size val j = i - acl.subnets.size
val k = i - acl.subnets.size - acl.urls.size if (j < 0) vh.bind(acl.subnets(i)) else {
if (j < 0) val k = j - acl.proxyHostnames.size
vh.bind(acl.urls(i)) if (k < 0) vh.bind(acl.proxyHostnames(j)) else vh.bind(acl.urls(k))
else if (k < 0) }
vh.bind(acl.subnets(j))
else
vh.bind(acl.hostnames(k))
} }
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext) def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
.inflate(android.R.layout.simple_list_item_1, vg, false)) .inflate(android.R.layout.simple_list_item_1, vg, false))
def addUrl(url: String): Int = if (acl.urls.add(url)) { def addSubnet(subnet: Subnet): Int = if (acl.subnets.add(subnet)) {
val index = acl.urls.indexOf(url) val index = acl.subnets.indexOf(subnet)
notifyItemInserted(index) notifyItemInserted(index)
apply() apply()
index index
} else -1 } else -1
def addSubnet(subnet: Subnet): Int = if (acl.subnets.add(subnet)) { def addHostname(hostname: String): Int = if (acl.proxyHostnames.add(hostname)) {
val index = acl.subnets.indexOf(subnet) + acl.urls.size val index = acl.proxyHostnames.indexOf(hostname) + acl.subnets.size
notifyItemInserted(index) notifyItemInserted(index)
apply() apply()
index index
} else -1 } else -1
def addHostname(hostname: String): Int = if (acl.hostnames.add(hostname)) { def addURL(url: URL): Int = if (acl.urls.add(url)) {
val index = acl.hostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size val index = acl.urls.indexOf(url) + acl.subnets.size + acl.proxyHostnames.size
notifyItemInserted(index) notifyItemInserted(index)
apply() apply()
index index
} else -1 } else -1
def addToProxy(input: String): Int = { def addToProxy(input: String): Int = {
val acl = new Acl().fromSource(Source.fromString(input)) val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
var result = -1 var result = -1
for (url <- acl.urls) result = addUrl(url) if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
for (hostname <- acl.hostnames) result = addHostname(hostname) for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
for (subnet <- acl.subnets) result = addSubnet(subnet) for (url <- acl.urls) result = addURL(url)
result result
} }
def addFromTemplate(template: Int, text: CharSequence): Int = template match { def addFromTemplate(template: Int, text: CharSequence): Int = template match {
case GENERIC => addToProxy(text.toString) case TEMPLATE_GENERIC => addToProxy(text.toString)
case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH, case TEMPLATE_DOMAIN => try addHostname(TEMPLATE_REGEX_DOMAIN.formatLocal(Locale.ENGLISH,
IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch { IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch {
case exc: IllegalArgumentException => case exc: IllegalArgumentException =>
Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show() Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
-1 -1
} }
case TEMPLATE_URL => addURL(new URL(text.toString))
case _ => -1 case _ => -1
} }
def remove(i: Int) { def remove(i: Int) {
val j = i - acl.urls.size val j = i - acl.subnets.size
val k = i - acl.urls.size - acl.subnets.size
if (j < 0) { if (j < 0) {
undoManager.remove((i, acl.urls(i))) undoManager.remove((i, acl.subnets(i)))
acl.urls.remove(i) acl.subnets.remove(i)
} else if (k < 0) {
undoManager.remove((i, acl.subnets(j)))
acl.subnets.remove(j)
} else { } else {
undoManager.remove((i, acl.hostnames(k))) val k = j - acl.proxyHostnames.size
acl.hostnames.remove(k) if (k < 0) {
undoManager.remove((j, acl.proxyHostnames(j)))
acl.proxyHostnames.remove(j)
} else {
undoManager.remove((k, acl.urls(k)))
acl.urls.remove(k)
}
} }
notifyItemRemoved(i) notifyItemRemoved(i)
apply() apply()
} }
def remove(item: AnyRef): Unit = item match { def remove(item: AnyRef): Unit = item match {
case subnet: Subnet => case subnet: Subnet =>
notifyItemRemoved(acl.subnets.indexOf(subnet) + acl.urls.size) notifyItemRemoved(acl.subnets.indexOf(subnet))
acl.subnets.remove(subnet) acl.subnets.remove(subnet)
apply() apply()
case hostname: String => if (acl.isUrl(hostname)) { case hostname: String =>
notifyItemRemoved(acl.urls.indexOf(hostname)) notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
acl.urls.remove(hostname) acl.proxyHostnames.remove(hostname)
apply() apply()
} else { case url: URL =>
notifyItemRemoved(acl.hostnames.indexOf(hostname) notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
+ acl.urls.size + acl.subnets.size) acl.urls.remove(url)
acl.hostnames.remove(hostname)
apply() apply()
}
} }
def removeSelected() { def removeSelected() {
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*) undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
...@@ -215,28 +228,25 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -215,28 +228,25 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
onSelectedItemsUpdated() onSelectedItemsUpdated()
} }
def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match { def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
case hostname: String => if (acl.isUrl(hostname)) {
if (acl.urls.insert(hostname)) {
notifyItemInserted(acl.urls.indexOf(hostname))
apply()
}
} else {
if (acl.hostnames.insert(hostname)) {
notifyItemInserted(acl.hostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size)
apply()
}
}
case subnet: Subnet => if (acl.subnets.insert(subnet)) { case subnet: Subnet => if (acl.subnets.insert(subnet)) {
notifyItemInserted(acl.subnets.indexOf(subnet) + acl.urls.size) notifyItemInserted(acl.subnets.indexOf(subnet))
apply()
}
case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
notifyItemInserted(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
apply()
}
case url: URL => if (acl.urls.insert(url)) {
notifyItemInserted(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
apply() apply()
} }
} }
def selectAll() { def selectAll() {
selectedItems.clear() selectedItems.clear()
selectedItems ++= acl.urls
selectedItems ++= acl.subnets selectedItems ++= acl.subnets
selectedItems ++= acl.hostnames selectedItems ++= acl.proxyHostnames
selectedItems ++= acl.urls
onSelectedItemsUpdated() onSelectedItemsUpdated()
notifyDataSetChanged() notifyDataSetChanged()
} }
...@@ -245,11 +255,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -245,11 +255,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
val j = i - acl.subnets.size val j = i - acl.subnets.size
try { try {
(if (j < 0) acl.subnets(i).address.getHostAddress.substring(0, 1) else { (if (j < 0) acl.subnets(i).address.getHostAddress.substring(0, 1) else {
val hostname = acl.hostnames(i) val k = j - acl.proxyHostnames.size
PATTERN_DOMAIN.findFirstMatchIn(hostname) match { if (k < 0) {
case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet val hostname = acl.proxyHostnames(j)
case None => hostname PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
} case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
case None => hostname
}
} else acl.urls(k).getHost
}).substring(0, 1) }).substring(0, 1)
} catch { } catch {
case _: IndexOutOfBoundsException => " " case _: IndexOutOfBoundsException => " "
...@@ -276,6 +289,10 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -276,6 +289,10 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case null => case null =>
case arr => selectedItems ++= arr case arr => selectedItems ++= arr
} }
savedInstanceState.getStringArray(SELECTED_URLS) match {
case null =>
case arr => selectedItems ++= arr.map(new URL(_))
}
onSelectedItemsUpdated() onSelectedItemsUpdated()
} }
toolbar.setTitle(R.string.custom_rules) toolbar.setTitle(R.string.custom_rules)
...@@ -308,18 +325,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -308,18 +325,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray) outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray)
outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray) outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray)
outState.putStringArray(SELECTED_URLS, selectedItems.filter(_.isInstanceOf[URL]).map(_.toString).toArray)
} }
def copySelected() {
val acl = new Acl()
acl.bypass = true
selectedItems.foreach {
case subnet: Subnet => acl.subnets.add(subnet)
case hostname: String => acl.proxyHostnames.add(hostname)
case url: URL => acl.urls.add(url)
}
clipboard.setPrimaryClip(ClipData.newPlainText(null, acl.toString))
}
override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match { override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match {
case R.id.action_select_all => case R.id.action_select_all =>
adapter.selectAll() adapter.selectAll()
true true
case R.id.action_cut => case R.id.action_cut =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n"))) copySelected()
adapter.removeSelected() adapter.removeSelected()
true true
case R.id.action_copy => case R.id.action_copy =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n"))) copySelected()
true true
case R.id.action_delete => case R.id.action_delete =>
adapter.removeSelected() adapter.removeSelected()
...@@ -333,15 +361,18 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi ...@@ -333,15 +361,18 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
.create().show() .create().show()
true true
case R.id.action_import => case R.id.action_import =>
try if (adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1) return true catch { try adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1 catch {
case _: Exception => case exc: Exception =>
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
.show()
exc.printStackTrace()
} }
Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG).show()
true true
case R.id.action_import_gfwlist => case R.id.action_import_gfwlist =>
val acl = new Acl().fromId(Acl.GFWLIST) val acl = new Acl().fromId(Acl.GFWLIST)
acl.subnets.foreach(adapter.addSubnet) acl.subnets.foreach(adapter.addSubnet)
acl.hostnames.foreach(adapter.addHostname) acl.proxyHostnames.foreach(adapter.addHostname)
acl.urls.foreach(adapter.addURL)
true true
case _ => false case _ => false
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment