Skip to content

Commit 06007d8

Browse files
committed
New Subscription URL support (Clash and Shadowrocket)
1 parent 575239b commit 06007d8

File tree

4 files changed

+129
-73
lines changed

4 files changed

+129
-73
lines changed

README.md

+3-12
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ Serve pac js file as a file url. So only some souce code related to GUI left.
1919

2020
### Building
2121

22-
- Xcode 12.5+
22+
- Xcode 12.0+
2323

2424
## Features
2525

26+
- New Subscription URL support (Clash and Shadowrocket)
2627
- Show up/down speed
2728
- Limited SSR support
2829
- White domain list & white IP list
@@ -31,19 +32,9 @@ Serve pac js file as a file url. So only some souce code related to GUI left.
3132
- Auto update ACL white list from GutHub. (You can even customize your list)
3233
- Show QRCode for current server profile
3334
- Scan QRCode from screen
34-
- Import config.json to config all your servers (SSR-C# password protect not supported yet)
35+
- Import config.json to config all your servers
3536
- Auto launch at login
3637
- User rules for PAC
37-
- An advance preferences panel to configure:
38-
- Local socks5 listen address
39-
- Local socks5 listen port
40-
- Local socks5 timeout
41-
- If enable UDP relay
42-
- GFW List URL
43-
- ACL White List URL
44-
- ACL GFW list and proxy bach CHN list
45-
- Manual spesify network service profiles which would be configure the proxy
46-
- Could reorder shadowsocks profiles by drag & drop in servers preferences panel
4738

4839
## Differences from original ShadowsocksX
4940

ShadowsocksX-NG.xcodeproj/project.pbxproj

+17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
0958DA7826483A8C00AF66D5 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 0958DA7726483A8C00AF66D5 /* ArgumentParser */; };
3232
09666027261D4BC10022C66C /* privoxy in Resources */ = {isa = PBXBuildFile; fileRef = C6D4298F1DA75988002A5711 /* privoxy */; };
3333
0966602E261D51580022C66C /* libcrypto.1.0.0.dylib in Resources */ = {isa = PBXBuildFile; fileRef = 09F9DCD1261CC452006CA4B9 /* libcrypto.1.0.0.dylib */; };
34+
0981DECA268CCA7600A39589 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 0981DEC9268CCA7600A39589 /* Yams */; };
3435
0984E978263AFDF400A79681 /* SWBQRCodeWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0984E97A263AFDF400A79681 /* SWBQRCodeWindowController.xib */; };
3536
099C07EE26249E6300D6FD67 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 099C07ED26249E6300D6FD67 /* Alamofire */; };
3637
099F9844264ED10A00CC117B /* GCDWebServers in Frameworks */ = {isa = PBXBuildFile; productRef = 099F9843264ED10A00CC117B /* GCDWebServers */; };
@@ -213,6 +214,7 @@
213214
buildActionMask = 2147483647;
214215
files = (
215216
099F9844264ED10A00CC117B /* GCDWebServers in Frameworks */,
217+
0981DECA268CCA7600A39589 /* Yams in Frameworks */,
216218
094A892F26248D4C00394747 /* LaunchAtLogin in Frameworks */,
217219
099C07EE26249E6300D6FD67 /* Alamofire in Frameworks */,
218220
9B3FFF3E1D08D9910019A709 /* SystemConfiguration.framework in Frameworks */,
@@ -397,6 +399,7 @@
397399
094A892E26248D4C00394747 /* LaunchAtLogin */,
398400
099C07ED26249E6300D6FD67 /* Alamofire */,
399401
099F9843264ED10A00CC117B /* GCDWebServers */,
402+
0981DEC9268CCA7600A39589 /* Yams */,
400403
);
401404
productName = "ShadowsocksX-NG";
402405
productReference = 9B0BFFE51D0460A70040E62B /* ShadowsocksX-NG-R.app */;
@@ -486,6 +489,7 @@
486489
099C07EC26249E6300D6FD67 /* XCRemoteSwiftPackageReference "Alamofire" */,
487490
0958DA6B2648123C00AF66D5 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
488491
099F9842264ED10A00CC117B /* XCRemoteSwiftPackageReference "GCDWebServer" */,
492+
0981DEC8268CCA7600A39589 /* XCRemoteSwiftPackageReference "Yams" */,
489493
);
490494
productRefGroup = 9B0BFFE61D0460A70040E62B /* Products */;
491495
projectDirPath = "";
@@ -1053,6 +1057,14 @@
10531057
minimumVersion = 0.4.3;
10541058
};
10551059
};
1060+
0981DEC8268CCA7600A39589 /* XCRemoteSwiftPackageReference "Yams" */ = {
1061+
isa = XCRemoteSwiftPackageReference;
1062+
repositoryURL = "https://github.com/jpsim/Yams";
1063+
requirement = {
1064+
kind = upToNextMajorVersion;
1065+
minimumVersion = 4.0.6;
1066+
};
1067+
};
10561068
099C07EC26249E6300D6FD67 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
10571069
isa = XCRemoteSwiftPackageReference;
10581070
repositoryURL = "https://github.com/Alamofire/Alamofire";
@@ -1082,6 +1094,11 @@
10821094
package = 0958DA6B2648123C00AF66D5 /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
10831095
productName = ArgumentParser;
10841096
};
1097+
0981DEC9268CCA7600A39589 /* Yams */ = {
1098+
isa = XCSwiftPackageProductDependency;
1099+
package = 0981DEC8268CCA7600A39589 /* XCRemoteSwiftPackageReference "Yams" */;
1100+
productName = Yams;
1101+
};
10851102
099C07ED26249E6300D6FD67 /* Alamofire */ = {
10861103
isa = XCSwiftPackageProductDependency;
10871104
package = 099C07EC26249E6300D6FD67 /* XCRemoteSwiftPackageReference "Alamofire" */;

ShadowsocksX-NG/ServerProfile.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class ServerProfile: NSObject {
3333
self.uuid = uuid
3434
}
3535

36-
static func fromDictionary(_ data: [String: AnyObject]) -> ServerProfile {
36+
static func fromDictionary(_ data: [String: Any]) -> ServerProfile {
3737
let cp = {
3838
(profile: ServerProfile) in
3939
profile.serverHost = data["ServerHost"] as! String

ShadowsocksX-NG/Subscribe.swift

+108-60
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,52 @@
88

99
import Cocoa
1010
import Alamofire
11+
import Yams
12+
13+
class Subscribe: NSObject {
1114

12-
class Subscribe: NSObject{
13-
1415
@objc var subscribeFeed = ""
1516
var isActive = true
1617
@objc var maxCount = 0 // -1 is not limited
1718
@objc var groupName = ""
1819
@objc var token = ""
1920
var cache = ""
20-
21+
2122
var profileMgr: ServerProfileManager!
22-
23-
init(initUrlString:String, initGroupName: String, initToken: String, initMaxCount: Int){
23+
24+
init(initUrlString: String, initGroupName: String, initToken: String, initMaxCount: Int) {
2425
super.init()
2526
subscribeFeed = initUrlString
2627

2728
token = initToken
28-
29+
2930
setMaxCount(initMaxCount: initMaxCount)
3031
setGroupName(newGroupName: initGroupName)
3132
profileMgr = ServerProfileManager.instance
3233
}
33-
func getFeed() -> String{
34+
func getFeed() -> String {
3435
return subscribeFeed
3536
}
36-
func setFeed(newFeed: String){
37+
func setFeed(newFeed: String) {
3738
subscribeFeed = newFeed
3839
}
39-
func diactivateSubscribe(){
40+
func diactivateSubscribe() {
4041
isActive = false
4142
}
42-
func activateSubscribe(){
43+
func activateSubscribe() {
4344
isActive = true
4445
}
4546
func setGroupName(newGroupName: String) {
4647
func getGroupNameFromRes(resString: String) {
4748
let decodeRes = decode64(resString)!
4849
let ssrregexp = "ssr://([A-Za-z0-9_-]+)"
4950
let urls = splitor(url: decodeRes, regexp: ssrregexp)
50-
let profile = ServerProfile.fromDictionary(parseAppURLSchemes(URL(string: urls[0]))! as [String : AnyObject])
51+
let profile = ServerProfile.fromDictionary(parseAppURLSchemes(URL(string: urls[0]))! as [String: AnyObject])
5152
self.groupName = profile.ssrGroup
5253
}
5354
if newGroupName != "" { return groupName = newGroupName }
5455
if self.cache != "" { return getGroupNameFromRes(resString: cache) }
55-
sendRequest(url: self.subscribeFeed, options: "", callback: { resString in
56+
sendRequest(url: self.subscribeFeed, options: "", callback: { resString, contentType in
5657
if resString == "" { return self.groupName = "New Subscribe" }
5758
getGroupNameFromRes(resString: resString)
5859
self.cache = resString
@@ -64,11 +65,11 @@ class Subscribe: NSObject{
6465
func getMaxCount() -> Int {
6566
return maxCount
6667
}
67-
static func fromDictionary(_ data:[String:AnyObject]) -> Subscribe {
68-
var feed:String = ""
69-
var group:String = ""
70-
var token:String = ""
71-
var maxCount:Int = -1
68+
static func fromDictionary(_ data: [String: AnyObject]) -> Subscribe {
69+
var feed: String = ""
70+
var group: String = ""
71+
var token: String = ""
72+
var maxCount: Int = -1
7273
for (key, value) in data {
7374
switch key {
7475
case "feed":
@@ -86,33 +87,32 @@ class Subscribe: NSObject{
8687
return Subscribe.init(initUrlString: feed, initGroupName: group, initToken: token, initMaxCount: maxCount)
8788
}
8889
static func toDictionary(_ data: Subscribe) -> [String: AnyObject] {
89-
var ret : [String: AnyObject] = [:]
90+
var ret: [String: AnyObject] = [:]
9091
ret["feed"] = data.subscribeFeed as AnyObject
9192
ret["group"] = data.groupName as AnyObject
9293
ret["token"] = data.token as AnyObject
9394
ret["maxCount"] = data.maxCount as AnyObject
9495
return ret
9596
}
96-
fileprivate func sendRequest(url: String, options: Any, callback: @escaping (String) -> Void) {
97+
fileprivate func sendRequest(url: String, options: Any, callback: @escaping (String, String) -> Void) {
9798
let headers: HTTPHeaders = [
98-
// "Authorization": "Basic U2hhZG93c29ja1gtTkctUg==",
99-
// "Accept": "application/json",
10099
"token": self.token,
101100
"User-Agent": "ShadowsocksX-NG-R " + (getLocalInfo()["CFBundleShortVersionString"] as! String)
102101
]
103-
102+
104103
AF.request(url, headers: headers)
105-
.responseString{ response in
106-
switch response.result {
107-
case .success:
108-
callback(response.value!)
109-
case .failure( _):
110-
callback("")
111-
self.pushNotification(title: "请求失败", subtitle: "", info: "发送到\(url)的请求失败,请检查您的网络")
112-
}
104+
.responseString { response in
105+
switch response.result {
106+
case .success:
107+
let contentType = response.response?.allHeaderFields["Content-Type"] as? String ;
108+
callback(response.value!, contentType!)
109+
case .failure(_):
110+
callback("", "")
111+
self.pushNotification(title: "请求失败", subtitle: "", info: "发送到\(url)的请求失败,请检查您的网络")
112+
}
113113
}
114114
}
115-
func setMaxCount(initMaxCount:Int) {
115+
func setMaxCount(initMaxCount: Int) {
116116
func getMaxFromRes(resString: String) {
117117
let maxCountReg = "MAX=[0-9]+"
118118
let decodeRes = decode64(resString)!
@@ -121,71 +121,102 @@ class Subscribe: NSObject{
121121
let result = String(decodeRes[range!])
122122
self.maxCount = Int(result.replacingOccurrences(of: "MAX=", with: ""))!
123123
}
124-
else{
124+
else {
125125
self.maxCount = -1
126126
}
127127
}
128128
if initMaxCount != 0 { return self.maxCount = initMaxCount }
129129
if cache != "" { return getMaxFromRes(resString: cache) }
130-
sendRequest(url: self.subscribeFeed, options: "", callback: { resString in
130+
sendRequest(url: self.subscribeFeed, options: "", callback: { resString, contentType in
131131
if resString == "" { return }// Also should hold if token is wrong feedback
132132
getMaxFromRes(resString: resString)
133133
self.cache = resString
134134
})
135135
}
136-
func updateServerFromFeed(){
137-
func updateServerHandler(resString: String) {
138-
let decodeRes = decode64(resString)!
139-
let ssrregexp = "ssr://([A-Za-z0-9_-]+)"
140-
let urls = splitor(url: decodeRes, regexp: ssrregexp)
141-
// hold if user fill a maxCount larger then server return
142-
// Should push a notification about it and correct the user filled maxCOunt?
143-
let maxN = (self.maxCount > urls.count) ? urls.count : (self.maxCount == -1) ? urls.count: self.maxCount
144-
// TODO change the loop into random pick
136+
func updateServerFromFeed() {
137+
func updateServerHandler(resString: String, _ contentType: String) {
138+
if(!contentType.contains("application/octet-stream") && !contentType.contains("application/yaml")) {
139+
NSLog("unsupport content type")
140+
return
141+
}
142+
143+
var proxiesOrUrls: [Any] = []
144+
if(contentType.contains("application/octet-stream")) {
145+
let decodeRes = decode64(resString)!
146+
if decodeRes.hasPrefix("ss://") {
147+
NSLog("unsupport ss type")
148+
} else if decodeRes.hasPrefix("ssr://") {
149+
proxiesOrUrls = splitor(url: decodeRes, regexp: "ssr://([A-Za-z0-9_-]+)")
150+
}
151+
} else if(contentType.contains("application/yaml")) {
152+
do {
153+
proxiesOrUrls = try YAMLDecoder().decode(YamlContent.self, from: resString).proxies
154+
} catch {
155+
NSLog("parse yaml failed")
156+
}
157+
}
158+
159+
let maxN = (self.maxCount > proxiesOrUrls.count) ? proxiesOrUrls.count : (self.maxCount == -1) ? proxiesOrUrls.count : self.maxCount
160+
161+
var profileDict: [String: Any] = [:]
145162
for index in 0..<maxN {
146-
if let profileDict = parseAppURLSchemes(URL(string: urls[index])) {
147-
let profile = ServerProfile.fromDictionary(profileDict as [String : AnyObject])
148-
let (dupResult, _) = self.profileMgr.isDuplicated(profile: profile)
149-
let (existResult, existIndex) = self.profileMgr.isExisted(profile: profile)
150-
if dupResult {
151-
continue
163+
if(contentType.contains("application/octet-stream")) {
164+
let tempProfileDict = parseAppURLSchemes(URL(string: proxiesOrUrls[index] as! String))
165+
if(tempProfileDict == nil) {
166+
return
152167
}
153-
if existResult {
154-
self.profileMgr.profiles.replaceSubrange(existIndex..<existIndex + 1, with: [profile])
155-
continue
168+
169+
profileDict = tempProfileDict! as [String: Any]
170+
} else if(contentType.contains("application/yaml")) {
171+
let proxy = proxiesOrUrls[index] as! Proxy
172+
if(proxy.type != "ss") {
173+
NSLog("proxy type not ss")
156174
}
157-
self.profileMgr.profiles.append(profile)
175+
profileDict = ["ServerHost": proxy.server, "ServerPort": proxy.port, "Method": proxy.cipher, "Password": proxy.password, "Remark": proxy.name]
176+
}
177+
178+
let profile = ServerProfile.fromDictionary(profileDict)
179+
let (dupResult, _) = self.profileMgr.isDuplicated(profile: profile)
180+
let (existResult, existIndex) = self.profileMgr.isExisted(profile: profile)
181+
if dupResult {
182+
continue
183+
}
184+
if existResult {
185+
self.profileMgr.profiles.replaceSubrange(existIndex..<existIndex + 1, with: [profile])
186+
continue
158187
}
188+
self.profileMgr.profiles.append(profile)
159189
}
190+
160191
self.profileMgr.save()
161192
pushNotification(title: "成功更新订阅", subtitle: "", info: "更新来自\(subscribeFeed)的订阅")
162193
(NSApplication.shared.delegate as! AppDelegate).updateServersMenu()
163194
(NSApplication.shared.delegate as! AppDelegate).updateRunningModeMenu()
164195
}
165-
166-
if (!isActive){ return }
167196

168-
sendRequest(url: self.subscribeFeed, options: "", callback: { resString in
197+
if (!isActive) { return }
198+
199+
sendRequest(url: self.subscribeFeed, options: "", callback: { resString, contentType in
169200
if resString == "" { return }
170-
updateServerHandler(resString: resString)
201+
updateServerHandler(resString: resString, contentType)
171202
self.cache = resString
172203
})
173204
}
174-
func feedValidator() -> Bool{
205+
func feedValidator() -> Bool {
175206
// is the right format
176207
// should be http or https reg
177208
// but we should not support http only feed
178209
// TODO refine the regular expression
179210
let feedRegExp = "http[s]?://[A-Za-z0-9-_/.=?]*"
180-
return subscribeFeed.range(of:feedRegExp, options: .regularExpression) != nil
211+
return subscribeFeed.range(of: feedRegExp, options: .regularExpression) != nil
181212
}
182-
fileprivate func pushNotification(title: String, subtitle: String, info: String){
213+
fileprivate func pushNotification(title: String, subtitle: String, info: String) {
183214
let userNote = NSUserNotification()
184215
userNote.title = title
185216
userNote.subtitle = subtitle
186217
userNote.informativeText = info
187218
userNote.soundName = NSUserNotificationDefaultSoundName
188-
219+
189220
NSUserNotificationCenter.default
190221
.deliver(userNote);
191222
}
@@ -195,4 +226,21 @@ class Subscribe: NSObject{
195226
func isExist(_ target: Subscribe) -> Bool {
196227
return self.subscribeFeed == target.subscribeFeed
197228
}
229+
230+
//proxies:
231+
//- {"name":"🇭🇰 Hong Kong 01","type":"ss","server":"hk.it.dev","port":1344,"cipher":"chacha20","password":"xoW","udp":true}
232+
//- {"name":"🇭🇰 Hong Kong 02","type":"ss","server":"hk.kit.dev","port":1044,"cipher":"chacha20","password":"xoW","udp":true}
233+
struct YamlContent: Codable {
234+
var proxies: [Proxy]
235+
}
236+
237+
struct Proxy: Codable {
238+
var name: String
239+
var type: String
240+
var server: String
241+
var port: Int
242+
var cipher: String
243+
var password: String
244+
var udp: Bool
245+
}
198246
}

0 commit comments

Comments
 (0)