Compare commits

...
16 Commits
Author SHA1 Message Date
Max Lv bb25df8692 Bump version 2017-09-06 18:48:02 -07:00
Max Lv e8b5953701 Merge pull request #1364 from Mygod/url-rules
Rewrite network ACLs
2017-09-04 15:29:20 +08:00
Mygod b8d7ade44d Rewrite network ACLs
* A cleaner format;
* Provides better behavior when importing a non-compatible ACL;
* Supports importing URL from a imported file with up to 10 layers.
2017-09-03 17:32:32 -07:00
Mygod d7183fe147 Update strings.xml (POEditor.com)
Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)

Update strings.xml (POEditor.com)
2017-09-03 15:43:05 -07:00
Mygod 8b870877d5 Add persian language 2017-09-03 15:26:21 -07:00
Mygod 024628483e Ignore gen directories 2017-09-03 15:25:02 -07:00
Mygod 73fe14d643 Revert "Remove obsolete items in FAQ"
This reverts commit 068906cd0f.
2017-08-31 20:09:18 -07:00
Mygod 512cb06671 Update dependencies 2017-08-31 19:57:49 -07:00
Max Lv 3d069b10ee Fix #1349 2017-08-29 02:06:35 -07:00
Max Lv 4490d95c7f Bump version 2017-08-28 22:11:38 -07:00
Max Lv 390cb522d5 Fix #1351 2017-08-22 03:10:27 -07:00
Max Lv b14fdcaed9 Fix the missing selected rules 2017-08-20 18:14:07 +08:00
Max Lv b6adeda924 Add local DNS to CHINALIST routing. #1348 2017-08-20 17:41:30 +08:00
Max Lv 253286c482 Bump version 2017-08-15 12:02:03 +08:00
Max Lv f25406b86c Protect table creation with transaction 2017-08-10 08:39:49 +08:00
Max Lv 2c8718163a Add new lines for remote ACL 2017-08-08 07:45:56 +08:00
23 changed files with 390 additions and 171 deletions
+10
View File
@@ -14,6 +14,11 @@ Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issu
* Tap the number to enter the port you wish to use; (if the keyboard doesn't pop up automatically for some reason)
* Use Tasker integration to create a desktop widget.
### How to add QR code from local gallery?
Scan it with a third-party scanner like [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) and click the `ss` url.
### Why is NAT mode deprecated?
1. Requiring ROOT permission;
@@ -47,3 +52,8 @@ So if you notice a significant increase in battery usage after you use Shadowsoc
### It works fine under Wi-Fi but can't connect through cellular data?
Allow this app to consume background data in app settings.
### Why Camera permission is required on devices below Android 6.0?
To scan the QR code through the integrated QR scanner.
+1
View File
@@ -6,6 +6,7 @@ local.sbt
#Intellij IDEA
.idea/
.d
gen/
mobile/src/main/libs
mobile/src/main/obj
+1 -1
View File
@@ -18,7 +18,7 @@ lazy val commonSettings = Seq(
shrinkResources := true,
typedResources := false,
resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW"),
resConfigs := Seq("fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"),
resolvers += "google" at "https://maven.google.com"
)
+8 -8
View File
@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
android.useSupportVectors
name := "shadowsocks"
version := "4.2.2"
versionCode := Some(192)
version := "4.2.5"
versionCode := Some(195)
proguardOptions ++=
"-dontwarn com.google.android.gms.internal.**" ::
@@ -23,11 +23,11 @@ proguardOptions ++=
"-keep public class com.evernote.android.job.JobRescheduleService" ::
Nil
val playServicesVersion = "11.0.4"
val playServicesVersion = "11.2.0"
resolvers += Resolver.jcenterRepo
libraryDependencies ++=
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
"com.evernote" % "android-job" % "1.2.0-alpha3" ::
"com.evernote" % "android-job" % "1.2.0-alpha4" ::
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
@@ -35,15 +35,15 @@ libraryDependencies ++=
"com.google.firebase" % "firebase-config" % playServicesVersion ::
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
"com.mikepenz" % "crossfader" % "1.5.0" ::
"com.mikepenz" % "fastadapter" % "2.6.2" ::
"com.mikepenz" % "iconics-core" % "2.8.9" ::
"com.mikepenz" % "materialdrawer" % "5.9.4" ::
"com.mikepenz" % "fastadapter" % "2.6.3" ::
"com.mikepenz" % "iconics-core" % "2.9.3" ::
"com.mikepenz" % "materialdrawer" % "5.9.5" ::
"com.mikepenz" % "materialize" % "1.0.3" ::
"com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
"me.dm7.barcodescanner" % "zxing" % "1.9.8" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
+60
View File
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">"شدوساکس"</string>
<!-- misc -->
<string name="profile">"پروفایل"</string>
<string name="connection_test_testing">"در حال آزمایش..."</string>
<!-- proxy category -->
<string name="profile_name">"نام پروفایل"</string>
<string name="proxy">"سرور"</string>
<string name="sitekey">"کلمه عبور"</string>
<string name="enc_method">"طریقه رمزگذاری"</string>
<string name="on">"روشن"</string>
<string name="auto_connect">"وصل‌شدن اتوماتیک"</string>
<string name="auto_connect_summary">"فعال‌شدن شدوساکس لحظه روشن‌شدن گوشی"</string>
<!-- notification category -->
<string name="forward_success">"شدوساکس فعال شد."</string>
<string name="invalid_server">"آدرس سرور نامعتبر است"</string>
<string name="service_failed">"اتصال به سرور ناموفق بود!"</string>
<string name="stop">"غیرفعال‌کردن"</string>
<string name="stopping">"در حال قطع‌کردن اتصال..."</string>
<string name="profile_invalid_input">"هیچ پروفایل معتبری یافت نشد!"</string>
<!-- alert category -->
<string name="close">"خروج"</string>
<string name="profile_empty">"لطفا یک پروفایل انتخاب کنید."</string>
<string name="proxy_empty">"پروکسی یا کلمه عبور نباید خالی باشند!"</string>
<string name="connect">"اتصال"</string>
<!-- menu category -->
<string name="profiles">"پروفایل‌ها"</string>
<string name="settings">"تنظیمات"</string>
<string name="faq">"سوالات مداول"</string>
<string name="recovery">"ریست"</string>
<string name="about">"درباره ما"</string>
<string name="edit">"ویرایش"</string>
<string name="share">"به اشتراک گذاری"</string>
<string name="add_profile">"افزودن پروفایل"</string>
<string name="delete">"حذف"</string>
<string name="delete_confirm_prompt">"آیا مطمئن هستید که می‌خواهید این پروفایل را حذف کنید؟"</string>
<string name="share_qr_nfc">"NFC یا QR Code"</string>
<string name="add_profile_methods_scan_qr_code">"اسکن‌کردن QR Code"</string>
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
<!-- tasker -->
<string name="toggle_service_state">"فعال‌کردن سرویس"</string>
<string name="start_service_default">"وصل‌شدن به سرور فعلی"</string>
<string name="profile_default">"استفاده از پروفایل فعلی"</string>
<!-- status -->
<string name="sent">"فرستاده‌شده:"</string>
<string name="received">"دریافت‌شده:"</string>
<string name="connecting">"در حال اتصال..."</string>
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
<string name="nat_connected">"اتصال برقرار است."</string>
<string name="not_connected">"اتصال برقرار نیست!"</string>
<string name="route_entry_all">"همه"</string>
</resources>
+7 -5
View File
@@ -2,8 +2,7 @@
<resources>
<string name="quick_toggle">"ON/OFF"</string>
<!-- global/misc category -->
<string name="misc_cat">"その他"</string>
<!-- misc -->
<string name="profile">"プロファイル"</string>
<string name="profile_summary">"新規作成または既存ファイルを開く"</string>
<string name="nat">"NAT モード (デバックのみ)"</string>
@@ -90,8 +89,6 @@
<string name="add_profile_dialog">"Shadowsocks用プロファイルを追加しますか"</string>
<string name="add_profile_methods_scan_qr_code">"QR コードを読み取る"</string>
<string name="add_profile_methods_manual_settings">"手動設定"</string>
<string name="add_profile_scanner_not_installed">"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"</string>
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
<plurals name="removed">
<item quantity="other">"削除済み"</item>
</plurals>
@@ -133,4 +130,9 @@
<string name="plugin_unknown">"不明なプラグイン"</string>
<string name="plugin_untrusted">"警告:このプラグインは信頼されていないソースからの可能性があります"</string>
<string name="profile_plugin">"プラグイン"</string>
</resources>
<string name="add_profile_scanner_permission_required">"QRコードをスキャンするにはカメラ権限が必要です。"</string>
<!-- notification category -->
<string name="service_vpn">"VPNサービス"</string>
<string name="service_nat">"NATサービス"</string>
</resources>
+73 -9
View File
@@ -8,16 +8,11 @@
<string name="nat">"NAT 모드 (지원 중단)"</string>
<string name="nat_summary">"VPN 모드 대신 NAT 모드를 사용합니다. 루트 권한이 필요합니다."</string>
<string name="remote_dns">"원격 DNS"</string>
<string name="stat_summary">"송신: \t\t\t\t\t%3$s\t↑\t%1$s/s
수신: \t%4$s\t↓\t%2$s/s "</string>
<string name="stat_profiles">"%1$s↑\t%2$s↓"</string>
<string name="traffic_summary">"%1$s/s↑\t%2$s/s↓"</string>
<plurals name="bytes">
<item quantity="other">"바이트"</item>
</plurals>
<string name="stat_summary">"송신: \t%3$s\t↑\t%1$s/s
수신: \t%4$s\t↓\t%2$s/s"</string>
<string name="connection_test_pending">"인터넷 연결 상태 검사하기"</string>
<string name="connection_test_testing">"검사 중…"</string>
<string name="connection_test_available">"성공: %dms"</string>
<string name="connection_test_available">"성공: 지연시간 %dms"</string>
<string name="connection_test_error">"인터넷 연결을 찾을 수 없습니다: %s"</string>
<string name="connection_test_fail">"인터넷에 연결할 수 없습니다"</string>
<string name="connection_test_error_status_code">"오류 코드: #%d"</string>
@@ -45,4 +40,73 @@
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s &lt; 3.7.1"</string>
<string name="udp_dns">"DNS 포워딩"</string>
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
</resources>
<!-- notification category -->
<string name="forward_success">"Shadowsocks가 시작되었습니다"</string>
<string name="invalid_server">"잘못된 서버 이름입니다"</string>
<string name="service_failed">"원격 서버에 접속하는 데 실패했습니다"</string>
<string name="nat_deprecated">"경고: NAT 모드는 Android 5.0부터 지원 중단되었습니다"</string>
<string name="nat_no_root">"NAT 모드는 루트 권한이 필요합니다"</string>
<string name="switch_to_vpn">"VPN 모드로 전환"</string>
<string name="stop">"중지"</string>
<string name="stopping">"종료 중…"</string>
<string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string>
<string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string>
<!-- alert category -->
<string name="close">"닫기"</string>
<string name="profile_empty">"프로필을 선택해 주세요"</string>
<string name="proxy_empty">"서버 주소와 비밀번호는 반드시 입력해야 합니다"</string>
<string name="connect">"접속"</string>
<string name="recovering">"재설정 중…"</string>
<string name="remove_profile">"'%s' 프로필을 삭제 하시겠습니까?"</string>
<!-- menu category -->
<string name="profiles">"프로필"</string>
<string name="settings">"설정"</string>
<string name="faq">"자주 묻는 질문"</string>
<string name="recovery">"재설정"</string>
<string name="about">"이 앱에 대하여"</string>
<string name="edit">"수정"</string>
<string name="share">"공유"</string>
<string name="add_profile">"프로필 추가"</string>
<string name="action_export">"클립보드로 내보내기"</string>
<string name="action_import">"클립보드에서 불러오기"</string>
<string name="action_export_msg">"성공적으로 내보냈습니다"</string>
<string name="action_export_err">"내보내기에 실패했습니다"</string>
<string name="action_import_msg">"성공적으로 불러왔습니다"</string>
<string name="action_import_err">"불러오는 데 실패했습니다"</string>
<!-- profile -->
<string name="profile_config">"프로필 설정"</string>
<string name="delete">"삭제"</string>
<string name="delete_confirm_prompt">"정말 이 프로필을 삭제하시겠습니까?"</string>
<string name="share_qr_nfc">"QR 코드/NFC"</string>
<string name="add_profile_dialog">"이 Shadowsocks 프로필을 추가하시겠습니까?"</string>
<string name="add_profile_methods_scan_qr_code">"QR 코드 읽기"</string>
<string name="add_profile_methods_manual_settings">"손수 써넣기"</string>
<plurals name="removed">
<item quantity="other">"삭제했습니다"</item>
</plurals>
<string name="undo">"실행 취소"</string>
<!-- status -->
<string name="sent">"송신:"</string>
<string name="received">"수신:"</string>
<string name="connecting">"연결 중…"</string>
<string name="vpn_connected">"연결 완료. 탭 하면 연결 상태를 검사합니다."</string>
<string name="nat_connected">"연결 완료"</string>
<!-- acl -->
<string name="custom_rules">"사용자 정의 규칙"</string>
<string name="acl_rule_templates_generic">"URL, 서브넷 혹은 호스트 이름 PCRE 패턴"</string>
<!-- plugin -->
<string name="plugin">"플러그인"</string>
<string name="plugin_configure">"플러그인 설정…"</string>
<string name="plugin_disabled">"비활성화 됨"</string>
<string name="plugin_unknown">"알 수 없는 플러그인: %s"</string>
<string name="plugin_untrusted">"경고: 이 플러그인은 신뢰할 수 있는 출처에서 온 것이 아닌 것 같습니다"</string>
<string name="profile_plugin">"플러그인: %s"</string>
<string name="add_profile_scanner_permission_required">"QR 코드를 읽어 들이려면 카메라 권한이 필요합니다"</string>
</resources>
+2 -3
View File
@@ -95,8 +95,6 @@
<string name="add_profile_dialog">"Добавить этот Профиль Shadowsocks?"</string>
<string name="add_profile_methods_scan_qr_code">"Сканировать QR-код"</string>
<string name="add_profile_methods_manual_settings">"Ручные настройки"</string>
<string name="add_profile_scanner_not_installed">"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."</string>
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
<plurals name="removed">
<item quantity="one">"Удалено"</item>
<item quantity="few">"Удалено %d элемента"</item>
@@ -141,4 +139,5 @@
<string name="plugin_unknown">"Неизвестный плагин %s"</string>
<string name="plugin_untrusted">"Предупреждение: этот плагин получен из недоверенного источника."</string>
<string name="profile_plugin">"Плагин: %s"</string>
</resources>
<string name="add_profile_scanner_permission_required">"Разрешение камеры требуется для сканирования QR код."</string>
</resources>
+12 -7
View File
@@ -3,8 +3,7 @@
<string name="app_name">"影梭"</string>
<string name="quick_toggle">"开关"</string>
<!-- global/misc category -->
<string name="misc_cat">"其他"</string>
<!-- misc -->
<string name="profile">"配置文件"</string>
<string name="profile_summary">"新建或切换到其他配置文件"</string>
<string name="nat">"NAT 模式(仅限调试)"</string>
@@ -12,6 +11,9 @@
<string name="remote_dns">"远程 DNS"</string>
<string name="stat_summary">"发送: \t%3$s\t↑\t%1$s/s
接收: \t%4$s\t↓\t%2$s/s"</string>
<plurals name="bytes">
<item quantity="other">"字节"</item>
</plurals>
<string name="connection_test_pending">"检查网络连接"</string>
<string name="connection_test_testing">"测试中…"</string>
<string name="connection_test_available">"连接有效:延时 %d 毫秒"</string>
@@ -63,7 +65,7 @@
<string name="proxy_empty">"代理服务器地址及密码不能为空"</string>
<string name="connect">"连接"</string>
<string name="recovering">"重置中…"</string>
<string name="remove_profile">"删除此配置文件“%s”?"</string>
<string name="remove_profile">"删除此配置文件“%s”?"</string>
<!-- menu category -->
<string name="profiles">"配置文件"</string>
@@ -71,7 +73,7 @@
<string name="faq">"常见问题"</string>
<string name="recovery">"重置"</string>
<string name="about">"关于"</string>
<string name="about_title">"影梭 (shadowsocks) %s"</string>
<string name="about_title">"影梭 (Shadowsocks) %s"</string>
<string name="edit">"编辑"</string>
<string name="share">"分享"</string>
<string name="add_profile">"添加配置文件"</string>
@@ -91,8 +93,6 @@
<string name="add_profile_dialog">"为影梭添加此配置文件?"</string>
<string name="add_profile_methods_scan_qr_code">"扫描二维码"</string>
<string name="add_profile_methods_manual_settings">"手动设置"</string>
<string name="add_profile_scanner_not_installed">"请安装任意兼容 ZXing 的二维码扫描应用。"</string>
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
<plurals name="removed">
<item quantity="other">"已删除 %d 项"</item>
</plurals>
@@ -134,4 +134,9 @@
<string name="plugin_unknown">"未知插件 %s"</string>
<string name="plugin_untrusted">"警告:该插件似乎并非来自已知的可信源。"</string>
<string name="profile_plugin">"插件:%s"</string>
</resources>
<string name="add_profile_scanner_permission_required">"扫描二维码需要获得使用相机的权限。"</string>
<!-- notification category -->
<string name="service_vpn">"VPN 服务"</string>
<string name="service_nat">"NAT 服务"</string>
</resources>
+11 -8
View File
@@ -89,8 +89,6 @@
<string name="add_profile_dialog">"為 Shadowsocks 新增此設定檔?"</string>
<string name="add_profile_methods_scan_qr_code">"掃描 QR 碼"</string>
<string name="add_profile_methods_manual_settings">"手動設定"</string>
<string name="add_profile_scanner_not_installed">"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"</string>
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
<plurals name="removed">
<item quantity="other">"已移除 %d 項"</item>
</plurals>
@@ -126,10 +124,15 @@
<string name="acl_rule_templates_domain">"網域及其所有子網域"</string>
<!-- plugin -->
<string name="plugin">"插件"</string>
<string name="plugin_configure">"配置..."</string>
<string name="plugin_disabled">"未啟用"</string>
<string name="plugin">"外掛程式"</string>
<string name="plugin_configure">"設定..."</string>
<string name="plugin_disabled">"停用"</string>
<string name="plugin_unknown">"未知插件 %s"</string>
<string name="plugin_untrusted">"警告:該插件似乎不是來自一個已知的可信任源。"</string>
<string name="profile_plugin">"插件: %s"</string>
</resources>
<string name="plugin_untrusted">"警告:此外掛程式似乎不是來自一個已知的受信任來源。"</string>
<string name="profile_plugin">"外掛程式: %s"</string>
<string name="add_profile_scanner_permission_required">"掃描 QR 碼需要相機權限。"</string>
<!-- notification category -->
<string name="service_vpn">"VPN 服務"</string>
<string name="service_nat">"NAT 服務"</string>
</resources>
+1
View File
@@ -224,5 +224,6 @@
<string-array name="acl_rule_templates">
<item>@string/acl_rule_templates_generic</item>
<item>@string/acl_rule_templates_domain</item>
<item>URL</item>
</string-array>
</resources>
+1 -1
View File
@@ -132,7 +132,7 @@
<string name="custom_rules">Custom rules</string>
<string name="action_selection">Selection…</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="edit_rule">Edit rule</string>
@@ -150,8 +150,7 @@ trait BaseService extends Service {
profile.method = proxy(3).trim
}
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES), true)
if (profile.route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
@@ -374,7 +373,7 @@ trait BaseService extends Service {
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", "gfwlist.txt")
case Acl.CHINALIST => config
.put("PrimaryDNS", new JSONArray().put(makeDns("Primary", "119.29.29.29")))
.put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
case _ => config
.put("PrimaryDNS", remoteDns)
@@ -385,6 +385,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
displayFragment(new AboutFragment)
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
case DRAWER_CUSTOM_RULES => displayFragment(new CustomRulesFragment)
case _ => // Ignore
}
true // unexpected cases will throw exception
}
@@ -63,7 +63,10 @@ class ShadowsocksNatService extends BaseService {
if (profile.route != Acl.ALL) {
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()
@@ -161,7 +161,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL) {
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"
@@ -1,7 +1,9 @@
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.utils.IOUtils
import com.j256.ormlite.field.DatabaseField
@@ -17,16 +19,38 @@ import scala.io.Source
* @author Mygod
*/
class Acl {
import Acl._
@DatabaseField(generatedId = true)
var id: Int = _
val bypassHostnames = new mutable.SortedList[String]()
val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
val urls = new mutable.SortedList[String]()
val urls = new mutable.SortedList[URL]()(urlOrdering)
@DatabaseField
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 = {
bypassHostnames.clear()
@@ -40,30 +64,24 @@ class Acl {
bypass = other.bypass
this
}
def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
bypassHostnames.clear()
proxyHostnames.clear()
this.subnets.clear()
this.urls.clear()
urls.clear()
bypass = defaultBypass
lazy val bypassSubnets = new mutable.SortedList[Subnet]()
lazy val proxySubnets = new mutable.SortedList[Subnet]()
var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
var in_urls = false
for (line <- value.getLines()) (line.trim.indexOf('#') match {
case 0 => {
line.indexOf("NETWORK_ACL_BEGIN") match {
case -1 =>
case index => in_urls = true
}
line.indexOf("NETWORK_ACL_END") match {
case -1 =>
case index => in_urls = false
}
"" // ignore any comment lines
}
case index => if (!in_urls) line else ""
for (line <- value.getLines()) (line.indexOf('#') match {
case -1 => line
case index =>
line.substring(index + 1) match {
case networkAclParser(url) => urls.add(new URL(url))
case _ => // ignore
}
line.substring(0, index) // trim comments
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
@@ -77,11 +95,7 @@ class Acl {
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 _: IllegalArgumentException => if (isUrl(input)) {
urls += input
} else {
hostnames += input
}
case _: IllegalArgumentException => hostnames += input
}
case _ =>
}
@@ -90,24 +104,26 @@ class Acl {
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
def getAclString(network: Boolean): String = {
val result = new StringBuilder()
if (urls.nonEmpty) {
result.append(urls.mkString("\n"))
result.append("\n")
if (network) {
result.append("#NETWORK_ACL_BEGIN\n")
try {
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
} catch {
case e: IOException => // ignore
}
result.append("#NETWORK_ACL_END\n")
def flatten(depth: Int): Acl = {
if (depth > 0) for (url <- urls) {
val child = new Acl().fromSource(Source.fromURL(url), 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: %s"
.format(url))
child.subnets.clear() // subnets for the different mode are discarded
child.bypass = bypass
}
bypassHostnames ++= child.bypassHostnames
proxyHostnames ++= child.proxyHostnames
subnets ++= child.subnets
}
if (result.isEmpty) {
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
}
urls.clear()
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)
@@ -121,15 +137,10 @@ class Acl {
result.append(proxyList.mkString("\n"))
result.append('\n')
}
result.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
result.toString
}
override def toString: String = {
getAclString(false)
}
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
// Don't change: dummy fields for OrmLite interaction
// noinspection ScalaUnusedSymbol
@@ -144,6 +155,7 @@ class Acl {
}
object Acl {
final val TAG = "Acl"
final val ALL = "all"
final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china"
@@ -151,6 +163,10 @@ object Acl {
final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list"
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 customRules: Acl = {
@@ -158,9 +174,9 @@ object Acl {
try acl.fromId(CUSTOM_RULES) catch {
case _: FileNotFoundException =>
}
acl.bypass = true
acl.bypassHostnames.clear() // everything is bypassed
acl
}
def save(id: String, acl: Acl, network: Boolean = false): Unit = {
IOUtils.writeString(getFile(id), acl.getAclString(network))
}
def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString)
}
@@ -1,6 +1,6 @@
package com.github.shadowsocks.acl
import java.net.IDN
import java.net.{IDN, URL}
import java.util.Locale
import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
@@ -26,13 +26,15 @@ import scala.io.Source
* @author Mygod
*/
object CustomRulesFragment {
private final val GENERIC = 0
private final val DOMAIN = 1
private final val TEMPLATE_GENERIC = 0
private final val TEMPLATE_DOMAIN = 1
private final val TEMPLATE_URL = 2
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_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 {
@@ -44,18 +46,26 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case _ => false
}
private def createAclRuleDialog(text: CharSequence = "") = {
private def createAclRuleDialog(item: AnyRef = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById[EditText](R.id.content)
PATTERN_DOMAIN.findFirstMatchIn(text) match {
case Some(m) =>
templateSelector.setSelection(DOMAIN)
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
case None =>
templateSelector.setSelection(GENERIC)
editText.setText(text)
item match {
case hostname: String => PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) =>
templateSelector.setSelection(TEMPLATE_DOMAIN)
editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
case None =>
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)
.setTitle(R.string.edit_rule)
@@ -85,9 +95,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
text.setText(subnet.toString)
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 {
val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString)
val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog
.setNeutralButton(R.string.delete, ((_, _) => {
adapter.remove(item)
@@ -127,32 +142,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.size
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
val j = i - acl.urls.size
val k = i - acl.subnets.size - acl.urls.size
if (j < 0)
vh.bind(acl.urls(i))
else if (k < 0)
vh.bind(acl.subnets(j))
else
vh.bind(acl.proxyHostnames(k))
val j = i - acl.subnets.size
if (j < 0) vh.bind(acl.subnets(i)) else {
val k = j - acl.proxyHostnames.size
if (k < 0) vh.bind(acl.proxyHostnames(j)) else vh.bind(acl.urls(k))
}
}
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
.inflate(android.R.layout.simple_list_item_1, vg, false))
def addUrl(url: String): Int = if (acl.urls.add(url)) {
val index = acl.urls.indexOf(url)
notifyItemInserted(index)
apply()
index
} else -1
def addSubnet(subnet: Subnet): Int = if (acl.subnets.add(subnet)) {
val index = acl.subnets.indexOf(subnet) + acl.urls.size
val index = acl.subnets.indexOf(subnet)
notifyItemInserted(index)
apply()
index
} else -1
def addHostname(hostname: String): Int = if (acl.proxyHostnames.add(hostname)) {
val index = acl.proxyHostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size
val index = acl.proxyHostnames.indexOf(hostname) + acl.subnets.size
notifyItemInserted(index)
apply()
index
} else -1
def addURL(url: URL): Int = if (acl.urls.add(url)) {
val index = acl.urls.indexOf(url) + acl.subnets.size + acl.proxyHostnames.size
notifyItemInserted(index)
apply()
index
@@ -160,53 +172,54 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
def addToProxy(input: String): Int = {
val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
var result = -1
for (url <- acl.urls) result = addUrl(url)
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
for (url <- acl.urls) result = addURL(url)
result
}
def addFromTemplate(template: Int, text: CharSequence): Int = template match {
case GENERIC => addToProxy(text.toString)
case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH,
case TEMPLATE_GENERIC => addToProxy(text.toString)
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 {
case exc: IllegalArgumentException =>
Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
-1
}
case TEMPLATE_URL => addURL(new URL(text.toString))
case _ => -1
}
def remove(i: Int) {
val j = i - acl.urls.size
val k = i - acl.urls.size - acl.subnets.size
val j = i - acl.subnets.size
if (j < 0) {
undoManager.remove((i, acl.urls(i)))
acl.urls.remove(i)
} else if (k < 0) {
undoManager.remove((i, acl.subnets(j)))
acl.subnets.remove(j)
undoManager.remove((i, acl.subnets(i)))
acl.subnets.remove(i)
} else {
undoManager.remove((i, acl.proxyHostnames(k)))
acl.proxyHostnames.remove(k)
val k = j - acl.proxyHostnames.size
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)
apply()
}
def remove(item: AnyRef): Unit = item match {
case subnet: Subnet =>
notifyItemRemoved(acl.subnets.indexOf(subnet) + acl.urls.size)
notifyItemRemoved(acl.subnets.indexOf(subnet))
acl.subnets.remove(subnet)
apply()
case hostname: String => if (acl.isUrl(hostname)) {
notifyItemRemoved(acl.urls.indexOf(hostname))
acl.urls.remove(hostname)
apply()
} else {
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname)
+ acl.urls.size + acl.subnets.size)
case hostname: String =>
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
acl.proxyHostnames.remove(hostname)
apply()
}
case url: URL =>
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
acl.urls.remove(url)
apply()
}
def removeSelected() {
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
@@ -215,19 +228,16 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
onSelectedItemsUpdated()
}
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.proxyHostnames.insert(hostname)) {
notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size)
apply()
}
}
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()
}
}
@@ -236,6 +246,7 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
selectedItems.clear()
selectedItems ++= acl.subnets
selectedItems ++= acl.proxyHostnames
selectedItems ++= acl.urls
onSelectedItemsUpdated()
notifyDataSetChanged()
}
@@ -244,11 +255,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
val j = i - acl.subnets.size
try {
(if (j < 0) acl.subnets(i).address.getHostAddress.substring(0, 1) else {
val hostname = acl.proxyHostnames(i)
PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
case None => hostname
}
val k = j - acl.proxyHostnames.size
if (k < 0) {
val hostname = acl.proxyHostnames(j)
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)
} catch {
case _: IndexOutOfBoundsException => " "
@@ -275,6 +289,10 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case null =>
case arr => selectedItems ++= arr
}
savedInstanceState.getStringArray(SELECTED_URLS) match {
case null =>
case arr => selectedItems ++= arr.map(new URL(_))
}
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
@@ -307,18 +325,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
super.onSaveInstanceState(outState)
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_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 {
case R.id.action_select_all =>
adapter.selectAll()
true
case R.id.action_cut =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
copySelected()
adapter.removeSelected()
true
case R.id.action_copy =>
clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
copySelected()
true
case R.id.action_delete =>
adapter.removeSelected()
@@ -332,15 +361,18 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
.create().show()
true
case R.id.action_import =>
try if (adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1) return true catch {
case _: Exception =>
try adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1 catch {
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
case R.id.action_import_gfwlist =>
val acl = new Acl().fromId(Acl.GFWLIST)
acl.subnets.foreach(adapter.addSubnet)
acl.proxyHostnames.foreach(adapter.addHostname)
acl.urls.foreach(adapter.addURL)
true
case _ => false
}
@@ -141,7 +141,9 @@ class DBHelper(val context: Context)
}
if (oldVersion < 23) {
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
profileDao.executeRawNoArgs("COMMIT;")
import KeyValuePair._
val old = PreferenceManager.getDefaultSharedPreferences(app)
kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- proxy category -->
<string name="proxy_cat">"تنظیمات سرور"</string>
<!-- alert category -->
<string name="yes">"بله"</string>
<string name="no">"خیر"</string>
<string name="apply">"تایید"</string>
</resources>
@@ -6,4 +6,10 @@
<!-- feature category -->
<string name="feature_cat">"기능 설정"</string>
<!-- alert category -->
<string name="yes">"예"</string>
<string name="no">"아니오"</string>
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
<string name="apply">"적용"</string>
</resources>
+1
View File
@@ -0,0 +1 @@
sbt.version=0.13.16
+1 -1
View File
@@ -1,4 +1,4 @@
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.8")
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.9")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")