Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support username in URI #1284

Merged
merged 1 commit into from Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 29 additions & 20 deletions README.md
Expand Up @@ -134,6 +134,13 @@ You can also specify connection options as a [`redis://` URL](http://www.iana.or
```javascript
// Connect to 127.0.0.1:6380, db 4, using password "authpassword":
new Redis("redis://:authpassword@127.0.0.1:6380/4");

// Username can also be passed via URI.
// It's worth to noticing that for compatibility reasons `allowUsernameInURI`
// need to be provided, otherwise the username part will be ignored.
new Redis(
"redis://username:authpassword@127.0.0.1:6380/4?allowUsernameInURI=true"
);
```

See [API Documentation](API.md#new_Redis) for all available options.
Expand Down Expand Up @@ -680,7 +687,6 @@ This feature is useful when using Amazon ElastiCache instances with Auto-failove

On ElastiCache insances with Auto-failover enabled, `reconnectOnError` does not execute. Instead of returning a Redis error, AWS closes all connections to the master endpoint until the new primary node is ready. ioredis reconnects via `retryStrategy` instead of `reconnectOnError` after about a minute. On ElastiCache insances with Auto-failover enabled, test failover events with the `Failover primary` option in the AWS console.


## Connection Events

The Redis instance will emit some events about the state of the connection to the Redis server.
Expand Down Expand Up @@ -923,13 +929,17 @@ Sometimes you may want to send a command to multiple nodes (masters or slaves) o
```javascript
// Send `FLUSHDB` command to all slaves:
const slaves = cluster.nodes("slave");
Promise.all(slaves.map(node => node.flushdb()))
Promise.all(slaves.map((node) => node.flushdb()));

// Get keys of all the masters:
const masters = cluster.nodes("master");
Promise.all(masters.map(node => node.keys()).then(keys => {
// keys: [['key1', 'key2'], ['key3', 'key4']]
}));
Promise.all(
masters
.map((node) => node.keys())
.then((keys) => {
// keys: [['key1', 'key2'], ['key3', 'key4']]
})
);
```

### NAT Mapping
Expand Down Expand Up @@ -1064,7 +1074,7 @@ const cluster = new Redis.Cluster(

## Autopipelining

In standard mode, when you issue multiple commands, ioredis sends them to the server one by one. As described in Redis pipeline documentation, this is a suboptimal use of the network link, especially when such link is not very performant.
In standard mode, when you issue multiple commands, ioredis sends them to the server one by one. As described in Redis pipeline documentation, this is a suboptimal use of the network link, especially when such link is not very performant.

The TCP and network overhead negatively affects performance. Commands are stuck in the send queue until the previous ones are correctly delivered to the server. This is a problem known as Head-Of-Line blocking (HOL).

Expand All @@ -1076,38 +1086,39 @@ This feature can dramatically improve throughput and avoids HOL blocking. In our

While an automatic pipeline is executing, all new commands will be enqueued in a new pipeline which will be executed as soon as the previous finishes.

When using Redis Cluster, one pipeline per node is created. Commands are assigned to pipelines according to which node serves the slot.
When using Redis Cluster, one pipeline per node is created. Commands are assigned to pipelines according to which node serves the slot.

A pipeline will thus contain commands using different slots but that ultimately are assigned to the same node.
A pipeline will thus contain commands using different slots but that ultimately are assigned to the same node.

Note that the same slot limitation within a single command still holds, as it is a Redis limitation.


### Example of automatic pipeline enqueuing

This sample code uses ioredis with automatic pipeline enabled.

```javascript
const Redis = require('./built');
const http = require('http');
const Redis = require("./built");
const http = require("http");

const db = new Redis({ enableAutoPipelining: true });

const server = http.createServer((request, response) => {
const key = new URL(request.url, 'https://localhost:3000/').searchParams.get('key');
const key = new URL(request.url, "https://localhost:3000/").searchParams.get(
"key"
);

db.get(key, (err, value) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.writeHead(200, { "Content-Type": "text/plain" });
response.end(value);
});
})
});

server.listen(3000);
```

When Node receives requests, it schedules them to be processed in one or more iterations of the events loop.

All commands issued by requests processing during one iteration of the loop will be wrapped in a pipeline automatically created by ioredis.
All commands issued by requests processing during one iteration of the loop will be wrapped in a pipeline automatically created by ioredis.

In the example above, the pipeline will have the following contents:

Expand All @@ -1129,24 +1140,22 @@ This approach increases the utilization of the network link, reduces the TCP ove

### Benchmarks

Here's some of the results of our tests for a single node.
Here's some of the results of our tests for a single node.

Each iteration of the test runs 1000 random commands on the server.

| | Samples | Result | Tolerance |
|---------------------------|---------|---------------|-----------|
| ------------------------- | ------- | ------------- | --------- |
| default | 1000 | 174.62 op/sec | ± 0.45 % |
| enableAutoPipelining=true | 1500 | 233.33 op/sec | ± 0.88 % |


And here's the same test for a cluster of 3 masters and 3 replicas:

| | Samples | Result | Tolerance |
|---------------------------|---------|---------------|-----------|
| ------------------------- | ------- | ------------- | --------- |
| default | 1000 | 164.05 op/sec | ± 0.42 % |
| enableAutoPipelining=true | 3000 | 235.31 op/sec | ± 0.94 % |


# Error Handling

All the errors returned by the Redis server are instances of `ReplyError`, which can be accessed via `Redis`:
Expand Down
15 changes: 12 additions & 3 deletions lib/utils/index.ts
Expand Up @@ -255,10 +255,19 @@ export function parseURL(url) {
parsed = urllibParse(url, true, true);
}

const options = parsed.query || {};
const allowUsernameInURI =
options.allowUsernameInURI && options.allowUsernameInURI !== "false";
delete options.allowUsernameInURI;

const result: any = {};
if (parsed.auth) {
const index = parsed.auth.indexOf(":")
result.password = index === -1 ? '' : parsed.auth.slice(index + 1)
const index = parsed.auth.indexOf(":");
if (allowUsernameInURI) {
result.username =
index === -1 ? parsed.auth : parsed.auth.slice(0, index);
}
result.password = index === -1 ? "" : parsed.auth.slice(index + 1);
}
if (parsed.pathname) {
if (parsed.protocol === "redis:" || parsed.protocol === "rediss:") {
Expand All @@ -275,7 +284,7 @@ export function parseURL(url) {
if (parsed.port) {
result.port = parsed.port;
}
defaults(result, parsed.query);
defaults(result, options);

return result;
}
Expand Down
19 changes: 19 additions & 0 deletions test/functional/auth.ts
Expand Up @@ -170,6 +170,25 @@ describe("auth", function () {
redis = new Redis({ port: 17379, username, password });
});

it("should handle auth with Redis URL string with username and password (Redis >=6) (redis://foo:bar@baz.com/) correctly", function (done) {
let username = "user";
let password = "pass";
let redis;
new MockServer(17379, function (argv) {
if (
argv[0] === "auth" &&
argv[1] === username &&
argv[2] === password
) {
redis.disconnect();
done();
}
});
redis = new Redis(
`redis://user:pass@localhost:17379/?allowUsernameInURI=true`
);
});

it('should not emit "error" when the Redis >=6 server doesn\'t need auth', function (done) {
new MockServer(17379, function (argv) {
if (argv[0] === "auth" && argv[1] === "pass") {
Expand Down
57 changes: 54 additions & 3 deletions test/unit/utils.ts
Expand Up @@ -204,9 +204,7 @@ describe("utils", function () {
password: "pass:word",
key: "value",
});
expect(
utils.parseURL("redis://user@127.0.0.1:6380/4?key=value")
).to.eql({
expect(utils.parseURL("redis://user@127.0.0.1:6380/4?key=value")).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
Expand All @@ -226,6 +224,59 @@ describe("utils", function () {
key: "value",
});
});

it("supports allowUsernameInURI", function () {
expect(
utils.parseURL(
"redis://user:pass@127.0.0.1:6380/4?allowUsernameInURI=true"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
username: "user",
password: "pass",
});
expect(
utils.parseURL(
"redis://user:pass@127.0.0.1:6380/4?allowUsernameInURI=false"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
password: "pass",
});
expect(
utils.parseURL(
"redis://user:pass:word@127.0.0.1:6380/4?key=value&allowUsernameInURI=true"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
username: "user",
password: "pass:word",
key: "value",
});
expect(
utils.parseURL(
"redis://user@127.0.0.1:6380/4?key=value&allowUsernameInURI=true"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
username: "user",
password: "",
key: "value",
});
expect(
utils.parseURL("redis://127.0.0.1/?allowUsernameInURI=true")
).to.eql({
host: "127.0.0.1",
});
});
});

describe(".sample", function () {
Expand Down