diff --git a/Package.swift b/Package.swift index 890f5f4..5311008 100644 --- a/Package.swift +++ b/Package.swift @@ -1,7 +1,7 @@ import PackageDescription import Foundation -var repos = ["Perfect-Crypto"] +var repos = ["Perfect-HTTPServer"] var targets = [Target(name: "PerfectSSOAuth", dependencies: [])] let excludes: [String] let db: String diff --git a/README.md b/README.md index b047e3c..befc2d1 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ Certainly, you can skip the log system if no need, which will log to the console let man = LoginManager(udb: udb) ``` -Now you can use the `LoginManager` instance to register, login, load profile, update password and update profile, or drop users: +Now you can use the `LoginManager` instance to register, login, load profile, update password and update profile, or drop user: #### Register & Login @@ -264,6 +264,8 @@ let token = try man.login(id: "someone", password: "secret") **NOTE**: By default, both user id and password are in a length of **[5,80]** string. Check [Login / Password Quality Control](#login--password-quality-control) for more information. +**NOTE** For those end users who want user id to be an autoincrement UInt64 id, please fork this repo to make your own edition. + The token generated by `LoginManager.login()` is a JWT for HTTP web servers. It is supposed to send to the client (browser) as a proof of authentication. diff --git a/README.zh_CN.md b/README.zh_CN.md index b54084b..e65cad1 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -259,6 +259,8 @@ let token = try man.login(id: "someone", password: "secret") **注意**: 默认状态下用户名密码长度都是 **[5,80]** 之间的字符串。 详见 [用户名密码质量控制](#login--password-quality-control)。 +**注意** 如果最终用户希望用可以自动递增的UInt64作为用户id,请自行创建本项目分支版本进行调整。 + 调用 `LoginManager.login()` 登录后产生的令牌是一个JWT字符串,可以用于HTTP服务器进行权鉴认证。 该令牌应该送回给客户端(浏览器)用于后续安全会话的权限凭证。 diff --git a/Sources/PerfectSSOAuth/DataworkUtility.swift b/Sources/PerfectSSOAuth/DataworkUtility.swift index b246b17..356a312 100644 --- a/Sources/PerfectSSOAuth/DataworkUtility.swift +++ b/Sources/PerfectSSOAuth/DataworkUtility.swift @@ -8,6 +8,48 @@ public enum Exception: Error { /// an error with a readable reason. case fault(String) + + /// something about json + case json + + /// bad user name + case username + + /// bad password + case password + + /// encryption failure + case encryption + + /// digestion failure + case digestion + + /// access denied + case access + + /// bad format + case malformed + + /// bad request + case request + + /// expired + case expired + + /// violation + case violation + + /// record not found + case inexisting + + /// operation failure + case operation + + /// unsupported + case unsupported + + /// connection failure + case connection } /// a log file record @@ -148,7 +190,7 @@ public final class DataworkUtility { guard let json = String.init(bytes: data, encoding: .utf8), let payload = try json.jsonDecode() as? [String:Any] else { - throw Exception.fault("json decoding failure") + throw Exception.json } var result:[Field] = [] for (key, value) in payload { diff --git a/Sources/PerfectSSOAuth/PerfectSSOAuth.swift b/Sources/PerfectSSOAuth/PerfectSSOAuth.swift index bb2e878..e61f39f 100644 --- a/Sources/PerfectSSOAuth/PerfectSSOAuth.swift +++ b/Sources/PerfectSSOAuth/PerfectSSOAuth.swift @@ -1,5 +1,7 @@ import PerfectLib import PerfectCrypto +import PerfectHTTP +import PerfectHTTPServer import Foundation import Dispatch @@ -202,12 +204,12 @@ public final class HumblestLoginControl: LoginQualityControl { } public func goodEnough(userId: String) throws { guard userId.count.inRange(of: size) else { - throw Exception.fault("invalid username") + throw Exception.username } } public func goodEnough(password: String) throws { guard password.count.inRange(of: size) else { - throw Exception.fault("invalid password") + throw Exception.password } } } @@ -260,7 +262,7 @@ public class LoginManager where Profile: Codable { /// - throws: Exception. fileprivate func shadow(_ password: String, salt: String) throws -> String? { guard let hashData = salt.digest(.sha384) else { - throw Exception.fault("digest(sha384)") + throw Exception.digestion } let hashKeyData:[UInt8] = hashData[0..<32].map {$0} let ivData:[UInt8] = hashData[32..<48].map {$0} @@ -268,7 +270,7 @@ public class LoginManager where Profile: Codable { guard let x = data.encrypt(self._cipher, key: hashKeyData, iv: ivData), let y = x.encode(.base64) else { - throw Exception.fault("aes(128)+base64") + throw Exception.encryption } return String(validatingUTF8: y) } @@ -335,9 +337,9 @@ public class LoginManager where Profile: Codable { try _rate.onAttemptRegister(id, password: password) try _pass.goodEnough(userId: id) try _pass.goodEnough(password: password) - } catch (let err) { - _log.report(id, level: .warning, event: .registration, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .registration, message: error.localizedDescription) + throw error } guard let random = ([UInt8](randomCount: _saltLength)).encode(.hex), let salt = String(validatingUTF8: random), @@ -345,17 +347,17 @@ public class LoginManager where Profile: Codable { else { _log.report(id, level: .critical, event: .registration, message: "unable to register '\(id)'/'\(password)' because of encryption failure") - throw Exception.fault("crypto failure") + throw Exception.encryption } let u = UserRecord(id: id, salt: salt, shadow: shadow, profile: profile) _lock.wait() do { try _insert(u) _lock.signal() - } catch (let err) { - _log.report(id, level: .warning, event: .registration, message: err.localizedDescription) + } catch { + _log.report(id, level: .warning, event: .registration, message: error.localizedDescription) _lock.signal() - throw err + throw error } _log.report(id, level: .event, event: .registration, message: "user registered") } @@ -370,9 +372,9 @@ public class LoginManager where Profile: Codable { try _rate.onUpdate(id, password: password) try _pass.goodEnough(userId: id) try _pass.goodEnough(password: password) - } catch (let err) { - _log.report(id, level: .warning, event: .updating, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .updating, message: error.localizedDescription) + throw error } guard let random = ([UInt8](randomCount: _saltLength)).encode(.hex), let salt = String(validatingUTF8: random), @@ -380,7 +382,7 @@ public class LoginManager where Profile: Codable { else { _log.report("unknown", level: .warning, event: .updating, message: "invalid update attempt '\(id)'/'\(password)'") - throw Exception.fault("crypto failure") + throw Exception.encryption } do { _lock.wait() @@ -389,10 +391,10 @@ public class LoginManager where Profile: Codable { u.shadow = shadow try self._update(u) _lock.signal() - } catch (let err) { - _log.report(id, level: .warning, event: .updating, message: err.localizedDescription) + } catch { + _log.report(id, level: .warning, event: .updating, message: error.localizedDescription) _lock.signal() - throw err + throw error } _log.report(id, level: .event, event: .updating, message: "password updated") } @@ -405,9 +407,9 @@ public class LoginManager where Profile: Codable { public func update(id: String, profile: Profile) throws { do { try _pass.goodEnough(userId: id) - } catch (let err) { - _log.report(id, level: .warning, event: .updating, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .updating, message: error.localizedDescription) + throw error } do { _lock.wait() @@ -418,10 +420,10 @@ public class LoginManager where Profile: Codable { _lock.wait() try self._update(u) _lock.signal() - } catch (let err) { - _log.report(id, level: .warning, event: .updating, message: err.localizedDescription) + } catch { + _log.report(id, level: .warning, event: .updating, message: error.localizedDescription) _lock.signal() - throw err + throw error } _log.report(id, level: .event, event: .updating, message: "profile updated") } @@ -442,9 +444,9 @@ public class LoginManager where Profile: Codable { try _pass.goodEnough(userId: id) try _pass.goodEnough(password: password) try _rate.onAttemptLogin(id, password: password) - } catch (let err) { - _log.report(id, level: .warning, event: .login, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .login, message: error.localizedDescription) + throw error } let u: U do { @@ -452,10 +454,10 @@ public class LoginManager where Profile: Codable { u = try _select(id) _lock.signal() try _rate.onLogin(u) - } catch (let err) { - _log.report(id, level: .warning, event: .login, message: err.localizedDescription) + } catch { + _log.report(id, level: .warning, event: .login, message: error.localizedDescription) _lock.signal() - throw err + throw error } guard let shadow = try self.shadow(password, salt: u.salt), @@ -463,7 +465,7 @@ public class LoginManager where Profile: Codable { else { _log.report(id, level: .warning, event: .login, message: "access denied") - throw Exception.fault("access denied") + throw Exception.access } return try self.renew(u: u, subject: subject, timeout: timeout, headers: headers) } @@ -481,24 +483,24 @@ public class LoginManager where Profile: Codable { (header: [String: Any], content: [String: Any]) { do { try _rate.onAttemptToken(token: token) - } catch (let err) { - _log.report("unknown", level: .warning, event: .verification, message: err.localizedDescription) - throw err + } catch { + _log.report("unknown", level: .warning, event: .verification, message: error.localizedDescription) + throw error } guard let jwt = JWTVerifier(token), let id = jwt.payload["aud"] as? String else { _log.report("unknown", level: .warning, event: .verification, message: "jwt verification failure") - throw Exception.fault("jwt verification failure") + throw Exception.access } let u: U do { _lock.wait() u = try _select(id) _lock.signal() - } catch (let err) { - _log.report(id, level: .warning, event: .verification, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .verification, message: error.localizedDescription) + throw error } let now = time(nil) do { @@ -506,14 +508,14 @@ public class LoginManager where Profile: Codable { } catch { _log.report(id, level: .warning, event: .verification, message: "jwt verification failure: \(token)") - throw Exception.fault("jwt verification failure") + throw Exception.access } guard let iss = jwt.payload["iss"] as? String else { - throw Exception.fault("issuer is null") + throw Exception.malformed } if iss != _managerID { guard allowSSO else { - throw Exception.fault("invalid issuer") + throw Exception.malformed } } guard let timeout = jwt.payload["exp"] as? Int, @@ -524,7 +526,7 @@ public class LoginManager where Profile: Codable { else { _log.report(id, level: .warning, event: .verification, message: "jwt invalid payload: \(jwt.payload)") - throw Exception.fault("token failure") + throw Exception.malformed } _lock.wait() @@ -533,7 +535,7 @@ public class LoginManager where Profile: Codable { if rejected { _log.report(id, level: .warning, event: .verification, message: "rejected") - throw Exception.fault("rejected") + throw Exception.access } if logout { @@ -541,11 +543,11 @@ public class LoginManager where Profile: Codable { _lock.wait() try _ban(ticket, timeout) _lock.signal() - } catch (let err) { + } catch { _log.report(id, level: .warning, event: .logoff, - message: "log out failure:" + err.localizedDescription ) + message: "log out failure:" + error.localizedDescription ) _lock.signal() - throw err + throw error } } return (header: jwt.header, content: jwt.payload) @@ -565,16 +567,16 @@ public class LoginManager where Profile: Codable { guard let jwt = JWTCreator(payload: claims) else { _log.report(u.id, level: .critical, event: .login, message: "token failure") - throw Exception.fault("token failure") + throw Exception.malformed } let ret: String do { ret = try jwt.sign(alg: _alg, key: u.salt, headers: headers) - } catch (let err) { + } catch { _log.report(u.id, level: .critical, event: .login, - message: "jwt signature failure: \(err)") - throw err + message: "jwt signature failure: \(error.localizedDescription)") + throw error } _log.report(u.id, level: .event, event: .login, message: "user logged") return ret @@ -594,9 +596,9 @@ public class LoginManager where Profile: Codable { headers: [String:Any] = [:]) throws -> String { do { try _pass.goodEnough(userId: id) - } catch (let err) { - _log.report(id, level: .warning, event: .renewal, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .renewal, message: error.localizedDescription) + throw error } let u: U do { @@ -604,9 +606,9 @@ public class LoginManager where Profile: Codable { u = try _select(id) _lock.signal() try _rate.onRenewToken(u) - } catch (let err) { - _log.report(id, level: .warning, event: .renewal, message: err.localizedDescription) - throw err + } catch { + _log.report(id, level: .warning, event: .renewal, message: error.localizedDescription) + throw error } return try self.renew(u: u, subject: subject, timeout: timeout, headers: headers) } @@ -638,12 +640,205 @@ public class LoginManager where Profile: Codable { try _delete(id) _lock.signal() _log.report(id, level: .event, event: .unregistration, message: "user closed") - } catch (let err) { - _log.report(id, level: .warning, event: .unregistration, message: err.localizedDescription) + } catch { + _log.report(id, level: .warning, event: .unregistration, message: error.localizedDescription) _lock.signal() - throw err + throw error } } } +public class HTTPAccessControl: HTTPRequestFilter where Profile:Codable { + + public func filter(request: HTTPRequest, response: HTTPResponse, + callback: (HTTPRequestFilterResult) -> ()) { + + do { + switch request.uri { + case _config.reg: + let cookie = try self.register(request: request) + response.addCookie(cookie) + break + case _config.login: + let cookie = try self.login(request: request) + response.addCookie(cookie) + break + case _config.logout: + try self.logout(request: request) + break + case _config.modpass: + try self.modpass(request: request) + break + case _config.update: + try self.update(request: request) + break + case _config.drop: + try self.drop(request: request) + break + default: + let (id, profile) = try self.access(request: request) + response.request.scratchPad["id"] = id + response.request.scratchPad["profile"] = profile + callback(.continue(request, response)) + return + } + response.setHeader(.contentType, value: "text/json") + response.setBody(string: "{\"error\":\"\"}") + response.completed() + callback(.execute(request, response)) + } catch { + response.setHeader(.contentType, value: "text/json") + response.setBody(string: "{\"error\":\"\(error.localizedDescription)\"}") + response.completed() + callback(.halt(request, response)) + } + } + + public struct Configuration: Codable { + + /// URIs / routes + public var reg = "/api/reg" + public var login = "/api/login" + public var logout = "/api/logout" + public var update = "/api/update" + public var modpass = "/api/modpass" + public var drop = "/api/drop" + + /// variables + public var id = "id" + public var password = "password" + public var profile = "profile" + public var jwt = "jwt" + public var aud = "aud" + } + + let _man: LoginManager + let _config: Configuration + let _encoder: JSONEncoder + let _decoder: JSONDecoder + + public init(_ manager: LoginManager, configuration: Configuration) { + _man = manager + _config = configuration + _encoder = JSONEncoder() + _decoder = JSONDecoder() + } + + internal func profile(of: String) throws -> Profile { + let bytes: [UInt8] = of.utf8.map { $0 } + let data = Data.init(bytes: bytes) + return try _decoder.decode(Profile.self, from: data) + } + + public func register(request: HTTPRequest) throws -> PerfectHTTP.HTTPCookie { + guard + request.uri == _config.reg, + let id = request.param(name: _config.id), + let password = request.param(name: _config.password), + let json = request.param(name: _config.profile) + else { + throw Exception.request + } + try _man.register(id: id, password: password, profile: try profile(of: json)) + return try login(id: id, password: password) + } + + public func login(request: HTTPRequest) throws -> PerfectHTTP.HTTPCookie { + guard request.uri == _config.login, + let id = request.param(name: _config.id), + let password = request.param(name: _config.password) + else { + throw Exception.request + } + return try login(id: id, password: password) + } + + internal func login(id: String, password: String) throws -> PerfectHTTP.HTTPCookie { + let token = try _man.login(id: id, password: password) + return HTTPCookie( + name: _config.jwt, + value: token, + domain: "", + expires: .relativeSeconds(600), + path: "/", + secure: false, + httpOnly: true, + sameSite: .strict + ) + } + + public func jwt(of: HTTPRequest) -> String? { + var token = "" + for (k, v) in of.cookies { + if k == _config.jwt { + token = v + } + } + if token.isEmpty { return nil } + return token + } + + public func update(request: HTTPRequest) throws { + guard request.uri == _config.update, + let token = jwt(of: request), + let json = request.param(name: _config.profile) + else { + throw Exception.request + } + let (_, content) = try _man.verify(token: token) + guard let id = content[_config.aud] as? String else { + throw Exception.malformed + } + let p = try profile(of: json) + try _man.update(id: id, profile: p) + } + + public func modpass(request: HTTPRequest) throws { + guard request.uri == _config.modpass, + let token = jwt(of: request), + let newpass = request.param(name: _config.password) + else { + throw Exception.request + } + let (_, content) = try _man.verify(token: token) + guard let id = content[_config.aud] as? String else { + throw Exception.malformed + } + try _man.update(id: id, password: newpass) + } + + public func access(request: HTTPRequest) throws -> (String,Profile) { + guard let token = jwt(of: request) + else { + throw Exception.request + } + let (_, content) = try _man.verify(token: token) + guard let id = content[_config.aud] as? String else { + throw Exception.malformed + } + let p = try _man.load(id: id) + return (id, p) + } + + public func logout(request: HTTPRequest) throws { + guard request.uri == _config.logout, + let token = jwt(of: request) + else { + throw Exception.request + } + _ = try _man.verify(token: token, logout: true) + } + + public func drop(request: HTTPRequest) throws { + guard request.uri == _config.drop, + let token = jwt(of: request) else { + throw Exception.request + } + let (_, content) = try _man.verify(token: token) + guard let id = content[_config.aud] as? String else { + throw Exception.malformed + } + _ = try _man.drop(id: id) + } +} diff --git a/Sources/UDBJSONFile/UDBJSONFile.swift b/Sources/UDBJSONFile/UDBJSONFile.swift index 6420bc1..06b0243 100644 --- a/Sources/UDBJSONFile/UDBJSONFile.swift +++ b/Sources/UDBJSONFile/UDBJSONFile.swift @@ -31,7 +31,7 @@ public class UDBJSONFile: UserDatabase { public func ban(_ ticket: String, _ expiration: time_t) throws { guard expiration > time(nil) else { - throw Exception.fault("ticket has already expired") + throw Exception.expired } self.autoflush() tickets[ticket] = expiration @@ -68,14 +68,14 @@ public class UDBJSONFile: UserDatabase { public func insert(_ record: UserRecord) throws { let data = try encoder.encode(record) if 0 == access(path(of: record.id), 0) { - throw Exception.fault("record has already registered") + throw Exception.violation } try data.write(to: self.url(of: record.id)) } public func select(_ id: String) throws -> UserRecord { guard 0 == access(path(of: id), 0) else { - throw Exception.fault("record does not exist") + throw Exception.inexisting } let data = try Data(contentsOf: url(of: id)) return try decoder.decode(UserRecord.self, from: data) @@ -84,14 +84,14 @@ public class UDBJSONFile: UserDatabase { public func update(_ record: UserRecord) throws { let data = try encoder.encode(record) guard 0 == access(path(of: record.id), 0) else { - throw Exception.fault("record does not exist") + throw Exception.inexisting } try data.write(to: url(of: record.id)) } public func delete(_ id: String) throws { guard 0 == unlink(path(of: id)) else { - throw Exception.fault("operation failure") + throw Exception.operation } } @@ -101,7 +101,7 @@ public class UDBJSONFile: UserDatabase { closedir(dir) } else if autocreation { guard 0 == mkdir(directory, mode_t(permission)) else { - throw Exception.fault("operation failure") + throw Exception.operation } } touch = time(nil) diff --git a/Sources/UDBMariaDB/UDBMariaDB.swift b/Sources/UDBMariaDB/UDBMariaDB.swift index 318dd42..4b83b3c 100644 --- a/Sources/UDBMariaDB/UDBMariaDB.swift +++ b/Sources/UDBMariaDB/UDBMariaDB.swift @@ -18,8 +18,8 @@ extension MySQLStmt { } else if x is [Int8], let y = x as? [Int8] { self.bindParam(y, length: y.count) } else { - let tp = type(of: x) - throw Exception.fault("incompatible type: \(tp)") + //let tp = type(of: x) + throw Exception.unsupported } } } @@ -38,7 +38,7 @@ public class UDBMariaDB: UserDatabase { touch = now } } - + public init (host: String, user: String, password: String, database: String, sample: Profile) throws { @@ -49,15 +49,15 @@ public class UDBMariaDB: UserDatabase { guard db.setOption(.MYSQL_SET_CHARSET_NAME, "utf8mb4"), db.connect(host: host, user: user, password: password, db: database) else { - throw Exception.fault("connection failure") + throw Exception.connection } let properties = try DataworkUtility.explainProperties(of: sample) guard !properties.isEmpty else { - throw Exception.fault("invalid profile structure") + throw Exception.malformed } fields = try properties.map { s -> Field in guard let tp = DataworkUtility.ANSITypeOf(s.type) else { - throw Exception.fault("incompatible type name: \(s.type)") + throw Exception.unsupported } return Field(name: s.name, type: tp) } @@ -69,7 +69,7 @@ public class UDBMariaDB: UserDatabase { salt VARCHAR(256), shadow VARCHAR(1024), \(fieldDescription)) """ guard db.query(statement: sql) else { - throw Exception.fault("table creation failure") + throw Exception.operation } let sql2 = """ CREATE TABLE IF NOT EXISTS tickets ( @@ -77,13 +77,13 @@ public class UDBMariaDB: UserDatabase { expiration INTEGER) """ guard db.query(statement: sql2) else { - throw Exception.fault("tickets table creation failure") + throw Exception.operation } let sql3 = """ CREATE INDEX IF NOT EXISTS ticket_exp ON tickets( expiration) """ guard db.query(statement: sql3) else { - throw Exception.fault("tickets index creation failure") + throw Exception.operation } } @@ -93,7 +93,7 @@ public class UDBMariaDB: UserDatabase { public func ban(_ ticket: String, _ expiration: time_t) throws { guard expiration > time(nil) else { - throw Exception.fault("ticket has already expired") + throw Exception.expired } self.autoflush() @@ -102,12 +102,12 @@ public class UDBMariaDB: UserDatabase { defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(ticket) stmt.bindParam(expiration) guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } @@ -159,13 +159,13 @@ public class UDBMariaDB: UserDatabase { public func insert(_ record: UserRecord) throws { if exists(record.id) { - throw Exception.fault("user has already registered") + throw Exception.violation } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), let dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } let properties:[String] = fields.map { $0.name } let columns = ["id", "salt", "shadow"] + properties @@ -177,7 +177,7 @@ public class UDBMariaDB: UserDatabase { defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(record.id) stmt.bindParam(record.salt) @@ -187,19 +187,19 @@ public class UDBMariaDB: UserDatabase { try stmt.bindParameter(x) } guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } public func update(_ record: UserRecord) throws { guard exists(record.id) else { - throw Exception.fault("user does not exists") + throw Exception.inexisting } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), let dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } let properties:[String] = fields.map { $0.name } let columns:[String] = fields.map { "\($0.name) = ?" } @@ -208,7 +208,7 @@ public class UDBMariaDB: UserDatabase { let stmt = MySQLStmt(db) defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(record.salt) stmt.bindParam(record.shadow) @@ -218,7 +218,7 @@ public class UDBMariaDB: UserDatabase { } stmt.bindParam(record.id) guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } @@ -231,10 +231,10 @@ public class UDBMariaDB: UserDatabase { defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(self.db.errorMessage()) + throw Exception.operation } stmt.bindParam(id) - guard stmt.execute() else { throw Exception.fault(self.db.errorMessage())} + guard stmt.execute() else { throw Exception.operation } let fetched = stmt.results().forEachRow { rec in let _id = rec[0] as? String let _salt = rec[1] as? String @@ -260,24 +260,24 @@ public class UDBMariaDB: UserDatabase { debugPrint("json failure") } } - guard fetched, let v = u else { throw Exception.fault(self.db.errorMessage())} + guard fetched, let v = u else { throw Exception.operation } return v } public func delete(_ id: String) throws { guard exists(id) else { - throw Exception.fault("user does not exist") + throw Exception.inexisting } let stmt = MySQLStmt(db) defer { stmt.close() } let sql = "DELETE FROM users WHERE id = ?" guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(id) guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } } diff --git a/Sources/UDBMySQL/UDBMySQL.swift b/Sources/UDBMySQL/UDBMySQL.swift index 830c662..653f236 100644 --- a/Sources/UDBMySQL/UDBMySQL.swift +++ b/Sources/UDBMySQL/UDBMySQL.swift @@ -18,8 +18,8 @@ extension MySQLStmt { } else if x is [Int8], let y = x as? [Int8] { self.bindParam(y, length: y.count) } else { - let tp = type(of: x) - throw Exception.fault("incompatible type: \(tp)") + // let tp = type(of: x) + throw Exception.unsupported } } } @@ -49,15 +49,15 @@ public class UDBMySQL: UserDatabase { guard db.setOption(.MYSQL_SET_CHARSET_NAME, "utf8mb4"), db.connect(host: host, user: user, password: password, db: database) else { - throw Exception.fault("connection failure") + throw Exception.connection } let properties = try DataworkUtility.explainProperties(of: sample) guard !properties.isEmpty else { - throw Exception.fault("invalid profile structure") + throw Exception.malformed } fields = try properties.map { s -> Field in guard let tp = DataworkUtility.ANSITypeOf(s.type) else { - throw Exception.fault("incompatible type name: \(s.type)") + throw Exception.unsupported } return Field(name: s.name, type: tp) } @@ -69,7 +69,7 @@ public class UDBMySQL: UserDatabase { salt VARCHAR(256), shadow VARCHAR(1024), \(fieldDescription)) """ guard db.query(statement: sql) else { - throw Exception.fault("table creation failure") + throw Exception.operation } let sql2 = """ CREATE TABLE IF NOT EXISTS tickets ( @@ -77,13 +77,13 @@ public class UDBMySQL: UserDatabase { expiration INTEGER) """ guard db.query(statement: sql2) else { - throw Exception.fault("tickets table creation failure") + throw Exception.operation } let sql3 = """ CREATE INDEX IF NOT EXISTS ticket_exp ON tickets( expiration) """ guard db.query(statement: sql3) else { - throw Exception.fault("tickets index creation failure") + throw Exception.operation } } @@ -93,7 +93,7 @@ public class UDBMySQL: UserDatabase { public func ban(_ ticket: String, _ expiration: time_t) throws { guard expiration > time(nil) else { - throw Exception.fault("ticket has already expired") + throw Exception.expired } self.autoflush() @@ -102,12 +102,12 @@ public class UDBMySQL: UserDatabase { defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(ticket) stmt.bindParam(expiration) guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } @@ -159,13 +159,13 @@ public class UDBMySQL: UserDatabase { public func insert(_ record: UserRecord) throws { if exists(record.id) { - throw Exception.fault("user has already registered") + throw Exception.violation } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), let dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } let properties:[String] = fields.map { $0.name } let columns = ["id", "salt", "shadow"] + properties @@ -177,7 +177,7 @@ public class UDBMySQL: UserDatabase { defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(record.id) stmt.bindParam(record.salt) @@ -187,19 +187,19 @@ public class UDBMySQL: UserDatabase { try stmt.bindParameter(x) } guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } public func update(_ record: UserRecord) throws { guard exists(record.id) else { - throw Exception.fault("user does not exists") + throw Exception.inexisting } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), let dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } let properties:[String] = fields.map { $0.name } let columns:[String] = fields.map { "\($0.name) = ?" } @@ -208,7 +208,7 @@ public class UDBMySQL: UserDatabase { let stmt = MySQLStmt(db) defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(record.salt) stmt.bindParam(record.shadow) @@ -218,7 +218,7 @@ public class UDBMySQL: UserDatabase { } stmt.bindParam(record.id) guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } @@ -231,10 +231,10 @@ public class UDBMySQL: UserDatabase { defer { stmt.close() } guard stmt.prepare(statement: sql) else { - throw Exception.fault(self.db.errorMessage()) + throw Exception.operation } stmt.bindParam(id) - guard stmt.execute() else { throw Exception.fault(self.db.errorMessage())} + guard stmt.execute() else { throw Exception.operation } let fetched = stmt.results().forEachRow { rec in let _id = rec[0] as? String let _salt = rec[1] as? String @@ -260,24 +260,24 @@ public class UDBMySQL: UserDatabase { debugPrint("json failure") } } - guard fetched, let v = u else { throw Exception.fault(self.db.errorMessage())} + guard fetched, let v = u else { throw Exception.operation } return v } public func delete(_ id: String) throws { guard exists(id) else { - throw Exception.fault("user does not exist") + throw Exception.inexisting } let stmt = MySQLStmt(db) defer { stmt.close() } let sql = "DELETE FROM users WHERE id = ?" guard stmt.prepare(statement: sql) else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } stmt.bindParam(id) guard stmt.execute() else { - throw Exception.fault(db.errorMessage()) + throw Exception.operation } } } diff --git a/Sources/UDBPostgreSQL/UDBPostgreSQL.swift b/Sources/UDBPostgreSQL/UDBPostgreSQL.swift index 189058e..097f133 100644 --- a/Sources/UDBPostgreSQL/UDBPostgreSQL.swift +++ b/Sources/UDBPostgreSQL/UDBPostgreSQL.swift @@ -25,18 +25,18 @@ public class UDBPostgreSQL: UserDatabase { db = PGConnection() let status = db.connectdb(connection) guard status == .ok else { - throw Exception.fault("Connection Failure, please check the connection string " + connection) + throw Exception.connection } touch = time(nil) encoder = JSONEncoder() decoder = JSONDecoder() let properties = try DataworkUtility.explainProperties(of: sample) guard !properties.isEmpty else { - throw Exception.fault("invalid profile structure") + throw Exception.malformed } fields = try properties.map { s -> Field in guard let tp = DataworkUtility.ANSITypeOf(s.type) else { - throw Exception.fault("incompatible type name: \(s.type)") + throw Exception.unsupported } return Field(name: s.name, type: tp) } @@ -49,10 +49,10 @@ public class UDBPostgreSQL: UserDatabase { """ let result = try db.execute(statement: sql) let s = result.status() - let r = result.errorMessage() + //let r = result.errorMessage() result.clear() guard s == .commandOK || s == .tuplesOK else { - throw Exception.fault(r) + throw Exception.operation } let sql2 = """ CREATE TABLE IF NOT EXISTS tickets ( @@ -61,20 +61,20 @@ public class UDBPostgreSQL: UserDatabase { """ let result2 = try db.execute(statement: sql2) let s2 = result2.status() - let r2 = result2.errorMessage() + //let r2 = result2.errorMessage() result2.clear() guard s2 == .commandOK || s2 == .tuplesOK else { - throw Exception.fault(r2) + throw Exception.operation } let sql3 = """ CREATE INDEX IF NOT EXISTS ticket_exp ON tickets( expiration) """ let result3 = try db.execute(statement: sql3) let s3 = result3.status() - let r3 = result3.errorMessage() + //let r3 = result3.errorMessage() result3.clear() guard s3 == .commandOK || s3 == .tuplesOK else { - throw Exception.fault(r3) + throw Exception.operation } } @@ -84,17 +84,17 @@ public class UDBPostgreSQL: UserDatabase { public func ban(_ ticket: String, _ expiration: time_t) throws { guard expiration > time(nil) else { - throw Exception.fault("ticket has already expired") + throw Exception.expired } self.autoflush() let sql = "INSERT INTO tickets(id, expiration) VALUES ($1, $2)" let result = db.exec(statement: sql, params: [ticket, expiration]) let s = result.status() - let r = result.errorMessage() + // let r = result.errorMessage() result.clear() guard s == .commandOK || s == .tuplesOK else { - throw Exception.fault(r) + throw Exception.operation } } @@ -116,13 +116,13 @@ public class UDBPostgreSQL: UserDatabase { public func insert(_ record: UserRecord) throws { if exists(record.id) { - throw Exception.fault("user has already registered") + throw Exception.violation } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), var dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } dic["id"] = record.id dic["salt"] = record.salt @@ -139,7 +139,7 @@ public class UDBPostgreSQL: UserDatabase { if let v = dic[columns[i]] { values.append(v) } else { - throw Exception.fault("unexpected field \(columns[i])") + throw Exception.unsupported } } let col = columns.joined(separator: ",") @@ -147,22 +147,22 @@ public class UDBPostgreSQL: UserDatabase { sql = "INSERT INTO users (\(col)) VALUES(\(que))" let result = db.exec(statement: sql, params: values) let s = result.status() - let r = result.errorMessage() + // let r = result.errorMessage() result.clear() guard s == .commandOK || s == .tuplesOK else { - throw Exception.fault(r) + throw Exception.operation } } public func update(_ record: UserRecord) throws { guard exists(record.id) else { - throw Exception.fault("user does not exists") + throw Exception.inexisting } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), var dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } dic["salt"] = record.salt dic["shadow"] = record.shadow @@ -177,7 +177,7 @@ public class UDBPostgreSQL: UserDatabase { if let v = dic[col] { values.append(v) } else { - throw Exception.fault("unexpected field: \(col)") + throw Exception.unsupported } } values.append(record.id) @@ -186,24 +186,24 @@ public class UDBPostgreSQL: UserDatabase { let sql = "UPDATE users SET \(sentence) WHERE id = $\(idNum)" let result = db.exec(statement: sql, params: values) let s = result.status() - let r = result.errorMessage() + // let r = result.errorMessage() result.clear() guard s == .commandOK || s == .tuplesOK else { - throw Exception.fault(r) + throw Exception.operation } } public func delete(_ id: String) throws { guard exists(id) else { - throw Exception.fault("user does not exist") + throw Exception.violation } let sql = "DELETE FROM users WHERE id = $1" let result = db.exec(statement: sql, params: [id]) let s = result.status() - let r = result.errorMessage() + // let r = result.errorMessage() result.clear() guard s == .commandOK || s == .tuplesOK else { - throw Exception.fault(r) + throw Exception.operation } } @@ -213,18 +213,18 @@ public class UDBPostgreSQL: UserDatabase { let sql = "SELECT id, salt, shadow, \(col) FROM users WHERE id = $1 LIMIT 1" let r = db.exec(statement: sql, params: [id]) let s = r.status() - let msg = r.errorMessage() + // let msg = r.errorMessage() guard s == .commandOK || s == .tuplesOK, r.numTuples() == 1 else { r.clear() - throw Exception.fault(msg) + throw Exception.operation } guard let uid = r.getFieldString(tupleIndex: 0, fieldIndex: 0), let salt = r.getFieldString(tupleIndex: 0, fieldIndex: 1), let shadown = r.getFieldString(tupleIndex: 0, fieldIndex: 2), uid == id else { r.clear() - throw Exception.fault("unexpected select result") + throw Exception.operation } var dic: [String: Any] = [:] for i in 0 ..< fields.count { @@ -248,7 +248,7 @@ public class UDBPostgreSQL: UserDatabase { } else if tp == "BLOB" { dic[fields[i].name] = r.getFieldBlob(tupleIndex: 0, fieldIndex: j) } else { - throw Exception.fault("incompatible SQL type \(tp)") + throw Exception.unsupported } } r.clear() @@ -259,7 +259,7 @@ public class UDBPostgreSQL: UserDatabase { let u = UserRecord(id: id, salt: salt, shadow: shadown, profile: profile) return u } catch { - throw Exception.fault("json encoding / decoding failure") + throw Exception.json } } diff --git a/Sources/UDBSQLite/UDBSQLite.swift b/Sources/UDBSQLite/UDBSQLite.swift index d95f598..b2d943e 100644 --- a/Sources/UDBSQLite/UDBSQLite.swift +++ b/Sources/UDBSQLite/UDBSQLite.swift @@ -34,7 +34,7 @@ public class UDBSQLite: UserDatabase { let properties = try DataworkUtility.explainProperties(of: sample) guard !properties.isEmpty else { - throw Exception.fault("invalid profile structure") + throw Exception.malformed } fields = try properties.map { s -> Field in let tp = s.type @@ -48,7 +48,7 @@ public class UDBSQLite: UserDatabase { } else if tp == "String" { typeName = "TEXT" } else { - throw Exception.fault("incompatible type name: \(tp)") + throw Exception.unsupported } return Field(name: s.name, type: typeName) } @@ -76,7 +76,7 @@ public class UDBSQLite: UserDatabase { public func ban(_ ticket: String, _ expiration: time_t) throws { guard expiration > time(nil) else { - throw Exception.fault("ticket has already expired") + throw Exception.expired } self.autoflush() @@ -122,13 +122,13 @@ public class UDBSQLite: UserDatabase { public func insert(_ record: UserRecord) throws { if exists(record.id) { - throw Exception.fault("user has already registered") + throw Exception.violation } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), let dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } let properties:[String] = fields.map { $0.name } let columns = ["id", "salt", "shadow"] + properties @@ -158,7 +158,7 @@ public class UDBSQLite: UserDatabase { try stmt.bind(position: j, s) break default: - throw Exception.fault("incompatible value type") + throw Exception.unsupported } } } @@ -188,7 +188,7 @@ public class UDBSQLite: UserDatabase { case "REAL": dic[fname] = rec.columnDouble(position: j) default: - throw Exception.fault("unexpected column type: \(fields[i].type)") + throw Exception.unsupported } } let json = try dic.jsonEncodedString() @@ -197,14 +197,14 @@ public class UDBSQLite: UserDatabase { u = UserRecord(id: id, salt: salt, shadow: shadow, profile: profile) } guard let v = u else { - throw Exception.fault("record not found") + throw Exception.inexisting } return v } public func delete(_ id: String) throws { guard exists(id) else { - throw Exception.fault("user does not exists") + throw Exception.inexisting } let sql = "DELETE FROM users WHERE id = ?" try db.execute(statement: sql){ @@ -215,13 +215,13 @@ public class UDBSQLite: UserDatabase { public func update(_ record: UserRecord) throws { guard exists(record.id) else { - throw Exception.fault("user does not exists") + throw Exception.inexisting } let data = try encoder.encode(record.profile) let bytes:[UInt8] = data.map { $0 } guard let json = String(validatingUTF8:bytes), let dic = try json.jsonDecode() as? [String: Any] else { - throw Exception.fault("json encoding failure") + throw Exception.json } let columns:[String] = fields.map { "\($0.name) = ?" } let sentence = columns.joined(separator: ",") @@ -250,7 +250,7 @@ public class UDBSQLite: UserDatabase { let s = dic[f.name] as? [Int8] ?? [] try stmt.bind(position:j, s) default: - throw Exception.fault("incompatible value type") + throw Exception.unsupported } } try stmt.bind(position: fields.count + 3, record.id) diff --git a/Tests/PerfectSSOAuthTests/PerfectSSOAuthTests.swift b/Tests/PerfectSSOAuthTests/PerfectSSOAuthTests.swift index cca8a9f..55cd2b3 100644 --- a/Tests/PerfectSSOAuthTests/PerfectSSOAuthTests.swift +++ b/Tests/PerfectSSOAuthTests/PerfectSSOAuthTests.swift @@ -24,13 +24,13 @@ class PerfectSSOAuthTests: XCTestCase { let godpass = "rockford" let badpass = "treefrog" let folder = "/tmp" - let sqlite = "/tmp/users.db" + let sqlite = "/tmp/user.db" let mysql_hst = "maria" let mysql_usr = "root" let mysql_pwd = "rockford" let mysql_dbt = "test" let pgsql_usr = "rocky" - let table = "users" + let profile = Profile(firstName: "rocky", lastName: "wei", age: 21, email: "rocky@perfect.org") let log = FileLogger("/tmp", GMT: false) let pgconnection = "postgresql://rocky:rockford@maria/test" @@ -108,15 +108,15 @@ class PerfectSSOAuthTests: XCTestCase { let rocky = try manager.load(id: username) print(rocky) } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } do { let manager = LoginManager(udb: udb, log: log) _ = try manager.login(id: username, password: badpass) - } catch Exception.fault(let reason) { - print("expected error:", reason) + } catch Exception.access { + print("expected access denied") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } do { let manager = LoginManager(udb: udb, log: log) @@ -130,14 +130,14 @@ class PerfectSSOAuthTests: XCTestCase { let y = try manager.verify(token: tok2) XCTAssertEqual(x.content["iss"] as? String ?? "X", y.content["iss"] as? String ?? "Y") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } do { let manager = LoginManager(udb: udb, log: log) try manager.update(id: username, password: badpass) _ = try manager.login(id: username, password: badpass) } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } do { let manager = LoginManager(udb: udb, log: log, recycle: 3) @@ -157,7 +157,7 @@ class PerfectSSOAuthTests: XCTestCase { let y2 = try? manager.verify(token: token2) XCTAssertNil(y2) } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } do { let manager = LoginManager(udb: udb, log: log) @@ -177,13 +177,13 @@ class PerfectSSOAuthTests: XCTestCase { func testPostgreSQL() { let pg = PGConnection() _ = pg.connectdb(pgconnection) - _ = pg.exec(statement: "DROP TABLE \(table)") + _ = pg.exec(statement: "DROP TABLE users") _ = pg.exec(statement: "DROP TABLE tickets") do { let udb = try UDBPostgreSQL(connection: pgconnection, sample: profile) testStandard(udb: udb, label: "postgresql") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } } func testMariaDB() { @@ -192,14 +192,14 @@ class PerfectSSOAuthTests: XCTestCase { XCTFail("connection failure") return } - _ = mysql.query(statement: "DROP TABLE \(table)") + _ = mysql.query(statement: "DROP TABLE users") _ = mysql.query(statement: "DROP TABLE tickets") do { let udb = try UDBMariaDB(host: mysql_hst, user: mysql_usr, password: mysql_pwd, database: mysql_dbt, sample: profile) testStandard(udb: udb, label: "mariadb") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } } func testMySQL() { @@ -208,14 +208,14 @@ class PerfectSSOAuthTests: XCTestCase { XCTFail("connection failure") return } - _ = mysql.query(statement: "DROP TABLE \(table)") + _ = mysql.query(statement: "DROP TABLE users") _ = mysql.query(statement: "DROP TABLE tickets") do { let udb = try UDBMySQL(host: mysql_hst, user: mysql_usr, password: mysql_pwd, database: mysql_dbt, sample: profile) testStandard(udb: udb, label: "mysql") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } } func testSQLite() { @@ -224,7 +224,7 @@ class PerfectSSOAuthTests: XCTestCase { let udb = try UDBSQLite(path: sqlite, sample: profile) testStandard(udb: udb, label: "sqlite") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } } func testJSONDir() { @@ -233,7 +233,7 @@ class PerfectSSOAuthTests: XCTestCase { let udb = try UDBJSONFile(directory: folder) testStandard(udb: udb, label: "jsonfile") } catch { - XCTFail(error.localizedDescription) + XCTFail("\(error)") } } }