connect-memcached 0.0.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +2 -4
- package/LICENSE +22 -0
- package/Readme.md +56 -46
- package/lib/connect-memcached.js +166 -76
- package/package.json +23 -11
- package/tests/test.js +34 -0
- package/tests/test_encrypt.js +35 -0
- package/History.md +0 -14
- package/Makefile +0 -2
- package/doc.json +0 -333
- package/lib/_connect-memcached.js +0 -133
package/.npmignore
CHANGED
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
(The MIT License)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2014 Michał Thoma <michal@balor.pl>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/Readme.md
CHANGED
|
@@ -1,71 +1,81 @@
|
|
|
1
|
-
|
|
2
1
|
# connect-memcached
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Memcached session store, using [node-memcached](http://github.com/3rd-Eden/node-memcached) for communication with cache server.
|
|
5
4
|
|
|
6
5
|
## Installation
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
via npm:
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```bash
|
|
10
|
+
$ npm install connect-memcached
|
|
11
|
+
```
|
|
11
12
|
|
|
12
13
|
## Example
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
```javascript
|
|
16
|
+
var express = require("express"),
|
|
17
|
+
session = require("express-session"),
|
|
18
|
+
cookieParser = require("cookie-parser"),
|
|
19
|
+
http = require("http"),
|
|
20
|
+
app = express(),
|
|
21
|
+
MemcachedStore = require("connect-memcached")(session);
|
|
22
|
+
|
|
23
|
+
app.use(cookieParser());
|
|
24
|
+
app.use(
|
|
25
|
+
session({
|
|
26
|
+
secret: "CatOnKeyboard",
|
|
27
|
+
key: "test",
|
|
28
|
+
proxy: "true",
|
|
29
|
+
resave: false,
|
|
30
|
+
saveUninitialized: false,
|
|
31
|
+
store: new MemcachedStore({
|
|
32
|
+
hosts: ["127.0.0.1:11211"],
|
|
33
|
+
secret: "123, easy as ABC. ABC, easy as 123" // Optionally use transparent encryption for memcache session data
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
app.get("/", function(req, res) {
|
|
39
|
+
if (req.session.views) {
|
|
40
|
+
++req.session.views;
|
|
41
|
+
} else {
|
|
42
|
+
req.session.views = 1;
|
|
43
|
+
}
|
|
44
|
+
res.send("Viewed <strong>" + req.session.views + "</strong> times.");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
http.createServer(app).listen(9341, function() {
|
|
48
|
+
console.log("Listening on %d", this.address().port);
|
|
49
|
+
});
|
|
50
|
+
```
|
|
25
51
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// request logging
|
|
29
|
-
app.use(express.logger());
|
|
52
|
+
## Options
|
|
30
53
|
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
- `hosts` Memcached servers locations, can be string, array, hash.
|
|
55
|
+
- `prefix` An optional prefix for each memcache key, in case you are sharing your memcached servers with something generating its own keys.
|
|
56
|
+
- `ttl` An optional parameter used for setting the default TTL (in seconds)
|
|
57
|
+
- `secret` An optional secret can be used to encrypt/decrypt session contents.
|
|
58
|
+
- `algorithm` An optional algorithm parameter may be used, but must be valid based on returned `crypto.getCiphers()`. The current default is `aes-256-ctr` and was chosen based on the following [information](http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html)
|
|
59
|
+
- ... Rest of given option will be passed directly to the node-memcached constructor.
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
// - req.session
|
|
36
|
-
// - req.sessionStore
|
|
37
|
-
// - req.sessionID (or req.session.id)
|
|
61
|
+
For details see [node-memcached](http://github.com/3rd-Eden/node-memcached).
|
|
38
62
|
|
|
39
|
-
|
|
40
|
-
secret: 'CatOnTheKeyboard',
|
|
41
|
-
store: new MemcachedStore
|
|
42
|
-
}));
|
|
63
|
+
## Upgrading from v0.x.x -> v1.x.x
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
if (req.session.views) {
|
|
46
|
-
++req.session.views;
|
|
47
|
-
} else {
|
|
48
|
-
req.session.views = 1;
|
|
49
|
-
}
|
|
50
|
-
res.send('Viewed <strong>' + req.session.views + '</strong> times.');
|
|
51
|
-
});
|
|
65
|
+
If You're upgrading from the pre v1.0.0 version of this library and use encryption for session data be sure to **remove all session entries created with previous version**.
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
console.log('Express app started on port 3000');
|
|
67
|
+
Upgrading library without taking appropriate action will result in `SyntaxError` exceptions during JSON parsing of decoded entries.
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
Sessions without encryption are not affected.
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
- ... Rest of given option will be passed directly to the node-memcached constructor.
|
|
71
|
+
## Contributors
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
Big thanks for the contributors! See the actual list [here](https://github.com/balor/connect-memcached/graphs/contributors)!
|
|
62
74
|
|
|
63
|
-
## License
|
|
75
|
+
## License
|
|
64
76
|
|
|
65
77
|
(The MIT License)
|
|
66
78
|
|
|
67
|
-
Copyright (c) 2011 Michał Thoma <michal@balor.pl>
|
|
68
|
-
|
|
69
79
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
70
80
|
a copy of this software and associated documentation files (the
|
|
71
81
|
'Software'), to deal in the Software without restriction, including
|
package/lib/connect-memcached.js
CHANGED
|
@@ -1,46 +1,27 @@
|
|
|
1
|
-
|
|
2
1
|
/*!
|
|
3
2
|
* connect-memcached
|
|
4
|
-
* Copyright(c) 2012 Michał Thoma <michal@balor.pl>
|
|
5
3
|
* MIT Licensed
|
|
6
4
|
*/
|
|
5
|
+
const bufferFrom = require('buffer-from');
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
* Library version.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
exports.version = '0.0.3';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Module dependencies.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
var Memcached = require('memcached');
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* One day in seconds.
|
|
23
|
-
*/
|
|
24
|
-
|
|
7
|
+
var Memcached = require("memcached");
|
|
25
8
|
var oneDay = 86400;
|
|
26
9
|
|
|
27
|
-
|
|
10
|
+
function ensureCallback(fn) {
|
|
11
|
+
return function() {
|
|
12
|
+
fn && fn.apply(null, arguments);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
28
15
|
|
|
29
16
|
/**
|
|
30
17
|
* Return the `MemcachedStore` extending `connect`'s session Store.
|
|
31
|
-
*
|
|
32
|
-
* @param {object}
|
|
18
|
+
*
|
|
19
|
+
* @param {object} session
|
|
33
20
|
* @return {Function}
|
|
34
21
|
* @api public
|
|
35
22
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Connect's Store.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
var Store = connect.session.Store;
|
|
23
|
+
module.exports = function(session) {
|
|
24
|
+
var Store = session.Store;
|
|
44
25
|
|
|
45
26
|
/**
|
|
46
27
|
* Initialize MemcachedStore with the given `options`.
|
|
@@ -48,47 +29,70 @@ module.exports = function(connect){
|
|
|
48
29
|
* @param {Object} options
|
|
49
30
|
* @api public
|
|
50
31
|
*/
|
|
51
|
-
|
|
52
32
|
function MemcachedStore(options) {
|
|
53
33
|
options = options || {};
|
|
54
34
|
Store.call(this, options);
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
|
|
36
|
+
this.prefix = options.prefix || "";
|
|
37
|
+
this.ttl = options.ttl;
|
|
38
|
+
if (!options.client) {
|
|
39
|
+
if (!options.hosts) {
|
|
40
|
+
options.hosts = "127.0.0.1:11211";
|
|
41
|
+
}
|
|
42
|
+
if (options.secret) {
|
|
43
|
+
(this.crypto = require("crypto")), (this.secret = options.secret);
|
|
44
|
+
}
|
|
45
|
+
if (options.algorithm) {
|
|
46
|
+
this.algorithm = options.algorithm;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
options.client = new Memcached(options.hosts, options);
|
|
57
50
|
}
|
|
58
|
-
this.client = new Memcached(options.hosts, options);
|
|
59
|
-
console.log("MemcachedStore initialized for servers: " + options.hosts);
|
|
60
51
|
|
|
61
|
-
this.client
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
52
|
+
this.client = options.client;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
MemcachedStore.prototype.__proto__ = Store.prototype;
|
|
66
56
|
|
|
67
57
|
/**
|
|
68
|
-
*
|
|
58
|
+
* Translates the given `sid` into a memcached key, optionally with prefix.
|
|
59
|
+
*
|
|
60
|
+
* @param {String} sid
|
|
61
|
+
* @api private
|
|
69
62
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
MemcachedStore.prototype.getKey = function getKey(sid) {
|
|
64
|
+
return this.prefix + sid;
|
|
65
|
+
};
|
|
72
66
|
|
|
73
67
|
/**
|
|
74
68
|
* Attempt to fetch session by the given `sid`.
|
|
75
|
-
*
|
|
69
|
+
*
|
|
76
70
|
* @param {String} sid
|
|
77
71
|
* @param {Function} fn
|
|
78
72
|
* @api public
|
|
79
73
|
*/
|
|
80
|
-
|
|
81
74
|
MemcachedStore.prototype.get = function(sid, fn) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
75
|
+
(secret = this.secret), (self = this), (sid = this.getKey(sid));
|
|
76
|
+
|
|
77
|
+
this.client.get(sid, function(err, data) {
|
|
78
|
+
if (err) {
|
|
79
|
+
return fn(err, {});
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
if (!data) {
|
|
83
|
+
return fn();
|
|
84
|
+
}
|
|
85
|
+
if (secret) {
|
|
86
|
+
parseable_string = decryptData.call(self, data.toString());
|
|
87
|
+
} else {
|
|
88
|
+
parseable_string = data.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn(null, JSON.parse(parseable_string));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
fn(e);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
92
96
|
};
|
|
93
97
|
|
|
94
98
|
/**
|
|
@@ -99,30 +103,40 @@ module.exports = function(connect){
|
|
|
99
103
|
* @param {Function} fn
|
|
100
104
|
* @api public
|
|
101
105
|
*/
|
|
102
|
-
|
|
103
106
|
MemcachedStore.prototype.set = function(sid, sess, fn) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
sid = this.getKey(sid);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
var maxAge = sess.cookie.maxAge;
|
|
111
|
+
var ttl =
|
|
112
|
+
this.ttl || ("number" == typeof maxAge ? (maxAge / 1000) | 0 : oneDay);
|
|
113
|
+
var sess = JSON.stringify(
|
|
114
|
+
this.secret
|
|
115
|
+
? encryptData.call(
|
|
116
|
+
this,
|
|
117
|
+
JSON.stringify(sess),
|
|
118
|
+
this.secret,
|
|
119
|
+
this.algorithm
|
|
120
|
+
)
|
|
121
|
+
: sess
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
this.client.set(sid, sess, ttl, ensureCallback(fn));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
fn && fn(err);
|
|
127
|
+
}
|
|
115
128
|
};
|
|
116
129
|
|
|
117
130
|
/**
|
|
118
131
|
* Destroy the session associated with the given `sid`.
|
|
119
|
-
*
|
|
132
|
+
*
|
|
120
133
|
* @param {String} sid
|
|
134
|
+
* @param {Function} fn
|
|
121
135
|
* @api public
|
|
122
136
|
*/
|
|
123
|
-
|
|
124
137
|
MemcachedStore.prototype.destroy = function(sid, fn) {
|
|
125
|
-
|
|
138
|
+
sid = this.getKey(sid);
|
|
139
|
+
this.client.del(sid, ensureCallback(fn));
|
|
126
140
|
};
|
|
127
141
|
|
|
128
142
|
/**
|
|
@@ -131,21 +145,97 @@ module.exports = function(connect){
|
|
|
131
145
|
* @param {Function} fn
|
|
132
146
|
* @api public
|
|
133
147
|
*/
|
|
134
|
-
|
|
135
148
|
MemcachedStore.prototype.length = function(fn) {
|
|
136
|
-
|
|
149
|
+
this.client.items(ensureCallback(fn));
|
|
137
150
|
};
|
|
138
151
|
|
|
139
152
|
/**
|
|
140
153
|
* Clear all sessions.
|
|
141
|
-
*
|
|
154
|
+
*
|
|
142
155
|
* @param {Function} fn
|
|
143
156
|
* @api public
|
|
144
|
-
*/
|
|
145
|
-
|
|
157
|
+
*/
|
|
146
158
|
MemcachedStore.prototype.clear = function(fn) {
|
|
147
|
-
|
|
159
|
+
this.client.flush(ensureCallback(fn));
|
|
148
160
|
};
|
|
149
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Refresh the time-to-live for the session with the given `sid`.
|
|
164
|
+
*
|
|
165
|
+
* @param {String} sid
|
|
166
|
+
* @param {Session} sess
|
|
167
|
+
* @param {Function} fn
|
|
168
|
+
* @api public
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
MemcachedStore.prototype.touch = function(sid, sess, fn) {
|
|
172
|
+
this.set(sid, sess, fn);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function encryptData(plaintext) {
|
|
176
|
+
var pt = encrypt.call(this, this.secret, plaintext, this.algo),
|
|
177
|
+
hmac = digest.call(this, this.secret, pt);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
ct: pt,
|
|
181
|
+
mac: hmac
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function decryptData(ciphertext) {
|
|
186
|
+
ciphertext = JSON.parse(ciphertext);
|
|
187
|
+
|
|
188
|
+
var hmac = digest.call(this, this.secret, ciphertext.ct);
|
|
189
|
+
|
|
190
|
+
if (hmac != ciphertext.mac) {
|
|
191
|
+
throw "Encrypted session was tampered with!";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return decrypt.call(this, this.secret, ciphertext.ct, this.algo);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function digest(key, obj) {
|
|
198
|
+
var hmac = this.crypto.createHmac("sha512", key);
|
|
199
|
+
hmac.setEncoding("hex");
|
|
200
|
+
hmac.write(obj);
|
|
201
|
+
hmac.end();
|
|
202
|
+
return hmac.read();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function encrypt(key, pt, algo) {
|
|
206
|
+
algo = algo || "aes-256-ctr";
|
|
207
|
+
pt = Buffer.isBuffer(pt) ? pt : new bufferFrom(pt);
|
|
208
|
+
var iv = this.crypto.randomBytes(16);
|
|
209
|
+
var hashedKey = this.crypto
|
|
210
|
+
.createHash("sha256")
|
|
211
|
+
.update(key)
|
|
212
|
+
.digest();
|
|
213
|
+
var cipher = this.crypto.createCipheriv(algo, hashedKey, iv),
|
|
214
|
+
ct = [];
|
|
215
|
+
ct.push(iv.toString("hex"));
|
|
216
|
+
ct.push(cipher.update(pt, "buffer", "hex"));
|
|
217
|
+
ct.push(cipher.final("hex"));
|
|
218
|
+
|
|
219
|
+
return ct.join("");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function decrypt(key, ct, algo) {
|
|
223
|
+
algo = algo || "aes-256-ctr";
|
|
224
|
+
var dataBuffer = bufferFrom(ct, "hex");
|
|
225
|
+
var iv = dataBuffer.slice(0, 16);
|
|
226
|
+
var hashedKey = this.crypto
|
|
227
|
+
.createHash("sha256")
|
|
228
|
+
.update(key)
|
|
229
|
+
.digest();
|
|
230
|
+
|
|
231
|
+
var cipher = this.crypto.createDecipheriv(algo, hashedKey, iv),
|
|
232
|
+
pt = [];
|
|
233
|
+
|
|
234
|
+
pt.push(cipher.update(dataBuffer.slice(16), "hex", "utf8"));
|
|
235
|
+
pt.push(cipher.final("utf8"));
|
|
236
|
+
|
|
237
|
+
return pt.join("");
|
|
238
|
+
}
|
|
239
|
+
|
|
150
240
|
return MemcachedStore;
|
|
151
241
|
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
"name": "connect-memcached",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Memcached session store for Connect",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"memcached",
|
|
7
|
+
"connection",
|
|
8
|
+
"session",
|
|
9
|
+
"store",
|
|
10
|
+
"cache"
|
|
11
|
+
],
|
|
12
|
+
"author": "Michał Thoma <me@balor.pl>",
|
|
13
|
+
"repository": {
|
|
8
14
|
"type": "git",
|
|
9
15
|
"url": "https://github.com/balor/connect-memcached"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"buffer-from": "1.1.0",
|
|
19
|
+
"memcached": "2.2.x"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">= 0.10.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"directories": {
|
|
26
|
+
"lib": "./lib"
|
|
10
27
|
}
|
|
11
|
-
, "dependencies": { "memcached": ">= 0.0.1" }
|
|
12
|
-
, "devDependencies": { "connect": ">= 1.4.x" }
|
|
13
|
-
, "main": "index"
|
|
14
|
-
, "engines": { "node": ">=0.4.7" }
|
|
15
|
-
, "directories": { "lib": "./lib" }
|
|
16
28
|
}
|
package/tests/test.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
var express = require("express"),
|
|
2
|
+
session = require("express-session"),
|
|
3
|
+
cookieParser = require("cookie-parser"),
|
|
4
|
+
http = require("http"),
|
|
5
|
+
app = express(),
|
|
6
|
+
MemcachedStore = require("../lib/connect-memcached")(session);
|
|
7
|
+
|
|
8
|
+
app.use(cookieParser());
|
|
9
|
+
app.use(
|
|
10
|
+
session({
|
|
11
|
+
secret: "TestSecret",
|
|
12
|
+
key: "test",
|
|
13
|
+
proxy: "true",
|
|
14
|
+
resave: false,
|
|
15
|
+
saveUninitialized: false,
|
|
16
|
+
store: new MemcachedStore({
|
|
17
|
+
hosts: ["127.0.0.1:11211"],
|
|
18
|
+
prefix: "testapp_"
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
app.get("/", function(req, res) {
|
|
24
|
+
if (req.session.views) {
|
|
25
|
+
++req.session.views;
|
|
26
|
+
} else {
|
|
27
|
+
req.session.views = 1;
|
|
28
|
+
}
|
|
29
|
+
res.send("Viewed <strong>" + req.session.views + "</strong> times.");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
http.createServer(app).listen(9341, function() {
|
|
33
|
+
console.log("Listening on %d", this.address().port);
|
|
34
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
var express = require("express"),
|
|
2
|
+
session = require("express-session"),
|
|
3
|
+
cookieParser = require("cookie-parser"),
|
|
4
|
+
http = require("http"),
|
|
5
|
+
app = express(),
|
|
6
|
+
MemcachedStore = require("../lib/connect-memcached")(session);
|
|
7
|
+
|
|
8
|
+
app.use(cookieParser());
|
|
9
|
+
app.use(
|
|
10
|
+
session({
|
|
11
|
+
secret: "TestEncryptSecret",
|
|
12
|
+
key: "test_encrypt",
|
|
13
|
+
proxy: "true",
|
|
14
|
+
resave: false,
|
|
15
|
+
saveUninitialized: false,
|
|
16
|
+
store: new MemcachedStore({
|
|
17
|
+
hosts: ["127.0.0.1:11211"],
|
|
18
|
+
secret: "Hello there stranger!",
|
|
19
|
+
prefix: "testapp_encrypt_"
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
app.get("/", function(req, res) {
|
|
25
|
+
if (req.session.views) {
|
|
26
|
+
++req.session.views;
|
|
27
|
+
} else {
|
|
28
|
+
req.session.views = 1;
|
|
29
|
+
}
|
|
30
|
+
res.send("Viewed <strong>" + req.session.views + "</strong> times.");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
http.createServer(app).listen(9341, function() {
|
|
34
|
+
console.log("Listening on %d", this.address().port);
|
|
35
|
+
});
|
package/History.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
0.0.3 / 2012-03-06
|
|
2
|
-
==================
|
|
3
|
-
|
|
4
|
-
* Rewritten the module due to npm 1.x changes (we now need to pass connect to the function connect-memcached exports in order to extend connect.session.Store:
|
|
5
|
-
|
|
6
|
-
0.0.2 / 2010-01-03
|
|
7
|
-
==================
|
|
8
|
-
|
|
9
|
-
* Created basic version that actually works
|
|
10
|
-
|
|
11
|
-
0.0.1 / 2010-01-03
|
|
12
|
-
==================
|
|
13
|
-
|
|
14
|
-
* Initial release
|
package/Makefile
DELETED
package/doc.json
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"tags": [],
|
|
4
|
-
"description": {
|
|
5
|
-
"full": "<p>connect-memcached<br />Copyright(c) 2012 Michał Thoma <a href=\"mailto:michal@balor.pl\">michal@balor.pl</a><br />MIT Licensed</p>",
|
|
6
|
-
"summary": "<p>connect-memcached<br />Copyright(c) 2012 Michał Thoma <a href=\"mailto:michal@balor.pl\">michal@balor.pl</a><br />MIT Licensed</p>",
|
|
7
|
-
"body": ""
|
|
8
|
-
},
|
|
9
|
-
"ignore": true
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"tags": [],
|
|
13
|
-
"description": {
|
|
14
|
-
"full": "<p>Library version.</p>",
|
|
15
|
-
"summary": "<p>Library version.</p>",
|
|
16
|
-
"body": ""
|
|
17
|
-
},
|
|
18
|
-
"ignore": false,
|
|
19
|
-
"code": "exports.version = '0.0.3';",
|
|
20
|
-
"ctx": {
|
|
21
|
-
"type": "property",
|
|
22
|
-
"receiver": "exports",
|
|
23
|
-
"name": "version",
|
|
24
|
-
"value": "'0.0.3'",
|
|
25
|
-
"string": "exports.version"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"tags": [],
|
|
30
|
-
"description": {
|
|
31
|
-
"full": "<p>Module dependencies.</p>",
|
|
32
|
-
"summary": "<p>Module dependencies.</p>",
|
|
33
|
-
"body": ""
|
|
34
|
-
},
|
|
35
|
-
"ignore": false,
|
|
36
|
-
"code": "var Memcached = require('memcached');",
|
|
37
|
-
"ctx": {
|
|
38
|
-
"type": "declaration",
|
|
39
|
-
"name": "Memcached",
|
|
40
|
-
"value": "require('memcached')",
|
|
41
|
-
"string": "Memcached"
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"tags": [],
|
|
46
|
-
"description": {
|
|
47
|
-
"full": "<p>One day in seconds.</p>",
|
|
48
|
-
"summary": "<p>One day in seconds.</p>",
|
|
49
|
-
"body": ""
|
|
50
|
-
},
|
|
51
|
-
"ignore": false,
|
|
52
|
-
"code": "var oneDay = 86400;",
|
|
53
|
-
"ctx": {
|
|
54
|
-
"type": "declaration",
|
|
55
|
-
"name": "oneDay",
|
|
56
|
-
"value": "86400",
|
|
57
|
-
"string": "oneDay"
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"tags": [
|
|
62
|
-
{
|
|
63
|
-
"type": "param",
|
|
64
|
-
"types": [
|
|
65
|
-
"object"
|
|
66
|
-
],
|
|
67
|
-
"name": "connect",
|
|
68
|
-
"description": ""
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"type": "return",
|
|
72
|
-
"types": [
|
|
73
|
-
"Function"
|
|
74
|
-
],
|
|
75
|
-
"description": ""
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
"type": "api",
|
|
79
|
-
"visibility": "public"
|
|
80
|
-
}
|
|
81
|
-
],
|
|
82
|
-
"description": {
|
|
83
|
-
"full": "<p>Return the <code>MemcachedStore</code> extending <code>connect</code>'s session Store.</p>",
|
|
84
|
-
"summary": "<p>Return the <code>MemcachedStore</code> extending <code>connect</code>'s session Store.</p>",
|
|
85
|
-
"body": ""
|
|
86
|
-
},
|
|
87
|
-
"isPrivate": false,
|
|
88
|
-
"ignore": false,
|
|
89
|
-
"code": "module.exports = function(connect){",
|
|
90
|
-
"ctx": {
|
|
91
|
-
"type": "method",
|
|
92
|
-
"receiver": "module",
|
|
93
|
-
"name": "exports",
|
|
94
|
-
"string": "module.exports()"
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
"tags": [],
|
|
99
|
-
"description": {
|
|
100
|
-
"full": "<p>Connect's Store.</p>",
|
|
101
|
-
"summary": "<p>Connect's Store.</p>",
|
|
102
|
-
"body": ""
|
|
103
|
-
},
|
|
104
|
-
"ignore": false,
|
|
105
|
-
"code": "var Store = connect.session.Store;",
|
|
106
|
-
"ctx": {
|
|
107
|
-
"type": "declaration",
|
|
108
|
-
"name": "Store",
|
|
109
|
-
"value": "connect.session.Store",
|
|
110
|
-
"string": "Store"
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
"tags": [
|
|
115
|
-
{
|
|
116
|
-
"type": "param",
|
|
117
|
-
"types": [
|
|
118
|
-
"Object"
|
|
119
|
-
],
|
|
120
|
-
"name": "options",
|
|
121
|
-
"description": ""
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"type": "api",
|
|
125
|
-
"visibility": "public"
|
|
126
|
-
}
|
|
127
|
-
],
|
|
128
|
-
"description": {
|
|
129
|
-
"full": "<p>Initialize MemcachedStore with the given <code>options</code>.</p>",
|
|
130
|
-
"summary": "<p>Initialize MemcachedStore with the given <code>options</code>.</p>",
|
|
131
|
-
"body": ""
|
|
132
|
-
},
|
|
133
|
-
"isPrivate": false,
|
|
134
|
-
"ignore": false,
|
|
135
|
-
"code": "function MemcachedStore(options) {\n options = options || {};\n Store.call(this, options);\n if (!options.hosts) {\n options.hosts = '127.0.0.1:11211';\n }\n this.client = new Memcached(options.hosts, options);\n console.log(\"MemcachedStore initialized for servers: \" + options.hosts);\n\n this.client.on(\"issue\", function(issue) {\n console.log(\"MemcachedStore::Issue @ \" + issue.server + \": \" + \n issue.messages + \", \" + issue.retries + \" attempts left\");\n });\n };",
|
|
136
|
-
"ctx": {
|
|
137
|
-
"type": "function",
|
|
138
|
-
"name": "MemcachedStore",
|
|
139
|
-
"string": "MemcachedStore()"
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
"tags": [],
|
|
144
|
-
"description": {
|
|
145
|
-
"full": "<p>Inherit from <code>Store</code>.</p>",
|
|
146
|
-
"summary": "<p>Inherit from <code>Store</code>.</p>",
|
|
147
|
-
"body": ""
|
|
148
|
-
},
|
|
149
|
-
"ignore": false,
|
|
150
|
-
"code": "MemcachedStore.prototype.__proto__ = Store.prototype;",
|
|
151
|
-
"ctx": {
|
|
152
|
-
"type": "property",
|
|
153
|
-
"constructor": "MemcachedStore",
|
|
154
|
-
"name": "__proto__",
|
|
155
|
-
"value": "Store.prototype",
|
|
156
|
-
"string": "MemcachedStore.prototype__proto__"
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
"tags": [
|
|
161
|
-
{
|
|
162
|
-
"type": "param",
|
|
163
|
-
"types": [
|
|
164
|
-
"String"
|
|
165
|
-
],
|
|
166
|
-
"name": "sid",
|
|
167
|
-
"description": ""
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
"type": "param",
|
|
171
|
-
"types": [
|
|
172
|
-
"Function"
|
|
173
|
-
],
|
|
174
|
-
"name": "fn",
|
|
175
|
-
"description": ""
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
"type": "api",
|
|
179
|
-
"visibility": "public"
|
|
180
|
-
}
|
|
181
|
-
],
|
|
182
|
-
"description": {
|
|
183
|
-
"full": "<p>Attempt to fetch session by the given <code>sid</code>.</p>",
|
|
184
|
-
"summary": "<p>Attempt to fetch session by the given <code>sid</code>.</p>",
|
|
185
|
-
"body": ""
|
|
186
|
-
},
|
|
187
|
-
"isPrivate": false,
|
|
188
|
-
"ignore": false,
|
|
189
|
-
"code": "MemcachedStore.prototype.get = function(sid, fn) {\n this.client.get(sid, function(err, data) {\n try {\n if (!data) {\n return fn();\n }\n fn(null, JSON.parse(data.toString()));\n } catch (err) {\n fn(err);\n } \n });\n };",
|
|
190
|
-
"ctx": {
|
|
191
|
-
"type": "method",
|
|
192
|
-
"constructor": "MemcachedStore",
|
|
193
|
-
"name": "get",
|
|
194
|
-
"string": "MemcachedStore.prototype.get()"
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
"tags": [
|
|
199
|
-
{
|
|
200
|
-
"type": "param",
|
|
201
|
-
"types": [
|
|
202
|
-
"String"
|
|
203
|
-
],
|
|
204
|
-
"name": "sid",
|
|
205
|
-
"description": ""
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
"type": "param",
|
|
209
|
-
"types": [
|
|
210
|
-
"Session"
|
|
211
|
-
],
|
|
212
|
-
"name": "sess",
|
|
213
|
-
"description": ""
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
"type": "param",
|
|
217
|
-
"types": [
|
|
218
|
-
"Function"
|
|
219
|
-
],
|
|
220
|
-
"name": "fn",
|
|
221
|
-
"description": ""
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
"type": "api",
|
|
225
|
-
"visibility": "public"
|
|
226
|
-
}
|
|
227
|
-
],
|
|
228
|
-
"description": {
|
|
229
|
-
"full": "<p>Commit the given <code>sess</code> object associated with the given <code>sid</code>.</p>",
|
|
230
|
-
"summary": "<p>Commit the given <code>sess</code> object associated with the given <code>sid</code>.</p>",
|
|
231
|
-
"body": ""
|
|
232
|
-
},
|
|
233
|
-
"isPrivate": false,
|
|
234
|
-
"ignore": false,
|
|
235
|
-
"code": "MemcachedStore.prototype.set = function(sid, sess, fn) {\n try {\n var maxAge = sess.cookie.maxAge\n var ttl = 'number' == typeof maxAge ? maxAge / 1000 | 0 : oneDay\n var sess = JSON.stringify(sess);\n\n this.client.set(sid, sess, ttl, function() {\n fn && fn.apply(this, arguments);\n });\n } catch (err) {\n fn && fn(err);\n } \n };",
|
|
236
|
-
"ctx": {
|
|
237
|
-
"type": "method",
|
|
238
|
-
"constructor": "MemcachedStore",
|
|
239
|
-
"name": "set",
|
|
240
|
-
"string": "MemcachedStore.prototype.set()"
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
"tags": [
|
|
245
|
-
{
|
|
246
|
-
"type": "param",
|
|
247
|
-
"types": [
|
|
248
|
-
"String"
|
|
249
|
-
],
|
|
250
|
-
"name": "sid",
|
|
251
|
-
"description": ""
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
"type": "api",
|
|
255
|
-
"visibility": "public"
|
|
256
|
-
}
|
|
257
|
-
],
|
|
258
|
-
"description": {
|
|
259
|
-
"full": "<p>Destroy the session associated with the given <code>sid</code>.</p>",
|
|
260
|
-
"summary": "<p>Destroy the session associated with the given <code>sid</code>.</p>",
|
|
261
|
-
"body": ""
|
|
262
|
-
},
|
|
263
|
-
"isPrivate": false,
|
|
264
|
-
"ignore": false,
|
|
265
|
-
"code": "MemcachedStore.prototype.destroy = function(sid, fn) {\n this.client.del(sid, fn);\n };",
|
|
266
|
-
"ctx": {
|
|
267
|
-
"type": "method",
|
|
268
|
-
"constructor": "MemcachedStore",
|
|
269
|
-
"name": "destroy",
|
|
270
|
-
"string": "MemcachedStore.prototype.destroy()"
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
"tags": [
|
|
275
|
-
{
|
|
276
|
-
"type": "param",
|
|
277
|
-
"types": [
|
|
278
|
-
"Function"
|
|
279
|
-
],
|
|
280
|
-
"name": "fn",
|
|
281
|
-
"description": ""
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
"type": "api",
|
|
285
|
-
"visibility": "public"
|
|
286
|
-
}
|
|
287
|
-
],
|
|
288
|
-
"description": {
|
|
289
|
-
"full": "<p>Fetch number of sessions.</p>",
|
|
290
|
-
"summary": "<p>Fetch number of sessions.</p>",
|
|
291
|
-
"body": ""
|
|
292
|
-
},
|
|
293
|
-
"isPrivate": false,
|
|
294
|
-
"ignore": false,
|
|
295
|
-
"code": "MemcachedStore.prototype.length = function(fn) {\n this.client.items(fn);\n };",
|
|
296
|
-
"ctx": {
|
|
297
|
-
"type": "method",
|
|
298
|
-
"constructor": "MemcachedStore",
|
|
299
|
-
"name": "length",
|
|
300
|
-
"string": "MemcachedStore.prototype.length()"
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
"tags": [
|
|
305
|
-
{
|
|
306
|
-
"type": "param",
|
|
307
|
-
"types": [
|
|
308
|
-
"Function"
|
|
309
|
-
],
|
|
310
|
-
"name": "fn",
|
|
311
|
-
"description": ""
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"type": "api",
|
|
315
|
-
"visibility": "public"
|
|
316
|
-
}
|
|
317
|
-
],
|
|
318
|
-
"description": {
|
|
319
|
-
"full": "<p>Clear all sessions.</p>",
|
|
320
|
-
"summary": "<p>Clear all sessions.</p>",
|
|
321
|
-
"body": ""
|
|
322
|
-
},
|
|
323
|
-
"isPrivate": false,
|
|
324
|
-
"ignore": false,
|
|
325
|
-
"code": "MemcachedStore.prototype.clear = function(fn) {\n this.client.flush(fn);\n };\n\n return MemcachedStore;\n};",
|
|
326
|
-
"ctx": {
|
|
327
|
-
"type": "method",
|
|
328
|
-
"constructor": "MemcachedStore",
|
|
329
|
-
"name": "clear",
|
|
330
|
-
"string": "MemcachedStore.prototype.clear()"
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
]
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/*!
|
|
3
|
-
* connect-memcached
|
|
4
|
-
* Copyright(c) 2011 Michał Thoma <michal@balor.pl>
|
|
5
|
-
* MIT Licensed
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Library version.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
exports.version = '0.0.3';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Module dependencies.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
var Store = require('connect').session.Store;
|
|
20
|
-
var Memcached = require('memcached');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* One day in seconds.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
var oneDay = 86400;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Initialize MemcachedStore with the given `options`.
|
|
32
|
-
*
|
|
33
|
-
* @param {Object} options
|
|
34
|
-
* @api public
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
var MemcachedStore = module.exports = function MemcachedStore(options) {
|
|
38
|
-
options = options || {};
|
|
39
|
-
Store.call(this, options);
|
|
40
|
-
if (!options.hosts) {
|
|
41
|
-
options.hosts = '127.0.0.1:11211';
|
|
42
|
-
}
|
|
43
|
-
this.client = new Memcached(options.hosts, options);
|
|
44
|
-
console.log("MemcachedStore initialized for servers: " + options.hosts);
|
|
45
|
-
|
|
46
|
-
this.client.on("issue", function(issue) {
|
|
47
|
-
console.log("MemcachedStore::Issue @ " + issue.server + ": " +
|
|
48
|
-
issue.messages + ", " + issue.retries + " attempts left");
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Inherit from `Store`.
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
MemcachedStore.prototype.__proto__ = Store.prototype;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Attempt to fetch session by the given `sid`.
|
|
60
|
-
*
|
|
61
|
-
* @param {String} sid
|
|
62
|
-
* @param {Function} fn
|
|
63
|
-
* @api public
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
MemcachedStore.prototype.get = function(sid, fn) {
|
|
67
|
-
this.client.get(sid, function(err, data) {
|
|
68
|
-
try {
|
|
69
|
-
if (!data) {
|
|
70
|
-
return fn();
|
|
71
|
-
}
|
|
72
|
-
fn(null, JSON.parse(data.toString()));
|
|
73
|
-
} catch (err) {
|
|
74
|
-
fn(err);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Commit the given `sess` object associated with the given `sid`.
|
|
81
|
-
*
|
|
82
|
-
* @param {String} sid
|
|
83
|
-
* @param {Session} sess
|
|
84
|
-
* @param {Function} fn
|
|
85
|
-
* @api public
|
|
86
|
-
*/
|
|
87
|
-
|
|
88
|
-
MemcachedStore.prototype.set = function(sid, sess, fn) {
|
|
89
|
-
try {
|
|
90
|
-
var maxAge = sess.cookie.maxAge
|
|
91
|
-
var ttl = 'number' == typeof maxAge ? maxAge / 1000 | 0 : oneDay
|
|
92
|
-
var sess = JSON.stringify(sess);
|
|
93
|
-
|
|
94
|
-
this.client.set(sid, sess, ttl, function() {
|
|
95
|
-
fn && fn.apply(this, arguments);
|
|
96
|
-
});
|
|
97
|
-
} catch (err) {
|
|
98
|
-
fn && fn(err);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Destroy the session associated with the given `sid`.
|
|
104
|
-
*
|
|
105
|
-
* @param {String} sid
|
|
106
|
-
* @api public
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
MemcachedStore.prototype.destroy = function(sid, fn) {
|
|
110
|
-
this.client.del(sid, fn);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Fetch number of sessions.
|
|
115
|
-
*
|
|
116
|
-
* @param {Function} fn
|
|
117
|
-
* @api public
|
|
118
|
-
*/
|
|
119
|
-
|
|
120
|
-
MemcachedStore.prototype.length = function(fn) {
|
|
121
|
-
this.client.items(fn);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Clear all sessions.
|
|
126
|
-
*
|
|
127
|
-
* @param {Function} fn
|
|
128
|
-
* @api public
|
|
129
|
-
*/
|
|
130
|
-
|
|
131
|
-
MemcachedStore.prototype.clear = function(fn) {
|
|
132
|
-
this.client.flush(fn);
|
|
133
|
-
};
|