UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

441 lines (363 loc) 14.8 kB
'use strict'; // [Multipurpose Internet Mail Extensions Chapter 5.1 (rfc2046)](https://www.ietf.org/rfc/rfc2046.txt) // boundary := 0*69<bchars> bcharsnospace // // bchars := bcharsnospace / " " // // bcharsnospace := DIGIT / ALPHA / "'" / "(" / ")" / // "+" / "_" / "," / "-" / "." / // "/" / ":" / "=" / "?" // // Overall, the body of a "multipart" entity may be specified as // follows: // // dash-boundary := "--" boundary // ; boundary taken from the value of // ; boundary parameter of the // ; Content-Type field. // // multipart-body := [preamble CRLF] // dash-boundary transport-padding CRLF // body-part *encapsulation // close-delimiter transport-padding // [CRLF epilogue] // // transport-padding := *LWSP-char // ; Composers MUST NOT generate // ; non-zero length transport // ; padding, but receivers MUST // ; be able to handle padding // ; added by message transports. // // encapsulation := delimiter transport-padding // CRLF body-part // // delimiter := CRLF dash-boundary // // close-delimiter := delimiter "--" // // preamble := discard-text // // epilogue := discard-text // // discard-text := *(*text CRLF) *text // ; May be ignored or discarded. // // body-part := MIME-part-headers [CRLF *OCTET] // ; Lines in a body-part must not start // ; with the specified dash-boundary and // ; the delimiter must not appear anywhere // ; in the body part. Note that the // ; semantics of a body-part differ from // ; the semantics of a message, as // ; described in the text. // // OCTET := <any 0-255 octet value> // const Reader = require('./Reader'); const DataReader = require('./DataReader'); const PartReader = require('./PartReader'); const parseContentTypeHeader = require('../http/HttpHeader').parseContentTypeHeader; const DeserializationError = require('../errors/DeserializationError'); const CRLF = '\r\n'; const DELIM = 45; /** * States * @enum {number} * @readonly */ const STATES = { MORE_DATA: 0, READ_NEXT_PREAMBLE: 1, READ_NEXT_BOUNDARY: 2, READ_NEXT_PART: 3, READ_NEXT_PART_RETURN: 4, READ_NEXT_CRLF_BOUNDARY: 5, READ_NEXT_LAST_BOUNDARY: 6, READ_NEXT_EPILOGUE: 7, READ_NEXT_EPILOGUE_RETURN: 8, FINISHED: 9 }; /** * Events * @enum {string} * @readonly */ const EVENTS = { START: 'multipart.start', PREAMBLE_START: 'multipart.preamble.start', PREAMBLE: 'multipart.preamble.data', PREAMBLE_END: 'multipart.preamble.end', EPILOGUE_START: 'multipart.epilogue.start', // EPILOGUE: 'multipart.epilog', // EPILOGUE_END: 'multipart.epilogEnd', END: 'multipart.end' }; /** * Reads a Multipart request from the Cache. * * @extends Reader */ class MultipartParser extends Reader { /** * Factory for automatic creation depending on content-type * * @param {ContentTypeInfo} contentTypeInfo * @param {Object} headers - Header information (currently only the content-type header is evaluated) * @returns {MultipartParser} */ static createInstance(contentTypeInfo, headers) { return new MultipartParser(headers); } /** * @param {Object} [headers] Header information, keys are header-names, values are header value string or objects of type * {ContentTypeInfo}. Used to determine the multipart boundary */ constructor(headers) { super(); if (headers) { const contentType = headers['content-type']; if (!contentType) { throw new DeserializationError('missing content type'); } if (typeof contentType === 'string') { const contentTypeInfo = parseContentTypeHeader(contentType, false); this._stringBoundary = contentTypeInfo.getParameter('boundary'); } else { this._stringBoundary = contentType.getParameter('boundary'); } } if (this._stringBoundary === null || this._stringBoundary === undefined) { // '' is a valid boundary throw new DeserializationError('No boundary found while processing header/request'); } this._boundary = Buffer.from('--' + this._stringBoundary, 'utf8'); this._crlfBoundary = Buffer.from(CRLF + '--' + this._stringBoundary, 'utf8'); /** * @enum {STATES} */ this._state = STATES.READ_NEXT_PREAMBLE; this._partReader = null; this._emittedPreambleStart = false; this._emittedEpilogueStart = false; this._emittedStart = false; this._stopPattern = null; } /** * Sets the stop pattern(boundary), which is used if this multipart is a nested multipart (e.g. an OData change set). * The epilogue of this multipart is then read up to this pattern. * * @param {string} stopPattern */ setStopPattern(stopPattern) { this._stopPattern = stopPattern; } /** * Read cache, if the cache contains processable data, then the data is processed (e.g. boundaries), if the cache * contains incomplete data (e.g. the first half of a boundary) then more data is requested. The attribute _state * ensures a correct re-entry * * @param {ContentDeserializer} reader - Reader instance containing the cache * @param {Cache} cache - Cache to read bytes from * @param {boolean} last - Last call, no more data available * @returns {boolean} * this: this reader needs more data and caller should call this method again with more data in cache * false: this reader is finished caller should pop this reader from stack * null: new sub reader is on stack, call this method again after the sub reader is finished */ readCache(reader, cache, last) { let needMoreData = false; if (!this._emittedStart) { this.emit(EVENTS.START, this._stringBoundary); this._emittedStart = true; } while (needMoreData === false && this._state !== STATES.FINISHED) { switch (this._state) { case STATES.READ_NEXT_PREAMBLE: needMoreData = this.readPreamble(reader, cache, last); break; case STATES.READ_NEXT_BOUNDARY: needMoreData = this.readReadFirstBoundary(reader, cache, last); break; case STATES.READ_NEXT_CRLF_BOUNDARY: needMoreData = this.readCrlfBoundary(reader, cache, last); break; case STATES.READ_NEXT_PART: this._partReader = new PartReader(this._crlfBoundary).setEmitter(this._emitter); reader.pushReader(this._partReader); this._state = STATES.READ_NEXT_PART_RETURN; needMoreData = null; break; case STATES.READ_NEXT_PART_RETURN: this._state = STATES.READ_NEXT_CRLF_BOUNDARY; break; case STATES.READ_NEXT_EPILOGUE: needMoreData = this.readReadNextEpilogue(reader, cache); break; case STATES.READ_NEXT_EPILOGUE_RETURN: this._state = STATES.FINISHED; needMoreData = false; break; default: throw new DeserializationError('Internal parser error'); } } if (this._state === STATES.FINISHED) { this.emit(EVENTS.END); return false; } return needMoreData; } readReadNextEpilogue(reader, cache) { // Special handling for nested multipart requests // After a '--boundary--' a epilogue may be there. ABNF : ... [CRLF epilogue] // But for nested parts we have also a stop pattern, e.g. --boundary_outer'. // The there may '--boundary--'[CRLF]'--boundary_outer'[*.] and boundary_outer MUST // no detected as epilogue. So we check here if the stop pattern is there and if yes // the nested multipart is finished. if (this._stopPattern) { if (cache.length - cache.getReadPos() < this._stopPattern.length) { return true; } if (cache.indexOf(this._stopPattern, cache.getSearchPosition()) === cache.getSearchPosition()) { this._state = STATES.FINISHED; return null; } } cache.advance(CRLF.length); // because it is not read by the closing boundary if (!this._emittedEpilogueStart) { this.emit(EVENTS.EPILOGUE_START); this._emittedEpilogueStart = true; } // Epilogue can read like data till the outer stop pattern occures or there is no input this.dataReader = new DataReader(this._emitter) .setEmitter(this._emitter, 'multipart.epilogue'); if (this._stopPattern) { this.dataReader.setStopPattern(this._stopPattern); } reader.pushReader(this.dataReader); this._state = STATES.READ_NEXT_EPILOGUE_RETURN; return null; } /** * Read a first boundary from the cache. * Update getReadPos, searchPos, consumedBytes accordingly * * @param {Reader} reader - The reader * @param {Cache} cache - The readers cache * @returns {boolean} - returns true if more data is required, otherwise false * @throws {Error} - Throws and error if boundary is not found */ readReadFirstBoundary(reader, cache) { // required data available if (cache.getLength() - cache.getReadPos() < this._boundary.length + 2) { return true; } // boundary MUST be at searchPos const check = cache.indexOf(this._boundary, cache.getSearchPosition()); if (check !== cache.getSearchPosition()) { throw new DeserializationError(`Boundary expected at position ${cache.getReadPos()}`); } cache.advanceSearchPosition(this._boundary.length); // boundary MAY contain trialing spaces before CRLF const pos = cache.indexOf(CRLF, cache.getSearchPosition()); if (pos === -1) { // no CRLF found return true; } this._state = STATES.READ_NEXT_PART; // consume boundary cache.advance(pos - cache.getReadPos() + CRLF.length); return false; } /** * Read a non first boundary from the cache and consume boundary * * @param {Reader} reader - The reader * @param {Cache} cache - The readers cache * @param {boolean} last - Last call, no more data available * @returns {boolean} - returns true if more data is required, otherwise false */ readCrlfBoundary(reader, cache, last) { // required data available if (cache.getLength() - cache.getReadPos() < this._crlfBoundary.length + 2) { return true; } // boundary MUST be at searchPos const check = cache.indexOf(this._crlfBoundary, cache.getSearchPosition()); if (check !== cache.getSearchPosition()) { throw new DeserializationError(`Boundary expected at position ${cache.getReadPos()}`); } // boundary MAY contain trialing spaces before CRLF const pos = cache.indexOf(CRLF, cache.getSearchPosition() + this._crlfBoundary.length); if (pos === -1) { // no CRLF found if (last) { // last boundary's CRLF is optional, but trailing -- is a MUST if (cache.getByte(this._crlfBoundary.length) === DELIM && cache.getByte(this._crlfBoundary.length + 1) === DELIM) { cache.advance(this._crlfBoundary.length + 2); this._state = STATES.FINISHED; return false; } throw new DeserializationError( `Last boundary of multipart must end with '--' at position ${cache.getReadPos()}`); } else { // request more data return true; } } // CRLF found if (cache.getByte(this._crlfBoundary.length) === DELIM && cache.getByte(this._crlfBoundary.length + 1) === DELIM) { // is last boundary this._state = STATES.READ_NEXT_EPILOGUE; cache.advance(pos - cache.getReadPos()); // keep the CLRF it is parsed in the READ_NEXT_EPILOGUE step } else { this._state = STATES.READ_NEXT_PART; cache.advance(pos + CRLF.length - cache.getReadPos()); } // consume boundary return false; } /** * Read preamble * * @param {Reader} reader - The reader * @param {Cache} cache - The readers cache * @returns {boolean} - returns true if more data is required, otherwise false */ readPreamble(reader, cache) { // required data available if (cache.getLength() - cache.getReadPos() < this._boundary.length) { // min a boundary is required return true; // need more data } // the preamble is optional, check if cache starts with boundary if (this._boundary.compare(cache._cache, cache.getReadPos(), cache.getReadPos() + this._boundary.length) === 0) { // cache starts with boundary, no preamble defined this._state = STATES.READ_NEXT_BOUNDARY; // as next step consume the boundary return false; } // preamble found if (!this._emittedPreambleStart) { this.emit(EVENTS.PREAMBLE_START); this._emittedPreambleStart = true; } // search for next boundary (now with leading space) const pos = cache.indexOf(this._crlfBoundary, cache.getSearchPosition()); if (pos === -1) { // boundary not found in cache, emit the first bytes from the cache where it is // guaranteed that there is no part fo the boundary inside const readUpTo = cache.getLength() - this._crlfBoundary.length < cache.getReadPos() ? cache.getReadPos() : cache.getLength() - this._crlfBoundary.length; this.emitAndConsume(cache, readUpTo - cache.getReadPos(), EVENTS.PREAMBLE); return true; } this.emitAndConsume(cache, pos - cache.getReadPos(), EVENTS.PREAMBLE); this.emit(EVENTS.PREAMBLE_END); this._state = STATES.READ_NEXT_CRLF_BOUNDARY; return false; } } MultipartParser.EVENTS = EVENTS; module.exports = MultipartParser;