all files / modules/ Message.js

89.89% Statements 80/89
69.57% Branches 32/46
91.67% Functions 22/24
89.89% Lines 80/89
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271                               373× 373×                                         553×   388×   388×                   388× 53× 74× 74×                                 94×             95×   95× 95×         93×               16× 16×   16×                       11×       16×             143×   20×                     22× 22×   17× 17×                     108× 108× 67×   41× 41×               156×   445× 309×     445× 53× 53×   392×     445×                                 153× 153×     153×                   96×   96× 93×                               15×       15× 12×     15× 15×   15×          
const bodec = require("bodec");
const d = require("describe-property");
const Stream = require("bufferedstream");
const bufferStream = require("./utils/bufferStream");
const normalizeHeaderName = require("./utils/normalizeHeaderName");
const parseCookie = require("./utils/parseCookie");
const parseQuery = require("./utils/parseQuery");
const R = require("ramda");
 
/**
 * The default content to use for new messages.
 */
const DEFAULT_CONTENT = bodec.fromString("");
 
/**
 * The default maximum length (in bytes) to use in Message#parseContent.
 */
const DEFAULT_MAX_CONTENT_LENGTH = Math.pow(2, 20); // 1M
 
const HEADERS_LINE_SEPARATOR = /\r?\n/;
const HEADER_SEPARATOR = ": ";
 
function defaultParser(message, maxLength) {
    return message.stringifyContent(maxLength);
}
 
/**
 * An HTTP message.
 */
function Message(content, headers) {
    this.headers = headers;
    this.content = content;
}
 
Object.defineProperties(Message, {
 
    PARSERS: d({
        enumerable: true,
        value: {
            "application/json"(message, maxLength) {
                return message.stringifyContent(maxLength).then(JSON.parse);
            },
            "application/x-www-form-urlencoded"(message, maxLength) {
                return message.stringifyContent(maxLength).then(parseQuery);
            }
        }
    })
 
});
 
