nats
Version:
Node.js client for NATS, a lightweight, high-performance cloud native messaging system
276 lines • 8.83 kB
JavaScript
"use strict";
/*
* Copyright 2020-2023 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MsgHdrsImpl = void 0;
exports.canonicalMIMEHeaderKey = canonicalMIMEHeaderKey;
exports.headers = headers;
// Heavily inspired by Golang's https://golang.org/src/net/http/header.go
const encoders_1 = require("./encoders");
const core_1 = require("./core");
// https://www.ietf.org/rfc/rfc822.txt
// 3.1.2. STRUCTURE OF HEADER FIELDS
//
// Once a field has been unfolded, it may be viewed as being com-
// posed of a field-name followed by a colon (":"), followed by a
// field-body, and terminated by a carriage-return/line-feed.
// The field-name must be composed of printable ASCII characters
// (i.e., characters that have values between 33. and 126.,
// decimal, except colon). The field-body may be composed of any
// ASCII characters, except CR or LF. (While CR and/or LF may be
// present in the actual text, they are removed by the action of
// unfolding the field.)
function canonicalMIMEHeaderKey(k) {
const a = 97;
const A = 65;
const Z = 90;
const z = 122;
const dash = 45;
const colon = 58;
const start = 33;
const end = 126;
const toLower = a - A;
let upper = true;
const buf = new Array(k.length);
for (let i = 0; i < k.length; i++) {
let c = k.charCodeAt(i);
if (c === colon || c < start || c > end) {
throw new core_1.NatsError(`'${k[i]}' is not a valid character for a header key`, core_1.ErrorCode.BadHeader);
}
if (upper && a <= c && c <= z) {
c -= toLower;
}
else if (!upper && A <= c && c <= Z) {
c += toLower;
}
buf[i] = c;
upper = c == dash;
}
return String.fromCharCode(...buf);
}
function headers(code = 0, description = "") {
if ((code === 0 && description !== "") || (code > 0 && description === "")) {
throw new Error("setting status requires both code and description");
}
return new MsgHdrsImpl(code, description);
}
const HEADER = "NATS/1.0";
class MsgHdrsImpl {
constructor(code = 0, description = "") {
this._code = code;
this._description = description;
this.headers = new Map();
}
[Symbol.iterator]() {
return this.headers.entries();
}
size() {
return this.headers.size;
}
equals(mh) {
if (mh && this.headers.size === mh.headers.size &&
this._code === mh._code) {
for (const [k, v] of this.headers) {
const a = mh.values(k);
if (v.length !== a.length) {
return false;
}
const vv = [...v].sort();
const aa = [...a].sort();
for (let i = 0; i < vv.length; i++) {
if (vv[i] !== aa[i]) {
return false;
}
}
}
return true;
}
return false;
}
static decode(a) {
const mh = new MsgHdrsImpl();
const s = encoders_1.TD.decode(a);
const lines = s.split("\r\n");
const h = lines[0];
if (h !== HEADER) {
// malformed headers could add extra space without adding a code or description
let str = h.replace(HEADER, "").trim();
if (str.length > 0) {
mh._code = parseInt(str, 10);
if (isNaN(mh._code)) {
mh._code = 0;
}
const scode = mh._code.toString();
str = str.replace(scode, "");
mh._description = str.trim();
}
}
if (lines.length >= 1) {
lines.slice(1).map((s) => {
if (s) {
const idx = s.indexOf(":");
if (idx > -1) {
const k = s.slice(0, idx);
const v = s.slice(idx + 1).trim();
mh.append(k, v);
}
}
});
}
return mh;
}
toString() {
if (this.headers.size === 0 && this._code === 0) {
return "";
}
let s = HEADER;
if (this._code > 0 && this._description !== "") {
s += ` ${this._code} ${this._description}`;
}
for (const [k, v] of this.headers) {
for (let i = 0; i < v.length; i++) {
s = `${s}\r\n${k}: ${v[i]}`;
}
}
return `${s}\r\n\r\n`;
}
encode() {
return encoders_1.TE.encode(this.toString());
}
static validHeaderValue(k) {
const inv = /[\r\n]/;
if (inv.test(k)) {
throw new core_1.NatsError("invalid header value - \\r and \\n are not allowed.", core_1.ErrorCode.BadHeader);
}
return k.trim();
}
keys() {
const keys = [];
for (const sk of this.headers.keys()) {
keys.push(sk);
}
return keys;
}
findKeys(k, match = core_1.Match.Exact) {
const keys = this.keys();
switch (match) {
case core_1.Match.Exact:
return keys.filter((v) => {
return v === k;
});
case core_1.Match.CanonicalMIME:
k = canonicalMIMEHeaderKey(k);
return keys.filter((v) => {
return v === k;
});
default: {
const lci = k.toLowerCase();
return keys.filter((v) => {
return lci === v.toLowerCase();
});
}
}
}
get(k, match = core_1.Match.Exact) {
const keys = this.findKeys(k, match);
if (keys.length) {
const v = this.headers.get(keys[0]);
if (v) {
return Array.isArray(v) ? v[0] : v;
}
}
return "";
}
last(k, match = core_1.Match.Exact) {
const keys = this.findKeys(k, match);
if (keys.length) {
const v = this.headers.get(keys[0]);
if (v) {
return Array.isArray(v) ? v[v.length - 1] : v;
}
}
return "";
}
has(k, match = core_1.Match.Exact) {
return this.findKeys(k, match).length > 0;
}
set(k, v, match = core_1.Match.Exact) {
this.delete(k, match);
this.append(k, v, match);
}
append(k, v, match = core_1.Match.Exact) {
// validate the key
const ck = canonicalMIMEHeaderKey(k);
if (match === core_1.Match.CanonicalMIME) {
k = ck;
}
// if we get non-sensical ignores/etc, we should try
// to do the right thing and use the first key that matches
const keys = this.findKeys(k, match);
k = keys.length > 0 ? keys[0] : k;
const value = MsgHdrsImpl.validHeaderValue(v);
let a = this.headers.get(k);
if (!a) {
a = [];
this.headers.set(k, a);
}
a.push(value);
}
values(k, match = core_1.Match.Exact) {
const buf = [];
const keys = this.findKeys(k, match);
keys.forEach((v) => {
const values = this.headers.get(v);
if (values) {
buf.push(...values);
}
});
return buf;
}
delete(k, match = core_1.Match.Exact) {
const keys = this.findKeys(k, match);
keys.forEach((v) => {
this.headers.delete(v);
});
}
get hasError() {
return this._code >= 300;
}
get status() {
return `${this._code} ${this._description}`.trim();
}
toRecord() {
const data = {};
this.keys().forEach((v) => {
data[v] = this.values(v);
});
return data;
}
get code() {
return this._code;
}
get description() {
return this._description;
}
static fromRecord(r) {
const h = new MsgHdrsImpl();
for (const k in r) {
h.headers.set(k, r[k]);
}
return h;
}
}
exports.MsgHdrsImpl = MsgHdrsImpl;
//# sourceMappingURL=headers.js.map