Merge pull request #434 from timothyqiu/qr-code

Fixes #433 Implements SIP002 QR code
This commit is contained in:
Qiu Yuzhou
2017-08-26 00:52:03 +08:00
committed by GitHub
6 changed files with 121 additions and 27 deletions

View File

@ -265,6 +265,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
} }
qrcodeWinCtrl = SWBQRCodeWindowController(windowNibName: "SWBQRCodeWindowController") qrcodeWinCtrl = SWBQRCodeWindowController(windowNibName: "SWBQRCodeWindowController")
qrcodeWinCtrl.qrCode = profile.URL()!.absoluteString qrcodeWinCtrl.qrCode = profile.URL()!.absoluteString
qrcodeWinCtrl.legacyQRCode = profile.URL(legacy: true)!.absoluteString
qrcodeWinCtrl.title = profile.title() qrcodeWinCtrl.title = profile.title()
qrcodeWinCtrl.showWindow(self) qrcodeWinCtrl.showWindow(self)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)

View File

@ -11,6 +11,7 @@
@interface SWBQRCodeWindowController : NSWindowController @interface SWBQRCodeWindowController : NSWindowController
@property (nonatomic, copy) NSString *legacyQRCode;
@property (nonatomic, copy) NSString *qrCode; @property (nonatomic, copy) NSString *qrCode;
@property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *title;

View File

@ -78,4 +78,13 @@
[pasteboard writeObjects:copiedObjects]; [pasteboard writeObjects:copiedObjects];
} }
- (void)flagsChanged:(NSEvent *)event {
NSUInteger modifiers = event.modifierFlags & NSDeviceIndependentModifierFlagsMask;
if (modifiers & NSAlternateKeyMask) {
[self setQRCode:self.legacyQRCode];
} else {
[self setQRCode:self.qrCode];
}
}
@end @end

View File