Object.defineProperties(Message.prototype, {
 
  /**
   * The headers of this message as { headerName, value }.
   */
    headers: d.gs(function () {
        return this._headers;
    }, function (value) {
        this._headers = {};
 
        Iif (typeof value === "string") {
            value.split(HEADERS_LINE_SEPARATOR).forEach(function (line) {
                const index = line.indexOf(HEADER_SEPARATOR);
 
                if (index === -1) {
                    this.addHeader(line, true);
                } else {
                    this.addHeader(line.substring(0, index), line.substring(index + HEADER_SEPARATOR.length));
                }
            }, this);
        } else if (!R.isNil(value)) {
            for (const headerName in value) {
                Eif (value.hasOwnProperty(headerName)) {
                    this.addHeader(headerName, value[headerName]);
                }
            }
        }
    }),
 
  /**
   * Returns the value of the header with the given name.
   */
    getHeader: d(function (headerName) {
        return this.headers[normalizeHeaderName(headerName)];
    }),
 
  /**
   * Sets the value of the header with the given name.
   */
    setHeader: d(function (headerName, value) {
        this.headers[normalizeHeaderName(headerName)] = value;
    }),
 
  /**
   * Adds the value to the header with the given name.
   */
    addHeader: d(function (headerName, value) {
        headerName = normalizeHeaderName(headerName);
 
        const headers = this.headers;
        if (headerName in headers) {
            Iif (Array.isArray(headers[headerName])) {
                headers[headerName].push(value);
            } else {
                headers[headerName] = [headers[headerName], value];
            }
        } else {
            headers[headerName] = value;
        }
    }),
 
  /**
   * An object containing cookies in this message, keyed by name.
   */
    cookies: d.gs(function () {
        Eif (!this._cookies) {
            const header = this.headers.Cookie;
 
            if (header) {
                const cookies = parseCookie(header);
 
        // From RFC 2109:
        // If multiple cookies satisfy the criteria above, they are ordered in
        // the Cookie header such that those with more specific Path attributes
        // precede those with less specific. Ordering with respect to other
        // attributes (e.g., Domain) is unspecified.
                for (const cookieName in cookies) {
                    Iif (Array.isArray(cookies[cookieName])) {
                        cookies[cookieName] = cookies[cookieName][0] || "";
                    }
                }
 
                this._cookies = cookies;
            } else {
                this._cookies = {};
            }
        }
 
        return this._cookies;
    }),
 
  /**
   * Gets/sets the value of the Content-Type header.
   */
    contentType: d.gs(function () {
        return this.headers["Content-Type"];
    }, function (value) {
        this.headers["Content-Type"] = value;
    }),
 
  /**
   * The media type (type/subtype) portion of the Content-Type header without any
   * media type parameters. e.g. when Content-Type is "text/plain;charset=utf-8",
   * the mediaType is "text/plain".
   *
   * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
   */
    mediaType: d.gs(function () {
        const contentType = this.contentType;
        if (!contentType) {
            return null;
        }
        const match = contentType.match(/^([^;,]+)/);
        return match ? match[1].toLowerCase() : null;
    }, function (value) {
        this.contentType = value + (this.charset ? `;charset=${this.charset}` : "");
    }),
 
  /**
   * Returns the character set used to encode the content of this message. e.g.
   * when Content-Type is "text/plain;charset=utf-8", charset is "utf-8".
   *
   * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.4
   */
    charset: d.gs(function () {
        const contentType = this.contentType;
        if (!contentType) {
            return null;
        }
        const match = contentType.match(/\bcharset=([\w-]+)/);
        return match ? match[1] : null;
    }, function (value) {
        this.contentType = this.mediaType + (value ? `;charset=${value}` : "");
    }),
 
  /**
   * The content of this message as a binary stream.
   */
    content: d.gs(function () {
        return this._content;
    }, function (value) {
        if (R.isNil(value)) {
            value = DEFAULT_CONTENT;
        }
 
        if (value instanceof Stream) {
            this._content = value;
            value.pause();
        } else {
            this._content = new Stream(value);
        }
 
        Reflect.deleteProperty(this, "_bufferedContent");
    }),
 
  /**
   * True if the content of this message is buffered, false otherwise.
   */
    isBuffered: d.gs(function () {
        return !R.isNil(this._bufferedContent);
    }),
 
  /**
   * Returns a binary representation of the content of this message up to
   * the given length. This is useful in applications that need to access the
   * entire message body at once, instead of as a stream.
   *
   * Note: 0 is a valid value for maxLength. It means "no limit".
   */
    bufferContent: d(function (maxLength) {
        Eif (R.isNil(this._bufferedContent)) {
            this._bufferedContent = bufferStream(this.content, maxLength);
        }
 
        return this._bufferedContent;
    }),
 
  /**
   * Returns the content of this message up to the given length as a string
   * with the given encoding.
   *
   * Note: 0 is a valid value for maxLength. It means "no limit".
   */
    stringifyContent: d(function (maxLength, encoding) {
        encoding = encoding || this.charset;
 
        return this.bufferContent(maxLength).then(function (chunk) {
            return bodec.toString(chunk, encoding);
        });
    }),
 
  /**
   * Returns a promise for an object of data contained in the content body.
   *
   * The maxLength argument specifies the maximum length (in bytes) that the
   * parser will accept. If the content stream exceeds the maximum length, the
   * promise is rejected with a MaxLengthExceededError. The appropriate response
   * to send to the client in this case is 413 Request Entity Too Large, but
   * many HTTP clients including most web browsers may not understand it.
   *
   * Note: 0 is a valid value for maxLength. It means "no limit".
   */
    parseContent: d(function (maxLength) {
        Iif (this._parsedContent) {
            return this._parsedContent;
        }
 
        if (!R.is(Number, maxLength)) {
            maxLength = DEFAULT_MAX_CONTENT_LENGTH;
        }
 
        const parser = Message.PARSERS[this.mediaType] || defaultParser;
        this._parsedContent = parser(this, maxLength);
 
        return this._parsedContent;
    })
 
});
 
module.exports = Message;