Skip to content

Commit

Permalink
Add TLS support.
Browse files Browse the repository at this point in the history
  • Loading branch information
QuChen88 committed Jul 26, 2023
1 parent 60f48e5 commit 4be7532
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 6 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Change Log.

## (2023-07-24)

### Features

* **memcache-client:**
* add TLS support which allows secure connection to a remote memcached server with TLS enabled
* add new client test cases for TLS

## (2022-09-07)

### Features
Expand All @@ -22,4 +30,4 @@ Updated versions in `package.json` for both **memcache-client** and **memcache-p

**Things to improve**

There can be some `any`s in either code base or test files, those should be changed to real types. typescript should be as typed as possible
There can be some `any`s in either code base or test files, those should be changed to real types. typescript should be as typed as possible
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This repo uses [lerna](https://lernajs.io/) to manage multiple packages.
3. run `npm ci`
4. run `npm run bootstrap`
5. optional: just to make sure everything is fine run `npm run build` and check if there's no error using that command
6. Testing: run `npm test`
## Publishing notes
- Recommended publish flow is to publish the modules individually using `npm publish`, can be improved to use lerna in the future

Expand Down
39 changes: 38 additions & 1 deletion packages/memcache-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ const options = {
connectTimeout: 8000, // connect to server timeout in ms
compressor: require("custom-compressor"),
logger: require("./custom-logger"),
Promise
Promise,
tls: {}
};

const client = new MemcacheClient(options);
Expand All @@ -192,6 +193,7 @@ const client = new MemcacheClient(options);
- `compressor` - **_optional_** a custom compressor for compressing the data. See [data compression](#data-compression) for more details.
- `logger` - **_optional_** Custom logger like this:
- `Promise` - **_optional_** Internally this module will try to find `bluebird` in your `node_modules` and fallback to `global.Promise`. You can set this option to force the Promise to use.
- `tls` - **_optional_** If set, defines the TLS options to make the client connect to server in TLS mode
```js
module.exports = {
Expand Down Expand Up @@ -269,6 +271,41 @@ You can also pass in `server.config` with the following options:
- `failedServerOutTime` - (ms) how long a failed server should be out before retrying it. Default 60000 ms (1 min).
- `keepLastServer` - (boolean) Keep at least one server even if it failed connection. Default `true`.
### TLS / SSL
If the memcached server is configured with TLS, you can make the client connect to it via specifying the `tls` ConnectionOptions.
For production environments, the server should be using a TLS certificate that is signed by a trusted public CA. In
this case you can simply do the following to create the client:
```js
const client = new MemcacheClient({server: "{server_hostname}:11211", tls: {}});
client.set("key", "value");
```
If the server requires client certificate authentication, you can do the following:
```js
import Fs from "fs";
const client = new MemcacheClient({server: "{server_hostname}:11211", tls: {
key: Fs.readFileSync("client-key.pem"),
cert: Fs.readFileSync("client-cert.pem"),
}});
client.set("key", "value");
```
If you are running the server with a self-signed certificate (i.e. for local developments), you can create the client
by specifying the CA certificate and disable hostname verification as follows:
```js
import Fs from "fs";
const client = new MemcacheClient({server: "localhost:11211", tls: {
ca: Fs.readFileSync("ca-cert.pem"),
checkServerIdentity: () => {return undefined;}
}});
client.set("key", "value");
```
### Data Compression
The client supports automatic compression/decompression of the data you set. It's turned off by default.
Expand Down
14 changes: 13 additions & 1 deletion packages/memcache-client/src/lib/connection.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Net, { Socket } from "net";
import Tls from "tls";
import assert from "assert";
import { optionalRequire } from "optional-require";

Expand Down Expand Up @@ -103,7 +104,18 @@ export class MemcacheConnection extends MemcacheParser {
assert(host, "Must provide server hostname");
assert(typeof port === "number" && port > 0, "Must provide valid server port");

const socket = Net.createConnection({ host, port });
let socket: Net.Socket | Tls.TLSSocket;
if (this.client?.options?.tls !== undefined) {
// Create a TLS connection
socket = Tls.connect({
host: host,
port: port,
...this.client.options.tls
});
} else {
// Create a regular TCP connection
socket = Net.createConnection({ host, port });
}
this._connectPromise = new Promise((resolve: ResolveCallback, reject: RejectCallback) => {
this._status = Status.CONNECTING;

Expand Down
24 changes: 24 additions & 0 deletions packages/memcache-client/src/test/spec/cacert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID8DCCAtigAwIBAgIJAOCrnjDZoOG/MA0GCSqGSIb3DQEBCwUAMIGLMSgwJgYD
VQQDDB9UZXN0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQswCQYDVQQIDAJD
QTELMAkGA1UEBhMCVVMxHDAaBgkqhkiG9w0BCQEWDXJvb3RAdGVzdC5jb20xDTAL
BgNVBAoMBFRlc3QxGDAWBgNVBAsMD1Rlc3QgRGVwYXJ0bWVudDAgFw0yMDEwMjcw
MzA0NTVaGA8yNTIwMDYyODAzMDQ1NVowgYsxKDAmBgNVBAMMH1Rlc3QgUm9vdCBD
ZXJ0aWZpY2F0ZSBBdXRob3JpdHkxCzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzEc
MBoGCSqGSIb3DQEJARYNcm9vdEB0ZXN0LmNvbTENMAsGA1UECgwEVGVzdDEYMBYG
A1UECwwPVGVzdCBEZXBhcnRtZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAvz238GzKt1UpaxgIxv76hlty5hCZKmD5B952MCWBRi7OyYatl0sMjv38
pgsICIT68wGqQaTmV4W0XQQ54+NZ3o5ujJaDQX2kLs8x0sjuNkmt5Wt+orqdxO99
lb7f4hpKyGq0OGeqz4nBHCZeqEt37OLxUgGWQtKGVZI0X/m/m4LudmWwZpLgAp0w
5mY+FLaPavLoXQ95+f3htUzgRdzsFWMXi0piH/D4ua2i+ekajf8owxmR9AIKpAYY
oV1J+XexlzVzXG1OPUEkGvbxnoDIHUoL6IM4i/QEI52UL0b+sVVV0fmJw08mkN95
XhW+OhS52lSBdqiC3/uRRF/g8/5nowIDAQABo1MwUTAdBgNVHQ4EFgQUcqmERx2+
c9d5Qd04od22AYhwY/AwHwYDVR0jBBgwFoAUcqmERx2+c9d5Qd04od22AYhwY/Aw
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAFYdGLXwXTU4p/CIF
bVRzS+RTEMc6cwd/CC+q0iVZT5Yfhu+MvdlaLMEimYTO44qu32hZHO6ovvSoSNJ1
fofv5crljHyyQY8GoZIq2jm7DcUyWJLuanqIcVXM9o0xR2CdOhvvDo/M0vRpR6EB
ASZ7uV+w/tuc05YLdQOsXJyiXk80H7emJ0bRl1m/H2wxZRnL2jN2Iz1xeY8tGiP0
UU9x0hNS7eNzC1IdRUGVcfstyXznKs37F0ZyNHF1TOTZT80mdJxn0D1alVLqkiN3
RsIZpo1aZ0SShdtIx6luPU2wmGo5kLL2wN6Md18XSHViGA9dx2Mz00GxxDbiQl+F
6qzVDA==
-----END CERTIFICATE-----
76 changes: 76 additions & 0 deletions packages/memcache-client/src/test/spec/client_crt.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
78:f7:9b:75:a7:9d:83:12:a5:c7:a0:b6:af:87:da:c3:d9:32:c2:d7
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = Test Root Certificate Authority, ST = CA, C = US, emailAddress = root@test.com, O = Test, OU = Test Department
Validity
Not Before: Oct 27 03:10:04 2020 GMT
Not After : Jun 28 03:10:04 2520 GMT
Subject: C = US, ST = CA, O = Test Client, OU = Subunit of Test Organization, CN = client.test.com, emailAddress = root@client.test.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:d5:66:c9:c9:ef:64:ad:0f:c7:fe:74:fb:e4:75:
b3:76:60:7f:bb:5e:38:e9:25:e4:70:8c:99:63:ad:
76:26:b8:06:65:f5:0e:b6:6f:22:66:b7:4b:7a:8c:
4f:3a:fb:0e:ac:c1:0e:6e:76:db:b5:95:83:d4:a2:
86:eb:78:63:c5:d3:47:97:d9:6e:0f:39:ee:cd:51:
b6:af:cd:01:2c:a0:c0:bb:58:eb:23:6a:aa:09:e3:
69:31:2e:83:49:88:42:71:7d:25:aa:f1:63:5e:93:
a5:44:19:47:b2:3b:1c:7a:f1:f2:24:75:51:6f:13:
7e:9a:24:2c:42:e3:23:31:b6:dd:fa:50:36:92:ef:
c3:c2:40:fe:45:b9:80:79:d9:64:62:02:f4:de:8c:
58:fa:74:b9:fa:65:08:8f:46:79:a8:c6:69:df:88:
ee:18:d8:4d:90:a6:30:4f:3d:b7:78:56:1e:a7:24:
20:48:3b:e5:ab:e7:8e:41:6e:9f:f1:3d:34:f6:1a:
a3:ef:ad:3f:87:00:b9:88:0f:bf:ce:c4:4e:4a:fc:
1e:90:43:40:b1:df:86:cc:dd:1e:35:4f:95:9c:28:
65:e4:5e:ba:0d:0b:3f:c7:5f:28:b3:4a:be:d2:d7:
4f:38:da:f0:3d:68:3c:40:e7:db:fd:7a:79:89:4c:
43:d1
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:client.test.com, DNS:alt.client.test.com
Signature Algorithm: sha256WithRSAEncryption
47:6a:90:28:93:f3:31:ad:8d:70:59:1b:1d:c2:9a:63:b1:40:
64:39:a1:5d:22:25:c1:45:e0:0b:98:82:47:c9:0d:31:44:d7:
6a:cc:38:8b:ee:67:f4:43:7a:63:7e:b7:84:64:23:de:98:eb:
a3:5f:5a:ca:da:bc:37:16:76:dc:55:5b:eb:71:ef:f5:f9:f9:
db:77:34:1c:8e:b5:fe:9a:00:9d:bf:9f:5a:1a:51:68:54:47:
ef:0c:fb:cd:7f:08:81:01:bd:6e:6a:05:27:1d:66:38:75:7b:
69:66:bf:8d:e7:e1:15:a6:5b:2d:1f:7a:c3:8a:af:14:88:da:
ab:6f:87:8f:df:cd:b6:82:f0:5e:f4:aa:de:36:ab:a0:de:eb:
dd:8b:dc:e7:58:ec:48:8a:27:3f:a2:97:69:aa:63:9e:74:5b:
33:2d:1e:0a:17:02:7f:e3:97:10:75:3e:7d:77:48:a6:72:fb:
5d:d7:68:78:1e:5c:ca:5a:8d:6f:48:f0:ea:92:e3:25:85:9f:
7b:ec:fa:93:8e:ff:ef:bc:70:bd:77:5b:8e:38:cb:df:4e:5e:
35:51:1b:ee:58:1d:cc:cb:bb:e0:85:1f:b6:84:c3:da:c0:1e:
9b:71:88:02:c3:cc:8c:a1:77:0c:bb:e2:ea:05:d5:30:e6:9c:
2d:d6:d2:e5
-----BEGIN CERTIFICATE-----
MIID5jCCAs6gAwIBAgIUePebdaedgxKlx6C2r4faw9kywtcwDQYJKoZIhvcNAQEL
BQAwgYsxKDAmBgNVBAMMH1Rlc3QgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkx
CzAJBgNVBAgMAkNBMQswCQYDVQQGEwJVUzEcMBoGCSqGSIb3DQEJARYNcm9vdEB0
ZXN0LmNvbTENMAsGA1UECgwEVGVzdDEYMBYGA1UECwwPVGVzdCBEZXBhcnRtZW50
MCAXDTIwMTAyNzAzMTAwNFoYDzI1MjAwNjI4MDMxMDA0WjCBljELMAkGA1UEBhMC
VVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQKDAtUZXN0IENsaWVudDElMCMGA1UECwwc
U3VidW5pdCBvZiBUZXN0IE9yZ2FuaXphdGlvbjEYMBYGA1UEAwwPY2xpZW50LnRl
c3QuY29tMSMwIQYJKoZIhvcNAQkBFhRyb290QGNsaWVudC50ZXN0LmNvbTCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANVmycnvZK0Px/50++R1s3Zgf7te
OOkl5HCMmWOtdia4BmX1DrZvIma3S3qMTzr7DqzBDm5227WVg9Sihut4Y8XTR5fZ
bg857s1Rtq/NASygwLtY6yNqqgnjaTEug0mIQnF9JarxY16TpUQZR7I7HHrx8iR1
UW8TfpokLELjIzG23fpQNpLvw8JA/kW5gHnZZGIC9N6MWPp0ufplCI9GeajGad+I
7hjYTZCmME89t3hWHqckIEg75avnjkFun/E9NPYao++tP4cAuYgPv87ETkr8HpBD
QLHfhszdHjVPlZwoZeReug0LP8dfKLNKvtLXTzja8D1oPEDn2/16eYlMQ9ECAwEA
AaMzMDEwLwYDVR0RBCgwJoIPY2xpZW50LnRlc3QuY29tghNhbHQuY2xpZW50LnRl
c3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBHapAok/MxrY1wWRsdwppjsUBkOaFd
IiXBReALmIJHyQ0xRNdqzDiL7mf0Q3pjfreEZCPemOujX1rK2rw3FnbcVVvrce/1
+fnbdzQcjrX+mgCdv59aGlFoVEfvDPvNfwiBAb1uagUnHWY4dXtpZr+N5+EVplst
H3rDiq8UiNqrb4eP3822gvBe9KreNqug3uvdi9znWOxIiic/opdpqmOedFszLR4K
FwJ/45cQdT59d0imcvtd12h4HlzKWo1vSPDqkuMlhZ977PqTjv/vvHC9d1uOOMvf
Tl41URvuWB3My7vghR+2hMPawB6bcYgCw8yMoXcMu+LqBdUw5pwt1tLl
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions packages/memcache-client/src/test/spec/client_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1WbJye9krQ/H/nT75HWzdmB/u1446SXkcIyZY612JrgGZfUO
tm8iZrdLeoxPOvsOrMEObnbbtZWD1KKG63hjxdNHl9luDznuzVG2r80BLKDAu1jr
I2qqCeNpMS6DSYhCcX0lqvFjXpOlRBlHsjscevHyJHVRbxN+miQsQuMjMbbd+lA2
ku/DwkD+RbmAedlkYgL03oxY+nS5+mUIj0Z5qMZp34juGNhNkKYwTz23eFYepyQg
SDvlq+eOQW6f8T009hqj760/hwC5iA+/zsROSvwekENAsd+GzN0eNU+VnChl5F66
DQs/x18os0q+0tdPONrwPWg8QOfb/Xp5iUxD0QIDAQABAoIBAFbHF6NwH8ZoNnF4
7L14R2QJ1adBp27Xo89IvU9VSEhyaNepETSKeLjALIdG6ykW0l7Zmp684pcyl4su
FCNIr3nQcrqDBkhWzI7dfaZ7fNDDBdypSeT7CO5AdZQ0T9rk9+/ibiDXTAs3iuro
RMlrVCeCn27H7jd2+/J+ZylD6BncfpgF6wAz80Ac6PzhlE28tjl8JhDV3AQtoSXp
1oFFT1OIvg6+0FcrxocGsM61akEKdJYmWP8mvQvBWIDFKib9VQuIQ3UEJrwG8H6A
at1H1hFIZpgYTXJnU1n34xxfml16kSpp61Sap531vwLrfi6jrsCgCppdfpX3HEdl
ApGed0kCgYEA9MgV1cB3W/18zALmVf3LRfwVT2oAikeIYr34AcdQ+09TYo20iFIi
+s993M5LOIRkXuL1a3zQYzQplp5hwyXy0+4v6NVHDiEothbOcViCE6pxesHxRlty
ujud2taUIXo3z+fod0TBW19MOzC2EZFzuy6XOle2uTe785NzmmGtKpsCgYEA3y6I
z53b1S4uXtqm494pPxhYvQTZJZND5XeIIW/L/WjWZaLlXtwqv/x94PyX9a3BjLvC
NHuAJDV54KOXZDLn5d+nuqYhwx6DQ6eARTRkqcbsMxqBpbOxHo1CInUWkbCAzeI6
JiHa6+mbfyF1j9waxUcLLVCvI7/chYGCwEZojAMCgYAZOe2WI8mpP0x3Me1O+2LR
iZMgpgn8NjcGBhHjDQl5fMlZJMVwLxbPxkZwVQpHfs1hQf6M30YSkzOeBCCLHgQS
dz+UvTYfbKdnbJ5F7Bsrr6sWPZqB57bBHpe8D9UgRhouYPYCJKHQqteOWgzJbrYA
mmKXbqa0G9xQ0+dcB6jy9QKBgQDcuRoHMxkJ8b1chCSPsdTbGMoSTmwvlECO/bN9
ViLJwVhgdkUgluAUtbMMwuPKzwhflgXQf9/Qb67UsxXzu8DLAHHSz6EOZuvtCgh6
6QExQ5GwTOOxrFBcZdnlWCV0+rm4ZKebtNndfVES31V0bHtxZfyw2V4NHiALTXWx
kTCuXwKBgQDfDWjNsvHjHtY2RDO6OwI/X9sME49l9MmyaFlr7eCTvuGpTvvaZSvD
vJV2J9aBqlJlowpoH0UCy2TWjVSlh3GLFqlKVGOEVIT3FaR2QLHESzNhMCLoYKfd
819yNk6np4WYrN7RpcVoG1XR8sj4NJJQZJy0Iu1N8tnPm9AZEu8jxQ==
-----END RSA PRIVATE KEY-----
114 changes: 114 additions & 0 deletions packages/memcache-client/src/test/spec/client_tls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// eslint-disable-next-line filenames/match-regex
import {
MemcacheClient
} from "../../";
import fs from "fs";
import Path from "path";
import { text1, text2 } from "../data/text";
import NullLoger from "../../lib/null-logger";
import memcached, { MemcachedServer } from "memcached-njs";
import { AddressInfo } from "net";

describe("memcache TLS client", function () {
let memcachedServer: MemcachedServer | undefined;
let server: string;
let serverPort: number;
const defaultTlsOptions = {
key: fs.readFileSync(Path.join(__dirname, "server_key.pem")),
cert: fs.readFileSync(Path.join(__dirname, "server_crt.pem")),
requestCert: false
};

const tlsOptionsClientAuth = {
key: fs.readFileSync(Path.join(__dirname, "server_key.pem")),
cert: fs.readFileSync(Path.join(__dirname, "server_crt.pem")),
ca: fs.readFileSync(Path.join(__dirname, "cacert.pem")),
requestCert: true
};

const startMemcachedServer = (tlsOptions: any, port?: number) => {
const options: Record<string, unknown> = { logger: NullLoger };
options.tls = tlsOptions;
if (port) {
options.port = port;
}
return memcached.startServer(options).then((ms: MemcachedServer) => {
serverPort = (ms._server?.address() as AddressInfo).port;
server = `localhost:${serverPort}`;
memcachedServer = ms;
});
};

afterEach((done) => {
if (memcachedServer !== undefined) {
memcachedServer.shutdown();
memcachedServer = undefined;
}
done();
});

it("TLS handles ECONNREFUSED", (done) => {
startMemcachedServer(defaultTlsOptions).then(() => done()).catch(done);
const x = new MemcacheClient({ server: "localhost:65000", tls: {} });
let testError: Error;
x.cmd("stats")
.catch((err: Error) => (testError = err))
.then(() => {
expect(testError.message).toContain("ECONNREFUSED");
done();
});
});

it("TLS handles ENOTFOUND", (done) => {
const x = new MemcacheClient({ server: "badhost.baddomain.com:65000", tls: {} });
let testError: Error;
x.cmd("stats")
.catch((err: Error) => (testError = err))
.then(() => {
expect(testError.message).toContain("ENOTFOUND");
done();
});
});

it("TLS connection with basic commands", (done) => {
startMemcachedServer(defaultTlsOptions).then(() => {
const c = new MemcacheClient({server: server,
tls: {
ca: fs.readFileSync(Path.join(__dirname, "cacert.pem")),
checkServerIdentity: () => {return undefined;}
}});
c.version()
.then((v: string[]) => {
expect(v[0]).toEqual("VERSION");
expect(v[1]).not.toBe("");
})
.then(() => c.set("key", text1))
.then(() => c.get<string>("key").then((r) => {
expect(r.value).toEqual(text1);
done();
}));
});
});

it("TLS connection with client authentication", (done) => {
startMemcachedServer(tlsOptionsClientAuth).then(() => {
const c = new MemcacheClient({server: server,
tls: {
key: fs.readFileSync(Path.join(__dirname, "client_key.pem")),
cert: fs.readFileSync(Path.join(__dirname, "client_crt.pem")),
ca: fs.readFileSync(Path.join(__dirname, "cacert.pem")),
checkServerIdentity: () => {return undefined;}
}});
c.version()
.then((v: string[]) => {
expect(v[0]).toEqual("VERSION");
expect(v[1]).not.toBe("");
})
.then(() => c.set("key", text2))
.then(() => c.get<string>("key").then((r) => {
expect(r.value).toEqual(text2);
done();
}));
});
});
});
Loading

0 comments on commit 4be7532

Please sign in to comment.