Merge pull request #434 from timothyqiu/qr-code
Fixes #433 Implements SIP002 QR code
This commit is contained in:
@ -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)
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
@ -255,6 +276,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 {
|
||||||
return "\(serverHost):\(serverPort)"
|
return "\(serverHost):\(serverPort)"
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
Reference in New Issue
Block a user