Skip to content

Commit b81712a

Browse files
committed
Merge branch 'breaking-changes'
2 parents f9482a2 + e92ea5e commit b81712a

File tree

5 files changed

+161
-30
lines changed

5 files changed

+161
-30
lines changed

README.md

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ The callback function is called with the following three arguments:
307307
* `handshake` {Object} - A handshake object
308308
* `protocols` {Set} - A set of subprotocols purportedly supported by the client.
309309
* `identity` {String} - The identity portion of the connection URL, decoded.
310-
* `password` {String} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Read [Security Profile 1](#security-profile-1) for more details of how this works.
310+
* `password` {Buffer} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Typically this password would be a string, but the OCPP specs allow for this to be binary, so it is provided as a `Buffer` for you to interpret as you wish. Read [Security Profile 1](#security-profile-1) for more details of how this works.
311311
* `endpoint` {String} - The endpoint path portion of the connection URL. This is the part of the path before the identity.
312312
* `query` {URLSearchParams} - The query string parsed as [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).
313313
* `remoteAddress` {String} - The remote IP address of the socket.
@@ -384,7 +384,7 @@ Returns a `Promise` which resolves when the server has completed closing.
384384
- `endpoint` {String} - The RPC server's endpoint (a websocket URL). **Required**.
385385
- `identity` {String} - The RPC client's identity. Will be automatically encoded. **Required**.
386386
- `protocols` {Array<String>} - Array of subprotocols supported by this client. Defaults to `[]`.
387-
- `password` {String} - Optional password to use in [HTTP Basic auth](#security-profile-1). (The username will always be the identity).
387+
- `password` {String|Buffer} - Optional password to use in [HTTP Basic auth](#security-profile-1). This can be a Buffer to allow for binary auth keys as recommended in the OCPP security whitepaper. If provided as a string, it will be encoded as UTF-8. (The corresponding username will always be the identity).
388388
- `headers` {Object} - Additional HTTP headers to send along with the websocket upgrade request. Defaults to `{}`.
389389
- `query` {Object|String} - An optional query string or object to append as the query string of the connection URL. Defaults to `''`.
390390
- `callTimeoutMs` {Number} - Milliseconds to wait before unanswered outbound calls are rejected automatically. Defaults to `60000`.
@@ -415,7 +415,14 @@ If too many bad messages are received in succession, the client will be closed w
415415

416416
#### Event: 'strictValidationFailure'
417417

418-
* `error` [{RPCError}](#rpcerror)
418+
* `event` {Object}
419+
* `error` {Error} - The validation error that triggered the `strictValidationFailure` event.
420+
* `messageId` {String} - The RPC message ID
421+
* `method` {String} - The RPC method being invoked.
422+
* `params` {Object} - The RPC parameters.
423+
* `result` {Object} - If this error relates to a **CALLRESULT** validation failure, then this contains the invalid result, otherwise `null`.
424+
* `outbound` {Boolean} - This will be `true` if the invalid message originated locally.
425+
* `isCall` {Boolean} - This will be `true` if the invalid message is a **CALL** type. `false` indicates a **CALLRESULT** type.
419426

420427
This event is emitted in [strict mode](#strict-validation) when an inbound call or outbound response does not satisfy the subprotocol schema validator. See [Effects of `strictMode`](#effects-of-strictmode) to understand what happens in response to the invalid message.
421428

@@ -668,7 +675,7 @@ The RPCServerClient is a subclass of RPCClient. This represents an RPCClient fro
668675
* {Object}
669676
* `protocols` {Set} - A set of subprotocols purportedly supported by the client.
670677
* `identity` {String} - The identity portion of the connection URL, decoded.
671-
* `password` {String} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Read [Security Profile 1](#security-profile-1) for more details of how this works.
678+
* `password` {Buffer} - If HTTP Basic auth was used in the connection, and the username correctly matches the identity, this field will contain the password (otherwise `undefined`). Typically this password would be a string, but the OCPP specs allow for this to be binary, so it is provided as a `Buffer` for you to interpret as you wish. Read [Security Profile 1](#security-profile-1) for more details of how this works.
672679
* `endpoint` {String} - The endpoint path portion of the connection URL. This is the part of the path before the identity.
673680
* `query` {URLSearchParams} - The query string parsed as [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams).
674681
* `remoteAddress` {String} - The remote IP address of the socket.
@@ -717,7 +724,7 @@ An object containing additional error details.
717724

718725
This is a utility function to create a special type of RPC Error to be thrown from a call handler to return a non-generic error response.
719726

720-
Returns an [`RPCError`](#rpcerror) which corresponds to the specified type:
727+
Returns an [`RPCError`](#class-rpcerror--error) which corresponds to the specified type:
721728

722729
| Type | Description |
723730
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
@@ -763,12 +770,14 @@ const server = new RPCServer({
763770
### Effects of `strictMode`
764771

765772
As a caller, `strictMode` has the following effects:
766-
* If your method or params fail validation, your call will reject immediately with an [`RPCError`](#rpcerror). The call will not be sent.
767-
* If a response to your call fails validation, the call will reject with an [`RPCError`](#rpcerror).
773+
* If your method or params fail validation, your call will reject immediately with an [`RPCError`](#class-rpcerror--error). The call will not be sent.
774+
* If a response to your call fails validation, the call will reject with an [`RPCError`](#class-rpcerror--error) and you will not receive the actual response that was sent.
768775

769776
As a callee, `strictMode` has the following effects:
770-
* If an inbound call's params fail validation, the call will not be passed to a handler. Instead, an error response will be automatically issued to the caller with an appropriate RPC error. A [`'strictValidationFailure'`](#event-strictvalidationfailure) event will be emitted with an [`RPCError`](#rpcerror).
771-
* If your response to a call fails validation, the response will be discarded and an `"InternalError"` RPC error will be sent instead. A [`'strictValidationFailure'`](#event-strictvalidationfailure) event will be emitted with an [`RPCError`](#rpcerror).
777+
* If an inbound call's params fail validation, the call will not be passed to a handler. Instead, an error response will be automatically issued to the caller with an appropriate RPC error.
778+
* If your response to a call fails validation, then your response will be discarded and an `"InternalError"` RPC error will be sent instead.
779+
780+
In all cases, a [`'strictValidationFailure'`](#event-strictvalidationfailure) event will be emitted, detailing the circumstances of the failure.
772781

773782
**Important:** If you are using `strictMode`, you are strongly encouraged to listen for [`'strictValidationFailure'`](#event-strictvalidationfailure) events, otherwise you may not know if your responses or inbound calls are being dropped for failing validation.
774783

@@ -846,7 +855,7 @@ const cli = new RPCClient({
846855

847856
const server = new RPCServer();
848857
server.auth((accept, reject, handshake) => {
849-
if (handshake.identity === "AzureDiamond" && handshake.password === "hunter2") {
858+
if (handshake.identity === "AzureDiamond" && handshake.password.toString('utf8') === "hunter2") {
850859
accept();
851860
} else {
852861
reject(401);
@@ -867,6 +876,8 @@ In practice, it's not uncommon to see violations of RFC7617 in the wild. All maj
867876

868877
However, in OCPP, since we have the luxury of knowing that the username must always be equal to the client's identity, it is no longer necessary to rely upon a colon to delineate the username from the password. This module makes use of this guarantee to enable identities and passwords to contain as many or as few colons as you wish.
869878

879+
Additionally, the OCPP security whitepaper recommends passwords consist purely of random bytes (for maximum entropy), although this violates the Basic Auth RFC which requires all passwords to be TEXT (US-ASCII compatible with no control characters). For this reason, this library will not make any presumptions about the character encoding (or otherwise) of the password provided, and present the password as a `Buffer`.
880+
870881
```js
871882
const { RPCClient, RPCServer } = require('ocpp-rpc');
872883

@@ -877,8 +888,8 @@ const cli = new RPCClient({
877888

878889
const server = new RPCServer();
879890
server.auth((accept, reject, handshake) => {
880-
console.log(handshake.identity); // "this:is:ok"
881-
console.log(handshake.password); // "as:is:this"
891+
console.log(handshake.identity); // "this:is:ok"
892+
console.log(handshake.password.toString('utf8')); // "as:is:this"
882893
accept();
883894
});
884895

@@ -901,8 +912,8 @@ const server = new RPCServer();
901912
server.auth((accept, reject, handshake) => {
902913
const cred = auth.parse(handshake.headers.authorization);
903914

904-
console.log(cred.name); // "this"
905-
console.log(cred.pass); // "is:broken:as:is:this"
915+
console.log(cred.name); // "this"
916+
console.log(cred.pass.toString('utf8')); // "is:broken:as:is:this"
906917
accept();
907918
});
908919

@@ -1074,6 +1085,14 @@ server.auth((accept, reject, handshake) => {
10741085
* RPC calls while in this state are rejected.
10751086
* RPC responses will be silently dropped.
10761087

1088+
## Upgrading from 1.X -> 2.0
1089+
1090+
Breaking changes:
1091+
* The `RPCClient` event [`'strictValidationFailure'`](#event-strictvalidationfailure) now fires for both inbound & outbound requests & responses.
1092+
* The `RPCClient` event [`'strictValidationFailure'`](#event-strictvalidationfailure) emits an object containing more information than was previously available. The Error which was previously emitted is now a member of this object.
1093+
* The `password` option in the `RPCClient` [constructor](#new-rpcclientoptions) can now be supplied as a `Buffer`. If a string is provided, it will be encoded as utf8.
1094+
* The `password` field of `RPCServerClient`'s [`handshake`](#clienthandshake) object is now always provided as a Buffer instead of a string. Use `password.toString('utf8')` to convert back to a string as per previous versions.
1095+
10771096
## License
10781097

10791098
[MIT](LICENSE.md)

lib/client.js

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ class RPCClient extends EventEmitter {
309309
try {
310310
validator.validate(`urn:${method}.req`, params);
311311
} catch (error) {
312+
this.emit('strictValidationFailure', {
313+
messageId: msgId,
314+
method,
315+
params,
316+
result: null,
317+
error,
318+
outbound: true,
319+
isCall: true,
320+
});
312321
throw error;
313322
}
314323
}
@@ -483,8 +492,13 @@ class RPCClient extends EventEmitter {
483492
Object.assign(wsOpts.headers, this._options.headers);
484493

485494
if (this._options.password != null) {
486-
const userPass = [this._identity, this._options.password].join(':');
487-
const b64 = Buffer.from(userPass).toString('base64');
495+
const usernameBuffer = Buffer.from(this._identity + ':');
496+
let passwordBuffer = this._options.password;
497+
if (typeof passwordBuffer === 'string') {
498+
passwordBuffer = Buffer.from(passwordBuffer, 'utf8');
499+
}
500+
501+
const b64 = Buffer.concat([usernameBuffer, passwordBuffer]).toString('base64');
488502
wsOpts.headers.authorization = 'Basic ' + b64;
489503
}
490504

@@ -807,7 +821,15 @@ class RPCClient extends EventEmitter {
807821
try {
808822
validator.validate(`urn:${method}.req`, params);
809823
} catch (error) {
810-
this.emit('strictValidationFailure', error);
824+
this.emit('strictValidationFailure', {
825+
messageId: msgId,
826+
method,
827+
params,
828+
result: null,
829+
error,
830+
outbound: false,
831+
isCall: true,
832+
});
811833
throw error;
812834
}
813835
}
@@ -859,7 +881,15 @@ class RPCClient extends EventEmitter {
859881
try {
860882
validator.validate(`urn:${method}.conf`, result);
861883
} catch (error) {
862-
this.emit('strictValidationFailure', error);
884+
this.emit('strictValidationFailure', {
885+
messageId: msgId,
886+
method,
887+
params,
888+
result,
889+
error,
890+
outbound: true,
891+
isCall: false,
892+
});
863893
throw createRPCError("InternalError");
864894
}
865895
}
@@ -923,6 +953,15 @@ class RPCClient extends EventEmitter {
923953
try {
924954
validator.validate(`urn:${pendingCall.method}.conf`, result);
925955
} catch (error) {
956+
this.emit('strictValidationFailure', {
957+
messageId: msgId,
958+
method: pendingCall.method,
959+
params: pendingCall.params,
960+
result,
961+
error,
962+
outbound: false,
963+
isCall: false,
964+
});
926965
return pendingCall.reject(error);
927966
}
928967
}

lib/server.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,18 @@ class RPCServer extends EventEmitter {
144144
* However, this shouldn't cause any confusion as we have a
145145
* guarantee from OCPP that the username will always be equal to
146146
* the identity.
147+
* It also supports binary passwords, which is also a spec violation
148+
* but is necessary for allowing truly random binary keys as
149+
* recommended by the OCPP security whitepaper.
147150
*/
148151
const b64up = headers.authorization.match(/^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/)[1];
149-
const userPass = Buffer.from(b64up, 'base64').toString('utf8');
150-
if (userPass.substring(0, identity.length + 1) === (identity + ':')) {
151-
// identity matches!
152-
password = userPass.substring(identity.length + 1);
152+
const userPassBuffer = Buffer.from(b64up, 'base64');
153+
154+
const clientIdentityUserBuffer = Buffer.from(identity + ':');
155+
156+
if (clientIdentityUserBuffer.compare(userPassBuffer, 0, clientIdentityUserBuffer.length) === 0) {
157+
// first part of buffer matches `${identity}:`
158+
password = userPassBuffer.subarray(clientIdentityUserBuffer.length);
153159
}
154160
} catch (err) {
155161
// failing to parse authorization header is no big deal.

test/client.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ describe('RPCClient', function(){
835835
}
836836
});
837837

838-
it('should authenticate with password', async () => {
838+
it('should authenticate with string passwords', async () => {
839839

840840
const password = 'hunter2';
841841
let recPass;
@@ -854,7 +854,39 @@ describe('RPCClient', function(){
854854

855855
try {
856856
await cli.connect();
857-
assert.equal(password, recPass);
857+
assert.equal(password, recPass.toString('utf8'));
858+
859+
} finally {
860+
cli.close();
861+
close();
862+
}
863+
});
864+
865+
it('should authenticate with binary passwords', async () => {
866+
867+
const password = Buffer.from([
868+
0,1,2,3,4,5,6,7,8,9,
869+
65,66,67,68,69,
870+
251,252,253,254,255,
871+
]);
872+
let recPass;
873+
874+
const {endpoint, close, server} = await createServer();
875+
server.auth((accept, reject, handshake) => {
876+
recPass = handshake.password;
877+
accept();
878+
});
879+
880+
const cli = new RPCClient({
881+
endpoint,
882+
identity: 'X',
883+
password,
884+
});
885+
886+
try {
887+
await cli.connect();
888+
// console.log(Buffer.from(recPass, 'ascii'));
889+
assert.equal(password.toString('hex'), recPass.toString('hex'));
858890

859891
} finally {
860892
cli.close();
@@ -1329,9 +1361,9 @@ describe('RPCClient', function(){
13291361
const [c2] = await once(cli, 'strictValidationFailure');
13301362
const [c3] = await once(cli, 'strictValidationFailure');
13311363

1332-
assert.equal(c1.rpcErrorCode, 'OccurenceConstraintViolation');
1333-
assert.equal(c2.rpcErrorCode, 'TypeConstraintViolation');
1334-
assert.equal(c3.rpcErrorCode, 'ProtocolError');
1364+
assert.equal(c1.error.rpcErrorCode, 'OccurenceConstraintViolation');
1365+
assert.equal(c2.error.rpcErrorCode, 'TypeConstraintViolation');
1366+
assert.equal(c3.error.rpcErrorCode, 'ProtocolError');
13351367

13361368
assert.equal(calls, 3);
13371369
assert.equal(responses, 3);

test/server.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ describe('RPCServer', function(){
427427
}});
428428

429429
server.auth((accept, reject, handshake) => {
430-
accept({pwd: handshake.password});
430+
accept({pwd: handshake.password.toString('utf8')});
431431
});
432432

433433
const cli = new RPCClient({
@@ -470,7 +470,7 @@ describe('RPCServer', function(){
470470

471471
try {
472472
await cli.connect();
473-
assert.equal(password, recPass);
473+
assert.equal(password, recPass.toString('utf8'));
474474
assert.equal(identity, recIdent);
475475

476476
} finally {
@@ -498,7 +498,7 @@ describe('RPCServer', function(){
498498

499499
try {
500500
await cli.connect();
501-
assert.equal(password, recPass);
501+
assert.equal(password, recPass.toString('utf8'));
502502

503503
} finally {
504504
cli.close();
@@ -586,6 +586,41 @@ describe('RPCServer', function(){
586586
close();
587587
}
588588
});
589+
590+
it('should recognise binary passwords', async () => {
591+
592+
const password = Buffer.from([
593+
0,1,2,3,4,5,6,7,8,9,
594+
65,66,67,68,69,
595+
251,252,253,254,255,
596+
]);
597+
598+
const {endpoint, close, server} = await createServer({}, {withClient: cli => {
599+
cli.handle('GetPassword', () => {
600+
return cli.session.pwd;
601+
});
602+
}});
603+
604+
server.auth((accept, reject, handshake) => {
605+
accept({pwd: handshake.password.toString('hex')});
606+
});
607+
608+
const cli = new RPCClient({
609+
endpoint,
610+
identity: 'X',
611+
password,
612+
});
613+
614+
try {
615+
await cli.connect();
616+
const pass = await cli.call('GetPassword');
617+
assert.equal(password.toString('hex'), pass);
618+
619+
} finally {
620+
cli.close();
621+
close();
622+
}
623+
});
589624

590625
});
591626

0 commit comments

Comments
 (0)