-
Notifications
You must be signed in to change notification settings - Fork 946
/
SessionManager.swift
281 lines (243 loc) · 10.8 KB
/
SessionManager.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//
// SessionManager.swift
// PerfectLib
//
// Created by Kyle Jessup on 7/14/15.
// Copyright (C) 2015 PerfectlySoft, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version, as supplemented by the
// Perfect Additional Terms.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License, as supplemented by the
// Perfect Additional Terms, for more details.
//
// You should have received a copy of the GNU Affero General Public License
// and the Perfect Additional Terms that immediately follow the terms and
// conditions of the GNU Affero General Public License along with this
// program. If not, see <http://www.perfect.org/AGPL_3_0_With_Perfect_Additional_Terms.txt>.
//
import Foundation
import func ICU.ucal_getNow_55
let perfectSessionDB = "perfect_sessions"
let perfectSessionNamePrefix = "_PerfectSessionTracker_"
/// This struct is used for configuring the various options available for a session
/// - seealso `SessionManager`
public struct SessionConfiguration {
var id: String
let name: String
let expires: Int
let useCookie: Bool
let useLink: Bool
let useAuto: Bool
let useNone: Bool
let domain: String
let path: String
let cookieExpires: Double
let rotate: Bool
let secure: Bool
let httpOnly: Bool
var lastAccess = 0.0 // set after reading data
/// Create a new SessionConfiguration struct
/// - parameter name: The name for the new session. All session names must be unique for a given request. Attempting to initialize the same session twice will cause an exception.
/// - parameter expires: The number of minutes from the last access after which the session will expire.
/// - parameter useCookie: If true, indicates that the session should propagate by setting a browser cookie.
/// - parameter useLink: If true, indicates that the session should propagate by rewriting all resulting page links so that they include a search parameter.
/// - parameter useAuto: If true, the session begins by using both cookies and link rewriting. If the session manager detects that cookies are being properly passed then it will stop rewriting links.
/// - parameter useNone: If true, neither cookies nor link rewriting will be utilized. This makes session propagation the responsibility of the page handler.
/// - parameter id: If specified, this will be the value used to identify this session. Session ids are automatically generated when not explicitly provided.
/// - parameter domain: When using cookies for session proagation, this optional value will indicate the cookie's `domain` value. By default no domain value is set for the session cookie.
/// - parameter path: When using cookies for session proagation, this optional value will indicate the cookie's `path` value. By default no path value is set for the session cookie.
/// - parameter cookieExpires: When specified this value will be used as the expiration date for the session's cookie. When not specified, the cookie will have the same expiration as that of the session itself.
/// - parameter rotate: If true, the session will have a new unique session id generated for it on each request.
/// - parameter secure: If true, the session cookie will be marked as `secure` when it is set. This prevents the session from propagating on non-SSL requests.
/// - parameter httpOnly: If true, the session cookie will only be set on normal HTTP requests. This means the cookie will not be set on requests which come through the XMLHTTPRequest mechanism.
public init(_ name: String, expires: Int = 15, useCookie: Bool = true, useLink: Bool = false,
useAuto: Bool = true, useNone: Bool = false, id: String = "", domain: String = "", path: String = "/",
cookieExpires: Double = 0.0, rotate: Bool = false, secure: Bool = false, httpOnly: Bool = false) {
self.name = name
self.expires = expires
self.useCookie = useCookie
self.useLink = useLink
self.useAuto = useAuto
self.useNone = useNone
self.id = id
self.domain = domain
self.path = path
self.cookieExpires = cookieExpires != 0.0 ? cookieExpires : Double(self.expires)
self.rotate = rotate
self.secure = secure
self.httpOnly = httpOnly
}
/// Create a new SessionConfiguration struct
/// - parameter name: The name for the new session. All session names must be unique for a given request. Attempting to initialize the same session twice will cause an exception.
/// - parameter id: This will be the value used to identify this session.
/// - parameter copyFrom: Copy all other configuration values from the given `SessionConfiguration` struct.
public init(_ name: String, id: String, copyFrom: SessionConfiguration) {
self.name = name
self.id = id
self.expires = copyFrom.expires
self.useCookie = copyFrom.useCookie
self.useLink = copyFrom.useLink
self.useAuto = copyFrom.useAuto
self.useNone = copyFrom.useNone
self.domain = copyFrom.domain
self.path = copyFrom.path
self.cookieExpires = copyFrom.cookieExpires
self.rotate = copyFrom.rotate
self.secure = copyFrom.secure
self.httpOnly = copyFrom.httpOnly
}
}
/// This enum is used to indicate the result of initializing the session.
public enum SessionResult {
/// No session initialization result.
case None
/// The session existed and its values were loaded.
case Load
/// The session did not exist but was created anew.
case New
/// The session existed and its id was rotated.
case Rotate
/// The session existed but had expired and was created anew.
case Expire
}
/// This class is used to manage an individual session. One is a acquired through the current WebResponse object given to a page handler.
/// Session related variables can be set and retrieved though this object. Any variables which are set will be persisted and made available the next time the session is loaded.
public class SessionManager {
static func initializeSessionsDatabase() throws {
try Dir(PerfectServer.staticPerfectServer.homeDir() + serverSQLiteDBs).create()
let sqlite = try SQLite(PerfectServer.staticPerfectServer.homeDir() + serverSQLiteDBs + perfectSessionDB)
sqlite.doWithClose {
do {
try sqlite.execute("CREATE TABLE IF NOT EXISTS sessions (" +
"id INTEGER PRIMARY KEY," +
"session_key TEXT NOT NULL UNIQUE," +
"data BLOB," +
"last_access TEXT," +
"expire_minutes INTEGER DEFAULT 15" +
")")
} catch {
}
}
}
public typealias Key = JSONDictionaryType.Key
public typealias Value = JSONDictionaryType.Value
var dictionary: JSONDictionaryType?
var configuration: SessionConfiguration
var result = SessionResult.None
internal init(_ configuration: SessionConfiguration) {
self.configuration = configuration
let name = configuration.name
let key = configuration.id
let fullKey = name + ":" + key
// load values
do {
let sqlite = try SQLite(PerfectServer.staticPerfectServer.homeDir() + serverSQLiteDBs + perfectSessionDB)
defer { sqlite.close() }
try sqlite.execute("BEGIN")
try sqlite.forEachRow("SELECT data,last_access,expire_minutes FROM sessions WHERE session_key = ?",
doBindings: {
(stmt: SQLiteStmt) throws -> () in
try stmt.bind(1, fullKey)
},
handleRow: {
(stmt: SQLiteStmt, count: Int) -> () in
do {
let lastAccess = stmt.columnDouble(1)
let expireMinutes = stmt.columnDouble(2)
let now = self.getNowSeconds()
let minutesSinceAccess = (now - lastAccess) / 60
if minutesSinceAccess > expireMinutes {
try sqlite.execute("DELETE FROM sessions WHERE session_key = ?", doBindings: {
(stmt: SQLiteStmt) -> () in
try stmt.bind(1, fullKey)
self.result = .Expire
})
} else {
self.result = .Load
let data = stmt.columnText(0)
self.dictionary = try JSONDecode().decode(data) as? JSONDictionaryType
}
} catch {}
})
try sqlite.execute("COMMIT")
} catch {
}
if self.dictionary == nil {
self.dictionary = JSONDictionaryType()
if self.result == .None {
self.result = .New
}
} else if self.configuration.rotate {
self.result = .Rotate
self.configuration.id = SessionManager.generateSessionKey()
}
}
/// !FIX! needs to support all the special cookie options
func initializeForResponse(response: WebResponse) {
let c = Cookie()
c.name = perfectSessionNamePrefix + self.configuration.name
c.value = self.configuration.id
c.expiresIn = Double(self.configuration.cookieExpires)
response.addCookie(c)
}
/// Get the `SessionResult` for the current session.
public func getLoadResult() -> SessionResult {
return result
}
/// Get the `SessionConfiguration` which was used to intialize the current session.
public func getConfiguration() -> SessionConfiguration {
return self.configuration
}
func getNowSeconds() -> Double {
return Double(ICU.icuDateToSeconds(ICU.getNow()))
}
func commit() throws {
// save values
let fullKey = self.configuration.name + ":" + self.configuration.id
let encoded = try JSONEncode().encode(self.dictionary!)
let sqlite = try SQLite(PerfectServer.staticPerfectServer.homeDir() + serverSQLiteDBs + perfectSessionDB)
defer { sqlite.close() }
try sqlite.execute("INSERT OR REPLACE INTO sessions (data,last_access,expire_minutes,session_key) " +
"VALUES (?,?,?,?)", doBindings: {
(stmt: SQLiteStmt) -> () in
try stmt.bind(1, UTF8Encoding.decode(encoded))
try stmt.bind(2, self.getNowSeconds())
try stmt.bind(3, self.configuration.expires)
try stmt.bind(4, fullKey)
})
}
/// Get a session variable by name.
/// - parameter key: The name of the session variable.
/// - returns: The session variable's value or nil if no variable existed with the provided name.
public subscript(key: Key) -> Value? {
get {
return dictionary![key]
}
set(newValue) {
dictionary![key] = newValue
}
}
/// Get a session variable based on its name while also proving a default value.
/// If the indicated variable does not already exist, it will be created with the indicated value.
/// - parameter key: The name of the session variable.
/// - parameter defaultValue: The default value for the variable which will be used if the variable did not already exist.
/// - returns: The session variable value.
public func getVar<T: Value>(key: Key, defaultValue: T) -> T {
if let test = self[key] as? T {
return test
}
dictionary![key] = defaultValue
return defaultValue
}
/// Generate a presumably unique session id
static public func generateSessionKey() -> String {
return NSUUID().UUIDString
}
}