@ -31,7 +31,7 @@ class ServerProfile: NSObject, NSCopying {
self.uuid = uuid self.uuid = uuid
} }
convenience init?(url: URL?) { convenience init?(url: URL) {
self.init() self.init()
func padBase64(string: String) -> String { func padBase64(string: String) -> String {
@ -44,14 +44,12 @@ class ServerProfile: NSObject, NSCopying {
} }
} }
func decodeUrl(url: URL?) -> String? { func decodeUrl(url: URL) -> String? {
guard let urlStr = url?.absoluteString else { let urlStr = url.absoluteString
return nil
}
let index = urlStr.index(urlStr.startIndex, offsetBy: 5) let index = urlStr.index(urlStr.startIndex, offsetBy: 5)
let encodedStr = urlStr.substring(from: index) let encodedStr = urlStr.substring(from: index)
guard let data = Data(base64Encoded: padBase64(string: encodedStr)) else { guard let data = Data(base64Encoded: padBase64(string: encodedStr)) else {
return url?.absoluteString return url.absoluteString
} }
guard let decoded = String(data: data, encoding: String.Encoding.utf8) else { guard let decoded = String(data: data, encoding: String.Encoding.utf8) else {
return nil return nil
@ -67,17 +65,40 @@ class ServerProfile: NSObject, NSCopying {
return nil return nil
} }
guard let host = parsedUrl.host, let port = parsedUrl.port, guard let host = parsedUrl.host, let port = parsedUrl.port,
let method = parsedUrl.user, let password = parsedUrl.password else { let user = parsedUrl.user else {
return nil return nil
} }
self.serverHost = host self.serverHost = host
self.serverPort = UInt16(port) self.serverPort = UInt16(port)
self.method = method.lowercased()
self.password = password
// This can be overriden by the fragment part of SIP002 URL
remark = parsedUrl.queryItems? remark = parsedUrl.queryItems?
.filter({ $0.name == "Remark" }).first?.value ?? "" .filter({ $0.name == "Remark" }).first?.value ?? ""
if let password = parsedUrl.password {
self.method = user.lowercased()
self.password = password
} else {
// SIP002 URL have no password section
guard let data = Data(base64Encoded: padBase64(string: user)),
let userInfo = String(data: data, encoding: .utf8) else {
return nil
}
let parts = userInfo.characters.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
if parts.count != 2 {
return nil
}
self.method = String(parts[0]).lowercased()
self.password = String(parts[1])
// SIP002 defines where to put the profile name
if let profileName = parsedUrl.fragment {
self.remark = profileName
}
}
if let otaStr = parsedUrl.queryItems? if let otaStr = parsedUrl.queryItems?
.filter({ $0.name == "OTA" }).first?.value { .filter({ $0.name == "OTA" }).first?.value {
ota = NSString(string: otaStr).boolValue ota = NSString(string: otaStr).boolValue
@ -225,7 +246,7 @@ class ServerProfile: NSObject, NSCopying {
return true return true
} }
func URL() -> Foundation.URL? { private func makeLegacyURL() -> URL? {
var url = URLComponents() var url = URLComponents()
url.host = serverHost url.host = serverHost
@ -254,6 +275,39 @@ class ServerProfile: NSObject, NSCopying {
} }
return nil return nil
} }
func URL(legacy: Bool = false) -> URL? {
// If you want the URL from <= 1.5.1
if (legacy) {
return self.makeLegacyURL()
}
guard let rawUserInfo = "\(method):\(password)".data(using: .utf8) else {
return nil
}
let paddings = CharacterSet(charactersIn: "=")
let userInfo = rawUserInfo.base64EncodedString().trimmingCharacters(in: paddings)
var items = [URLQueryItem(name: "OTA", value: ota.description)]
if enabledKcptun {
items.append(URLQueryItem(name: "Kcptun", value: enabledKcptun.description))
items.append(contentsOf: kcptunProfile.urlQueryItems())
}
var comps = URLComponents()
comps.scheme = "ss"
comps.host = serverHost
comps.port = Int(serverPort)
comps.user = userInfo
comps.path = "/" // This is required by SIP0002 for URLs with fragment or query
comps.fragment = remark
comps.queryItems = items
let url = try? comps.asURL()
return url
}
func title() -> String { func title() -> String {
if remark.isEmpty { if remark.isEmpty {

View File

@ -56,7 +56,10 @@ void ScanQRCodeOnScreen() {
NSLog(@"%@", feature.messageString); NSLog(@"%@", feature.messageString);
if ( [feature.messageString hasPrefix:@"ss://"] ) if ( [feature.messageString hasPrefix:@"ss://"] )
{ {
[foundSSUrls addObject:[NSURL URLWithString:feature.messageString]]; NSURL *url = [NSURL URLWithString:feature.messageString];
if (url) {
[foundSSUrls addObject:url];
}
} }
} }
CGImageRelease(image); CGImageRelease(image);

View File

@ -31,7 +31,7 @@ class ServerProfileTests: XCTestCase {
} }
func testInitWithSelfGeneratedURL() { func testInitWithSelfGeneratedURL() {
let newProfile = ServerProfile.init(url: profile.URL()) let newProfile = ServerProfile.init(url: profile.URL()!)
XCTAssertEqual(newProfile?.serverHost, profile.serverHost) XCTAssertEqual(newProfile?.serverHost, profile.serverHost)
XCTAssertEqual(newProfile?.serverPort, profile.serverPort) XCTAssertEqual(newProfile?.serverPort, profile.serverPort)
@ -42,7 +42,7 @@ class ServerProfileTests: XCTestCase {
} }
func testInitWithPlainURL() { func testInitWithPlainURL() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388") let url = URL(string: "ss://aes-256-cfb:password@example.com:8388")!
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
@ -57,7 +57,7 @@ class ServerProfileTests: XCTestCase {
} }
func testInitWithPlainURLandQuery() { func testInitWithPlainURLandQuery() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true") let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true")!
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
@ -72,7 +72,7 @@ class ServerProfileTests: XCTestCase {
} }
func testInitWithPlainURLandAnotherQuery() { func testInitWithPlainURLandAnotherQuery() {
let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=0") let url = URL(string: "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=0")!
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
@ -88,7 +88,7 @@ class ServerProfileTests: XCTestCase {
func testInitWithBase64EncodedURL() { func testInitWithBase64EncodedURL() {
// "ss://aes-256-cfb:password@example.com:8388" // "ss://aes-256-cfb:password@example.com:8388"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OA") let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OA")!
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
@ -104,7 +104,7 @@ class ServerProfileTests: XCTestCase {
func testInitWithBase64EncodedURLandQuery() { func testInitWithBase64EncodedURLandQuery() {
// "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true" // "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OD9SZW1hcms9UHJpc20mT1RBPXRydWU") let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAZXhhbXBsZS5jb206ODM4OD9SZW1hcms9UHJpc20mT1RBPXRydWU")!
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
@ -119,15 +119,7 @@ class ServerProfileTests: XCTestCase {
} }
func testInitWithEmptyURL() { func testInitWithEmptyURL() {
let url = URL(string: "ss://") let url = URL(string: "ss://")!
let profile = ServerProfile(url: url)
XCTAssertNil(profile)
}
func testInitWithInvalidURL() {
let url = URL(string: "ss://invalid url")
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
@ -136,13 +128,47 @@ class ServerProfileTests: XCTestCase {
func testInitWithBase64EncodedInvalidURL() { func testInitWithBase64EncodedInvalidURL() {
// "ss://invalid url" // "ss://invalid url"
let url = URL(string: "ss://aW52YWxpZCB1cmw") let url = URL(string: "ss://aW52YWxpZCB1cmw")!
let profile = ServerProfile(url: url) let profile = ServerProfile(url: url)
XCTAssertNil(profile) XCTAssertNil(profile)
} }
func testInitWithSIP002URL() {
// "ss://aes-256-cfb:password@example.com:8388?Remark=Prism&OTA=true"
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmQ=@example.com:8388/?Remark=Prism&OTA=true")!
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.serverHost, "example.com")
XCTAssertEqual(profile?.serverPort, 8388)
XCTAssertEqual(profile?.method, "aes-256-cfb")
XCTAssertEqual(profile?.password, "password")
XCTAssertEqual(profile?.remark, "Prism")
XCTAssertEqual(profile?.ota, true)
}
func testInitWithSIP002URLProfileName() {
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmQ=@example.com:8388/#Name")!
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.remark, "Name")
}
func testInitWithSIP002URLProfileNameOverride() {
let url = URL(string: "ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmQ=@example.com:8388/?Remark=Name#Overriden")!
let profile = ServerProfile(url: url)
XCTAssertNotNil(profile)
XCTAssertEqual(profile?.remark, "Overriden")
}
func testPerformanceExample() { func testPerformanceExample() {
// This is an example of a performance test case. // This is an example of a performance test case.
self.measure { self.measure {