/******************************************************************************* * * * Copyright (C) 2017 by Max Lv * * Copyright (C) 2017 by Mygod Studio * * * * This program 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 Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program 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 this program. If not, see . * * * *******************************************************************************/ package com.github.shadowsocks.acl import android.content.Context import android.support.v7.util.SortedList import android.util.Log import com.github.shadowsocks.App.Companion.app import com.github.shadowsocks.utils.Subnet import com.github.shadowsocks.utils.asIterable import com.j256.ormlite.field.DatabaseField import java.io.File import java.io.FileNotFoundException import java.io.Reader import java.net.URL class Acl { companion object { const val TAG = "Acl" const val ALL = "all" const val BYPASS_LAN = "bypass-lan" const val BYPASS_CHN = "bypass-china" const val BYPASS_LAN_CHN = "bypass-lan-china" const val GFWLIST = "gfwlist" const val CHINALIST = "china-list" const val CUSTOM_RULES = "custom-rules" const val CUSTOM_RULES_FLATTENED = "custom-rules-flattened" val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".toRegex() fun getFile(id: String, context: Context = app.deviceContext) = File(context.filesDir, id + ".acl") val customRules: Acl get() { val acl = Acl() try { acl.fromId(CUSTOM_RULES) if (!acl.bypass) { acl.subnets.clear() acl.hostnames.clear() } } catch (_: FileNotFoundException) { } acl.bypass = true return acl } fun save(id: String, acl: Acl) = getFile(id).writeText(acl.toString()) } private abstract class BaseSorter : SortedList.Callback() { override fun onInserted(position: Int, count: Int) { } override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem override fun onMoved(fromPosition: Int, toPosition: Int) { } override fun onChanged(position: Int, count: Int) { } override fun onRemoved(position: Int, count: Int) { } override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2 override fun compare(o1: T?, o2: T?): Int = if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2) abstract fun compareNonNull(o1: T, o2: T): Int } private open class DefaultSorter> : BaseSorter() { override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2) } private object StringSorter : DefaultSorter() private object SubnetSorter : DefaultSorter() private object URLSorter : BaseSorter() { private val ordering = compareBy({ it.host }, { it.port }, { it.file }, { it.protocol }) override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2) } @DatabaseField(generatedId = true) var id = 0 val hostnames = SortedList(String::class.java, StringSorter) val subnets = SortedList(Subnet::class.java, SubnetSorter) val urls = SortedList(URL::class.java, URLSorter) @DatabaseField var bypass = false fun clear(): Acl { hostnames.clear() subnets.clear() urls.clear() return this } fun fromAcl(other: Acl): Acl { clear() for (item in other.hostnames.asIterable()) hostnames.add(item) for (item in other.subnets.asIterable()) subnets.add(item) for (item in other.urls.asIterable()) urls.add(item) bypass = other.bypass return this } fun fromReader(reader: Reader, defaultBypass: Boolean = false): Acl { clear() bypass = defaultBypass val proxyHostnames by lazy { SortedList(String::class.java, StringSorter) } val bypassHostnames by lazy { SortedList(String::class.java, StringSorter) } val bypassSubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) } val proxySubnets by lazy { SortedList(Subnet::class.java, SubnetSorter) } var hostnames: SortedList? = if (defaultBypass) proxyHostnames else bypassHostnames var subnets: SortedList? = if (defaultBypass) proxySubnets else bypassSubnets reader.useLines { for (line in it) { val blocks = line.split('#', limit = 2) val url = networkAclParser.matchEntire(blocks.getOrElse(1, {""}))?.groupValues?.getOrNull(1) if (url != null) urls.add(URL(url)) val input = blocks[0].trim() when (input) { "[outbound_block_list]" -> { hostnames = null subnets = null } "[black_list]", "[bypass_list]" -> { hostnames = bypassHostnames subnets = bypassSubnets } "[white_list]", "[proxy_list]" -> { hostnames = proxyHostnames subnets = proxySubnets } "[reject_all]", "[bypass_all]" -> bypass = true "[accept_all]", "[proxy_all]" -> bypass = false else -> if (subnets != null && input.isNotEmpty()) try { subnets!!.add(Subnet.fromString(input)) } catch (_: IllegalArgumentException) { hostnames!!.add(input) } } } } for (item in (if (bypass) proxyHostnames else bypassHostnames).asIterable()) this.hostnames.add(item) for (item in (if (bypass) proxySubnets else bypassSubnets).asIterable()) this.subnets.add(item) return this } fun fromId(id: String): Acl = fromReader(Acl.getFile(id).bufferedReader()) fun flatten(depth: Int): Acl { if (depth > 0) for (url in urls.asIterable()) { val child = Acl().fromReader(url.openStream().bufferedReader(), bypass).flatten(depth - 1) if (bypass != child.bypass) { Log.w(TAG, "Imported network ACL has a conflicting mode set. " + "This will probably not work as intended. URL: $url") // rules for the different mode are discarded child.hostnames.clear() child.subnets.clear() child.bypass = bypass } for (item in child.hostnames.asIterable()) hostnames.add(item) for (item in child.subnets.asIterable()) subnets.add(item) } urls.clear() return this } override fun toString(): String { val result = StringBuilder() result.append(if (bypass) "[bypass_all]\n[proxy_list]\n" else "[proxy_all]\n[bypass_list]\n") result.append(subnets.asIterable().joinToString("\n")) result.append('\n') result.append(hostnames.asIterable().joinToString("\n")) result.append('\n') result.append(urls.asIterable().joinToString("") { "#IMPORT_URL <$it>\n" }) return result.toString() } }