diff --git a/.mailmap b/.mailmap index 842d610df87..e7efb4f43ab 100644 --- a/.mailmap +++ b/.mailmap @@ -1,8 +1,10 @@ Aaron Bieber Aaron Heckmann +Aayush Ahuja aayush.a Abe Fettig Akito Ito Alejandro Oviedo Garcia Alejandro Oviedo +Alex Gilbert agilbert Alex Hultman Alex Jordan AJ Jordan Alex Kocharin @@ -19,6 +21,7 @@ Andy Bettisworth Angel Stoyanov atstojanov Anna Henningsen Anna Magdalena Kedzierska AnnaMag +Antoine Amara Antoine AMARA Aria Stewart Arlo Breault Arnaud Lefebvre BlackYoup @@ -26,6 +29,7 @@ Artem Zaytsev Artur G Vieira Artur Vieira Arnout Kazemier <3rd-Eden@users.noreply.github.com> Asaf David asafdav2 +Ashley Maceli ashleyraymaceli Atsuo Fukaya Ben Lugavere blugavere Ben Noordhuis @@ -42,30 +46,37 @@ Beth Griggs Bethany N Griggs BethGriggs Bidisha Pyne Brad Decker brad-decker +Brad Larson BradLarson Bradley Meck Bradley Farias Brandon Benvie Brandon Kobel kobelb Brendan Ashworth +Brent Pendergraft penDerGraft Brian White Brian White Caleb Boyd Calvin Metcalf Calvin Metcalf +Caralyn Reisle creisle Charles Chew Choon Keat Charles Rudolph +Chris Andrews cpandrews8 Chris Johnson +Chris Young Claudio Rodriguez Colin Ihrig Christophe Naud-Dulude Chris911 Christopher Lenz Dan Kaplun Dan Williams Dan.Williams +Daniel Abrão Daniel Abrão > Daniel Bevenius daniel.bevenius Daniel Berger Daniel Chcouri <333222@gmail.com> Daniel Gröber Daniel Gröber +Daniel Paulino dpaulino Daniel Pihlström Daniel Wang firedfox Daniel Wang firedfox @@ -84,6 +95,7 @@ Eduard Burtescu Einar Otto Stangvik Elliott Cable Eric Phetteplace +Erwin W. Ramadhan erwinwahyura Eugene Obrezkov ghaiklor EungJun Yi Evan Larkin @@ -99,14 +111,21 @@ Felix Geisendörfer Flandre Scarlet Flandre Florian Margaine Florian MARGAINE Forrest L Norvell +Franziska Hinkelmann F. Hinkelmann Friedemann Altrock Fuji Goro Gabriel de Perthuis Gareth Ellis +Geoffrey Bugaisky gbugaisky Gibson Fahnestock Gil Pedersen Graham Fairweather Xotic750 Greg Sabia Tucker +Gregor Martynus Gregor +Guy Bedford guybedford +Halil İbrahim Şener hisener +Hannah Kim heeeunkimmm +Hendrik Schwalm hschwalm Hitesh Kanwathirtha Henry Chin Herbert Vojčík @@ -122,8 +141,10 @@ Italo A. Casas Jackson Tian Jake Verbaten Jamen Marzonie Jamen Marz +James Beavers Druotic James Hartig James M Snell +James Nimlos JamesNimlos Jan Krems Jenna Vuong JeongHoon Byun Outsider @@ -139,13 +160,17 @@ Johann Hofmann John Barboza jBarz John Barboza jBarz John Gardner Alhadis +John McGuirk jmcgui05 Johnny Ray Austin Johnny Ray Jon Tippens legalcodes Jonas Pfenniger +Jonathan Gourlay mrgorbo Jonathan Ong Jonathan Persson Jonathan Rentzsch +Jose Luis Vivero jlvivero Josh Erickson +Josh Hunter jopann Joshua S. Weinstein Joyee Cheung joyeecheung Juan Soto @@ -154,6 +179,7 @@ Julien Waechter julien.waechter Junliang Yan Junshu Okamoto jun-oka +Justin Beckwith Jérémy Lal Jérémy Lal Kai Sasaki Lewuathe @@ -172,17 +198,21 @@ Lydia Kats Lydia Katsamberis Maciej Małecki Malte-Thorben Bruns Malte-Thorben Bruns skenqbx +Manil Chowdhurian Chowdhurian Marcelo Gobelli decareano Marcin Cieślak Marcin Cieślak Marcin Zielinski marzelin Marti Martz Martial James Jefferson +Martijn Schrage Oblosys Matt Lang matt-in-a-hat +Matt Reed matthewreed26 Matthias Bastian piepmatz Mathias Buus Mathias Pettersson Matthew Lye +Maurice Hayward maurice_hayward Michael Bernstein Michael Dawson Michael Wilber @@ -192,6 +222,7 @@ Micheil Smith Micleusanu Nicu Miguel Angel Asencio Hurtado maasencioh Mikael Bourges-Sevenier +Mike Kaufman Minqi Pan P.S.V.R Minwoo Jung JungMinu Miroslav Bajtoš @@ -208,8 +239,11 @@ Onne Gorter Paul Querna Pedro Lima Pedro Victor Pedro Lima Pedro lima +Peng Lyu rebornix Peter Flannery +Peter Paugh Peter Phillip Johnsen +Rachel White rachelnicole Ratikesh Misra Ravindra Barthwal Ravindra barthwal Ray Morgan @@ -220,11 +254,14 @@ Refael Ackermann Reza Akhavan jedireza Ricardo Sánchez Gregorio richnologies Rick Olson +Rob Adelmann +Rob Adelmann adelmann Rod Machen Roman Klauke Roman Reiss Ron Korving Ron Korving ronkorving +Russell Dempsey Ryan Dahl Ryan Emery Ryan Scheel Ryan Scheel @@ -245,11 +282,14 @@ Scott Blomquist Segu Riluvan Sergey Kryzhanovsky Shannen Saez +Shaopeng Zhang szhang351 Shigeki Ohtsu Shigeki Ohtsu +Shiya Luo shiya Siddharth Mahendraker Simon Willison Siobhan O'Donovan justshiv +Siyuan Gao r1cebank solebox solebox <5013box@gmail.com> Sreepurna Jasti Sreepurna Jasti sreepurnajasti @@ -261,6 +301,7 @@ Steve Mao Steven R. Loomis Stewart X Addison Stewart Addison Stewart X Addison sxa555 +Suraiya Hameed suraiyah Suramya shah ss22ever Surya Panikkal surya panikkal Surya Panikkal suryagh @@ -268,6 +309,8 @@ Taehee Kang hugnosis Tanuja-Sawant Taylor Woll taylor.woll Thomas Watson Steen Thomas Watson +Timur Shemsedinov tshemsedinov +Toby Farley tobyfarley Toby Stableford toboid Todd Kennedy TJ Holowaychuk @@ -278,6 +321,8 @@ Tarun Batra Tarun Ted Young Thomas Lee Thomas Reggi +Tierney Cyren &! (bitandbang) +Tierney Cyren bitandbang Tim Caswell Tim Price Tim Smart @@ -285,6 +330,8 @@ Tim Smart Timothy Leverett Timothy Tom Hughes Tom Hughes-Croucher +Tom Purcell tpurcell +Tomoki Okahana umatoma Travis Meisenheimer Trevor Burnham Tyler Larson @@ -295,10 +342,12 @@ vsemozhetbyt Vse Mozhet Byt Wang Xinyong Weijia Wang <381152119@qq.com> starkwang <381152119@qq.com> Willi Eggeling +Wyatt Preul geek xiaoyu <306766053@qq.com> Poker <306766053@qq.com> Yazhong Liu Yazhong Liu Yazhong Liu Yorkie Yazhong Liu Yorkie +Yazhong Liu Yorkie Liu Yoshihiro KIKUCHI Yosuke Furukawa Yuichiro MASUI diff --git a/AUTHORS b/AUTHORS index 977f3b619f4..ae3d6864dfc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -710,7 +710,7 @@ Ryan Scheel Benjamin Gruenbaum Pavel Medvedev Russell Dempsey -&! (bitandbang) +Tierney Cyren h7lin Michael Dawson Ruben Verborgh @@ -957,7 +957,7 @@ Bogdan Lobor Mihai Potra Brendon Pierson Brad Hill -Mike Kaufman +Mike Kaufman Igor Kalashnikov Amery James Reggio @@ -1194,12 +1194,12 @@ scalkpdev Ashton Kinslow Kevin Zurawel Wes Tyler -shiya +Shiya Luo Joyee Cheung Greg Valdez Bidur Adhikari Kyle Carter -adelmann +Rob Adelmann Daniel Pittman Ian White Chris Bystrek @@ -1274,7 +1274,6 @@ Sam Shull Michael-Bryant Choa CodeVana Daniel Sims -Rob Adelmann Diego Paez Paul Graham Jared Young @@ -1401,7 +1400,7 @@ James Sumners Bradley Curran chiaki-yokoo Benjamin Fleischer -maurice_hayward +Maurice Hayward Ali BARIN Nemanja Stojanovic Jeroen Mandersloot @@ -1526,11 +1525,10 @@ realwakka Gergely Nemeth Samuel Reed Anshul Guleria -Justin Beckwith +Justin Beckwith Scott McKenzie Julien Klepatch Dan Homola -Chris Young cornholio <0@mcornholio.ru> Tamás Hódi DuanPengfei <2459714173@qq.com> @@ -1667,5 +1665,311 @@ George Bezerra Benjamin Coe Tim Costa Rahul Mishra +Damien O'Reilly +Tuan Anh Tran +Alex Gresnel <31708810+agresnel@users.noreply.github.com> +Will Young +Martijn Schrage +Halil İbrahim Şener +Guy Bedford +Antoine Amara +Mani Maghsoudlou +Bartek Szczepański +Roga Pria Sembada +Jussi Räsänen +Thomas Corbière +atvoicu +Peng Lyu +Yang-Kichang +JP Wesselink +Rami Moshe +Rimas Misevičius +Jack Horton +Trivikram Kamat +Jose Luis Vivero +John-David Dalton +Pavel Pomerantsev +Daniela Borges Matos de Carvalho +Bruce Fletcher +Greg Byram +Manil Chowdhurian +Jonathan Eskew +James M. Greene +Pooya Paridel +Paul Berry +Ruxandra Fediuc +Saeed H +Rachel White +Geoffrey Bugaisky +Sam Skjonsberg +Emily Marigold Klassen +Ashley Maceli +Thomas Schorn +John Miller +rhalldearn +Annie Weng +Sean Cox +Luke Greenleaf +Alec Ferguson +Laura Cabrera +Barry Tam +Eric Pemberton +Josh Hunter +BinarySo1o +Chris Budy +Emily Platzer +jacjam +Brant Barger +Daniel Paulino +Emily Ford +Luis Del Águila +Mujtaba Al-Tameemi +Govee91 +joanne-jjb +Brad Larson +Alice Tsui +Greg Matthews +Daniel Kostro +Faisal Yaqoob +Alex McKenzie +Hannah Kim +Paul Milham +Christopher Choi +Suraiya Hameed +Charlie Duong +Joe Grace +Justin Lee +Brent Pendergraft +Gene Wu +nodexpertsdev +Rob Paton +Daniele Lisi +Sushil Tailor +Ben Michel +John McGuirk +Colin Leong +Caralyn Reisle +Savio Lucena +Rafal Leszczynski +Ivan Etchart +Robin Lungwitz +ryshep111 +gitHubTracey +tabulatedreams +Charles T Wall III +Minya Liang +Kinnan Kwok +Adil L +Seth Holladay +Chris Andrews +Matt Reed +Joe Henry +Alireza Alidousti +James Beavers +Cameron Burwell +Jakub Mrowiec - Alkagar +Oliver Luebeck +Chris Jimenez +James Hodgskiss +Guilherme Akio Sakae +Martin Michaelis +Christopher Sidebottom +Edward Andrew Robinson +Nigel Kibodeaux +Shakeel Mohamed +Tobias Kieslich +Ruy Adorno +Stefania Sharp +Pawel Golda +Steven Scott +Alex Gilbert +Siyuan Gao +Nicola Del Gobbo +Josh Lim +Feon Sua +Shawn McGinty +Jason Walton +Jonathan Gourlay +Peter Paugh +Gregor Martynus +Joel Dart +Tri Nguyen +Kasim Doctor +Steve Jenkins +AlexeyM +Nicolas Chaulet +Adarsh Honawad +Tim Ermilov +ekulnivek +Ethan Brown +Lewis Zhang +Kat Rosario +jpaulptr +Donovan Buck +Toby Farley +Suresh Srinivas +Alberto Lopez de Lara +Jem Bezooyen +Bob Clewell +Raj Parekh +Tom Boutell +Cristian Peñarrieta +Christian Murphy +Dolapo Toki +Shaopeng Zhang +Matthew Meyer +Chad Zezula +Eric Freiberg +Mabry Cervin +shaohui.liu2000@gmail.com +Chi-chi Wang +Roger Jiang +Cheyenne Arrowsmith +Tim Chon +Michael Pal +Fadi Asfour +Christina Chan +Alessandro Vergani +Ali Groening +Mike Fleming +WeiPlanet +243083df <243083df@dispostable.com> +Komivi Agbakpem +Tyler Seabrook +Bear Trickey +NiveditN +Shaun Sweet +James Nimlos +Kim Gentes +Vladimir Ilic +Randal Hanford +Jean-Baptiste Brossard +Orta +Ben Hallion +twk-b +Lam Chan +Jenna Zeigen +Lukas +tejbirsingh +Hendrik Schwalm +Jeremy Huang +Michael Rueppel +David8472 +Luke Childs +Robert Nagy +Nikki St Onge +zhangzifa +hwaisiu +Thomas Karsten +Lance Barlaan +Alvaro Cruz +Jean-Philippe Blais +Oscar Funes +Kanika Shah +Jack Wang +Braden Whitten +Omar Gonzalez +Supamic +Nikhil Komawar +Daniel Abrão +elisa lee +mog422 +André Føyn Berge +Tom Purcell +Tomoki Okahana +Aayush Ahuja +Paul Marion Camantigue +Jayson D. Henkel +Nicolas 'Pixel' Noble +Ashish Kaila +c0b <14798161+c0b@users.noreply.github.com> +Damian +Alec Perkins +Teppei Sato +Jinwoo Lee +Peter Marton +Erwin W. Ramadhan +Mark Walker +sharkfisher +nhoel +Hadis-Fard +Scott J Beck +Raphael Rheault +Iryna Yaremtso +Casie Lynch +Matthew Cantelon +Ben Halverson +cPhost <23620441+cPhost@users.noreply.github.com> +dicearr +Lucas Azzola +Ken Takagi +Ed Schouten +Andrew Stucki +Anthony Nandaa +Mithun Sasidharan +Mattias Holmlund +Mark S. Everitt +Alexey Kuzmin +gowpen <33104741+gowpen@users.noreply.github.com> +Adam Wegrzynek +Sascha Tandel +Patrick Heneise +Dumitru Glavan +Giovanni Lela +Matthias Reis +John Byrne +Octavian Ionescu +Kevin Yu +Jimi van der Woning +Dara Hayes +Maring, Damian Lion +Attila Gonda +Brian O'Connell +Sean Karson +Nicolas Morel +fjau +SonaySevik +jonask +Delapouite +Mark McNelis +mbornath +Andres Kalle +Paul Blanche +Vipin Menon +woj +Adam Jeffery +Paul Ashfield +Katie Stockton Roberts +Mamatha J V +Neil Vass +Vidya Subramanyam +Swathi Kalahastri +Tanvi Kini +Sabari Lakshmi Krishnamoorthy +Kabir Islam +subrahmanya chari p +Suryanarayana Murthy N +Chandrakala +Jayashree S Kumar +Nayana Das K +Anawesha Khuntia +Maton Anthony +saiHemak +Deepthi Sebastian +Pawan Jangid +Stephan Smith +joelostrowski +Javier Blanco +Cyril Lakech <1169286+clakech@users.noreply.github.com> +Grant Gasparyan +Klemen Kogovsek +Gus Caplan +ka3e +ChrBergert +sercan yersen +Steve Kinney +Sebastian Mayr +Vijayalakshmi Kannan +Benjamin Zaslavsky # Generated by tools/update-authors.sh diff --git a/benchmark/http2/headers.js b/benchmark/http2/headers.js index 62156774caa..3c8d0465acb 100644 --- a/benchmark/http2/headers.js +++ b/benchmark/http2/headers.js @@ -13,7 +13,9 @@ function main(conf) { const n = +conf.n; const nheaders = +conf.nheaders; const http2 = require('http2'); - const server = http2.createServer(); + const server = http2.createServer({ + maxHeaderListPairs: 20000 + }); const headersObject = { ':path': '/', @@ -34,7 +36,9 @@ function main(conf) { stream.end('Hi!'); }); server.listen(PORT, () => { - const client = http2.connect(`http://localhost:${PORT}/`); + const client = http2.connect(`http://localhost:${PORT}/`, { + maxHeaderListPairs: 20000 + }); function doRequest(remaining) { const req = client.request(headersObject); diff --git a/configure b/configure index 20d884e486e..d2fc9f01526 100755 --- a/configure +++ b/configure @@ -220,6 +220,27 @@ shared_optgroup.add_option('--shared-libuv-libpath', dest='shared_libuv_libpath', help='a directory to search for the shared libuv DLL') +shared_optgroup.add_option('--shared-nghttp2', + action='store_true', + dest='shared_nghttp2', + help='link to a shared nghttp2 DLL instead of static linking') + +shared_optgroup.add_option('--shared-nghttp2-includes', + action='store', + dest='shared_nghttp2_includes', + help='directory containing nghttp2 header files') + +shared_optgroup.add_option('--shared-nghttp2-libname', + action='store', + dest='shared_nghttp2_libname', + default='nghttp2', + help='alternative lib name to link to [default: %default]') + +shared_optgroup.add_option('--shared-nghttp2-libpath', + action='store', + dest='shared_nghttp2_libpath', + help='a directory to search for the shared nghttp2 DLLs') + shared_optgroup.add_option('--shared-openssl', action='store_true', dest='shared_openssl', @@ -1465,6 +1486,7 @@ configure_library('zlib', output) configure_library('http_parser', output) configure_library('libuv', output) configure_library('libcares', output) +configure_library('nghttp2', output) # stay backwards compatible with shared cares builds output['variables']['node_shared_cares'] = \ output['variables'].pop('node_shared_libcares') diff --git a/deps/v8/include/v8-platform.h b/deps/v8/include/v8-platform.h index ed2acc3a74e..6c3c4292c5c 100644 --- a/deps/v8/include/v8-platform.h +++ b/deps/v8/include/v8-platform.h @@ -36,6 +36,51 @@ class IdleTask { virtual void Run(double deadline_in_seconds) = 0; }; +/** + * A TaskRunner allows scheduling of tasks. The TaskRunner may still be used to + * post tasks after the isolate gets destructed, but these tasks may not get + * executed anymore. All tasks posted to a given TaskRunner will be invoked in + * sequence. Tasks can be posted from any thread. + */ +class TaskRunner { + public: + /** + * Schedules a task to be invoked by this TaskRunner. The TaskRunner + * implementation takes ownership of |task|. + */ + virtual void PostTask(std::unique_ptr task) = 0; + + /** + * Schedules a task to be invoked by this TaskRunner. The task is scheduled + * after the given number of seconds |delay_in_seconds|. The TaskRunner + * implementation takes ownership of |task|. + */ + virtual void PostDelayedTask(std::unique_ptr task, + double delay_in_seconds) = 0; + + /** + * Schedules an idle task to be invoked by this TaskRunner. The task is + * scheduled when the embedder is idle. Requires that + * TaskRunner::SupportsIdleTasks(isolate) is true. Idle tasks may be reordered + * relative to other task types and may be starved for an arbitrarily long + * time if no idle time is available. The TaskRunner implementation takes + * ownership of |task|. + */ + virtual void PostIdleTask(std::unique_ptr task) = 0; + + /** + * Returns true if idle tasks are enabled for this TaskRunner. + */ + virtual bool IdleTasksEnabled() = 0; + + TaskRunner() = default; + virtual ~TaskRunner() = default; + + private: + TaskRunner(const TaskRunner&) = delete; + TaskRunner& operator=(const TaskRunner&) = delete; +}; + /** * The interface represents complex arguments to trace events. */ @@ -150,6 +195,28 @@ class Platform { */ virtual size_t NumberOfAvailableBackgroundThreads() { return 0; } + /** + * Returns a TaskRunner which can be used to post a task on the foreground. + * This function should only be called from a foreground thread. + */ + virtual std::shared_ptr GetForegroundTaskRunner( + Isolate* isolate) { + // TODO(ahaas): Make this function abstract after it got implemented on all + // platforms. + return {}; + } + + /** + * Returns a TaskRunner which can be used to post a task on a background. + * This function should only be called from a foreground thread. + */ + virtual std::shared_ptr GetBackgroundTaskRunner( + Isolate* isolate) { + // TODO(ahaas): Make this function abstract after it got implemented on all + // platforms. + return {}; + } + /** * Schedules a task to be invoked on a background thread. |expected_runtime| * indicates that the task will run a long time. The Platform implementation diff --git a/doc/api/errors.md b/doc/api/errors.md index c2a1a4f2145..d7b5de448ff 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -908,6 +908,16 @@ limit. A message payload was specified for an HTTP response code for which a payload is forbidden. + +### ERR_HTTP2_PING_CANCEL + +An HTTP/2 ping was cancelled. + + +### ERR_HTTP2_PING_LENGTH + +HTTP/2 ping payloads must be exactly 8 bytes in length. + ### ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED diff --git a/doc/api/http2.md b/doc/api/http2.md index c1000661a92..3500c563a27 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -344,6 +344,44 @@ acknowledgement for a sent SETTINGS frame. Will be `true` after calling the `http2session.settings()` method. Will be `false` once all sent SETTINGS frames have been acknowledged. +#### http2session.ping([payload, ]callback) + + +* `payload` {Buffer|TypedArray|DataView} Optional ping payload. +* `callback` {Function} +* Returns: {boolean} + +Sends a `PING` frame to the connected HTTP/2 peer. A `callback` function must +be provided. The method will return `true` if the `PING` was sent, `false` +otherwise. + +The maximum number of outstanding (unacknowledged) pings is determined by the +`maxOutstandingPings` configuration option. The default maximum is 10. + +If provided, the `payload` must be a `Buffer`, `TypedArray`, or `DataView` +containing 8 bytes of data that will be transmitted with the `PING` and +returned with the ping acknowledgement. + +The callback will be invoked with three arguments: an error argument that will +be `null` if the `PING` was successfully acknowledged, a `duration` argument +that reports the number of milliseconds elapsed since the ping was sent and the +acknowledgement was received, and a `Buffer` containing the 8-byte `PING` +payload. + +```js +session.ping(Buffer.from('abcdefgh'), (err, duration, payload) => { + if (!err) { + console.log(`Ping acknowledged in ${duration} milliseconds`); + console.log(`With payload '${payload.toString()}`); + } +}); +``` + +If the `payload` argument is not specified, the default payload will be the +64-bit timestamp (little endian) marking the start of the `PING` duration. + #### http2session.remoteSettings - -* stream {Http2Stream} -* code {number} Unsigned 32-bit integer identifying the error code. **Default:** - `http2.constant.NGHTTP2_NO_ERROR` (`0x00`) -* Returns: {undefined} - -Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing the given -`Http2Stream` to be closed on both sides using [error code][] `code`. - #### http2session.setTimeout(msecs, callback) - -* `stream` {Http2Stream} -* `options` {Object} - * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream, - the given stream is made the sole direct dependency of the parent, with - all other existing dependents made a dependent of the given stream. **Default:** - `false` - * `parent` {number} Specifies the numeric identifier of a stream the given - stream is dependent on. - * `weight` {number} Specifies the relative dependency of a stream in relation - to other streams with the same `parent`. The value is a number between `1` - and `256` (inclusive). - * `silent` {boolean} When `true`, changes the priority locally without - sending a `PRIORITY` frame to the connected peer. -* Returns: {undefined} - -Updates the priority for the given `Http2Stream` instance. - #### http2session.settings(settings) +```C +NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env, + uv_loop_t** loop); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[out] loop`: The current libuv loop instance. + [Promises]: #n_api_promises [Simple Asynchronous Operations]: #n_api_simple_asynchronous_operations [Custom Asynchronous Operations]: #n_api_custom_asynchronous_operations diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 4da05899b09..3be2705648d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -299,6 +299,8 @@ E('ERR_HTTP2_OUT_OF_STREAMS', 'No stream ID is available because maximum stream ID has been reached'); E('ERR_HTTP2_PAYLOAD_FORBIDDEN', 'Responses with %s status must not have a payload'); +E('ERR_HTTP2_PING_CANCEL', 'HTTP2 ping cancelled'); +E('ERR_HTTP2_PING_LENGTH', 'HTTP2 ping payload must be 8 bytes'); E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers'); E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams'); E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent'); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 2e20c9ea88a..de20eb23f2c 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -4,6 +4,7 @@ require('internal/util').assertCrypto(); +const { async_id_symbol } = process.binding('async_wrap'); const binding = process.binding('http2'); const assert = require('assert'); const { Buffer } = require('buffer'); @@ -22,11 +23,12 @@ const { onServerStream, } = require('internal/http2/compat'); const { utcDate } = require('internal/http'); const { promisify } = require('internal/util'); -const { isUint8Array } = require('internal/util/types'); +const { isArrayBufferView } = require('internal/util/types'); const { _connectionListener: httpConnectionListener } = require('http'); const { createPromise, promiseResolve } = process.binding('util'); const debug = util.debuglog('http2'); +const kMaxStreams = (2 ** 31) - 1; const { assertIsObject, @@ -53,7 +55,7 @@ const { unenroll } = require('timers'); -const { WriteWrap } = process.binding('stream_wrap'); +const { ShutdownWrap, WriteWrap } = process.binding('stream_wrap'); const { constants } = binding; const NETServer = net.Server; @@ -100,7 +102,6 @@ const { NGHTTP2_REFUSED_STREAM, NGHTTP2_SESSION_CLIENT, NGHTTP2_SESSION_SERVER, - NGHTTP2_ERR_NOMEM, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE, NGHTTP2_ERR_INVALID_ARGUMENT, NGHTTP2_ERR_STREAM_CLOSED, @@ -145,7 +146,7 @@ function emit(self, ...args) { // create the associated Http2Stream instance and emit the 'stream' // event. If the stream is not new, emit the 'headers' event to pass // the block of headers on. -function onSessionHeaders(id, cat, flags, headers) { +function onSessionHeaders(handle, id, cat, flags, headers) { const owner = this[kOwner]; const type = owner[kType]; _unrefActive(owner); @@ -160,11 +161,10 @@ function onSessionHeaders(id, cat, flags, headers) { const obj = toHeaderObject(headers); if (stream === undefined) { + const opts = { readable: !endOfStream }; // owner[kType] can be only one of two possible values if (type === NGHTTP2_SESSION_SERVER) { - stream = new ServerHttp2Stream(owner, id, - { readable: !endOfStream }, - obj); + stream = new ServerHttp2Stream(owner, handle, id, opts, obj); if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) { // For head requests, there must not be a body... // end the writable side immediately. @@ -172,7 +172,7 @@ function onSessionHeaders(id, cat, flags, headers) { stream[kState].headRequest = true; } } else { - stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream }); + stream = new ClientHttp2Stream(owner, handle, id, opts); } streams.set(id, stream); process.nextTick(emit, owner, 'stream', stream, obj, flags, headers); @@ -210,16 +210,8 @@ function onSessionHeaders(id, cat, flags, headers) { // event handler returns, those are sent off for processing. Note that this // is a necessarily synchronous operation. We need to know immediately if // there are trailing headers to send. -function onSessionTrailers(id) { - const owner = this[kOwner]; - debug(`[${sessionName(owner[kType])}] checking for trailers`); - const streams = owner[kState].streams; - const stream = streams.get(id); - // It should not be possible for the stream not to exist at this point. - // If it does not exist, there is something very very wrong. - assert(stream !== undefined, - 'Internal HTTP/2 Failure. Stream does not exist. Please ' + - 'report this as a bug in Node.js'); +function onStreamTrailers() { + const stream = this[kOwner]; const trailers = Object.create(null); stream[kState].getTrailers.call(stream, trailers); const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer); @@ -234,24 +226,16 @@ function onSessionTrailers(id) { // Http2Stream instance. Note that this event is distinctly different than the // require('stream') interface 'close' event which deals with the state of the // Readable and Writable sides of the Duplex. -function onSessionStreamClose(id, code) { - const owner = this[kOwner]; - debug(`[${sessionName(owner[kType])}] session is closing the stream ` + - `${id}: ${code}`); - const stream = owner[kState].streams.get(id); - if (stream === undefined) - return; - _unrefActive(owner); - // Set the rst state for the stream +function onStreamClose(code) { + const stream = this[kOwner]; + _unrefActive(stream); + _unrefActive(stream[kSession]); abort(stream); const state = stream[kState]; state.rst = true; state.rstCode = code; - - if (state.fd !== undefined) { - debug(`Closing fd ${state.fd} for stream ${id}`); + if (state.fd !== undefined) fs.close(state.fd, afterFDClose.bind(stream)); - } setImmediate(stream.destroy.bind(stream)); } @@ -270,26 +254,16 @@ function onSessionError(error) { // Receives a chunk of data for a given stream and forwards it on // to the Http2Stream Duplex for processing. -function onSessionRead(nread, buf, handle) { - const owner = this[kOwner]; - const streams = owner[kState].streams; - const id = handle.id; - const stream = streams.get(id); - // It should not be possible for the stream to not exist at this point. - // If it does not, something is very very wrong - assert(stream !== undefined, - 'Internal HTTP/2 Failure. Stream does not exist. Please ' + - 'report this as a bug in Node.js'); - _unrefActive(owner); // Reset the session timeout timer - _unrefActive(stream); // Reset the stream timeout timer +function onStreamRead(nread, buf, handle) { + const stream = handle[kOwner]; + _unrefActive(stream); + _unrefActive(stream[kSession]); if (nread >= 0 && !stream.destroyed) { - // prevent overflowing the buffer while pause figures out the - // stream needs to actually pause and streamOnPause runs - if (!stream.push(buf)) - owner[kHandle].streamReadStop(id); + if (!stream.push(buf)) { + handle.readStop(); + } return; } - // Last chunk was received. End the readable side. stream.push(null); } @@ -310,11 +284,8 @@ function onSettings(ack) { owner[kRemoteSettings] = undefined; } // Only emit the event if there are listeners registered - if (owner.listenerCount(event) > 0) { - const settings = event === 'localSettings' ? - owner.localSettings : owner.remoteSettings; - process.nextTick(emit, owner, event, settings); - } + if (owner.listenerCount(event) > 0) + process.nextTick(emit, owner, event, owner[event]); } // If the stream exists, an attempt will be made to emit an event @@ -417,15 +388,14 @@ function requestOnConnect(headers, options) { // ret will be either the reserved stream ID (if positive) // or an error code (if negative) - const ret = session[kHandle].submitRequest(headersList, - streamOptions, - options.parent | 0, - options.weight | 0, - !!options.exclusive); + const ret = session[kHandle].request(headersList, + streamOptions, + options.parent | 0, + options.weight | 0, + !!options.exclusive); // In an error condition, one of three possible response codes will be // possible: - // * NGHTTP2_ERR_NOMEM - Out of memory, this should be fatal to the process. // * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE - Maximum stream ID is reached, this // is fatal for the session // * NGHTTP2_ERR_INVALID_ARGUMENT - Stream was made dependent on itself, this @@ -433,31 +403,27 @@ function requestOnConnect(headers, options) { // For the first two, emit the error on the session, // For the third, emit the error on the stream, it will bubble up to the // session if not handled. - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, session, 'error', err); - break; - case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: - err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS'); - process.nextTick(emit, session, 'error', err); - break; - case NGHTTP2_ERR_INVALID_ARGUMENT: - err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY'); - process.nextTick(emit, this, 'error', err); - break; - default: - // Some other, unexpected error was returned. Emit on the session. - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, session, 'error', err); + if (typeof ret === 'number') { + let err; + let target = session; + switch (ret) { + case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: + err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS'); + target = this; break; - } - debug(`[${sessionName(session[kType])}] stream ${ret} initialized`); - this[kInit](ret); - streams.set(ret, this); + case NGHTTP2_ERR_INVALID_ARGUMENT: + err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY'); + target = this; + break; + default: + err = new NghttpError(ret); + } + process.nextTick(emit, target, 'error', err); + return; } + const id = ret.id(); + streams.set(id, this); + this[kInit](id, ret); } function validatePriorityOptions(options) { @@ -502,6 +468,12 @@ function validatePriorityOptions(options) { } } +function onSessionInternalError(code) { + const owner = this[kOwner]; + const err = new NghttpError(code); + process.nextTick(emit, owner, 'error', err); +} + // Creates the internal binding.Http2Session handle for an Http2Session // instance. This occurs only after the socket connection has been // established. Note: the binding.Http2Session will take over ownership @@ -514,13 +486,11 @@ function setupHandle(session, socket, type, options) { updateOptionsBuffer(options); const handle = new binding.Http2Session(type); handle[kOwner] = session; + handle.error = onSessionInternalError; handle.onpriority = onPriority; handle.onsettings = onSettings; handle.onheaders = onSessionHeaders; - handle.ontrailers = onSessionTrailers; - handle.onstreamclose = onSessionStreamClose; handle.onerror = onSessionError; - handle.onread = onSessionRead; handle.onframeerror = onFrameError; handle.ongoawaydata = onGoawayData; @@ -548,134 +518,87 @@ function submitSettings(settings) { debug(`[${sessionName(type)}] submitting actual settings`); _unrefActive(this); this[kLocalSettings] = undefined; - updateSettingsBuffer(settings); - const ret = this[kHandle].submitSettings(); - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, this, 'error', err); - break; - default: - // Some other unexpected error was reported. - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, this, 'error', err); - } - } + this[kHandle].settings(); debug(`[${sessionName(type)}] settings complete`); } // Submits a PRIORITY frame to be sent to the remote peer // Note: If the silent option is true, the change will be made // locally with no PRIORITY frame sent. -function submitPriority(stream, options) { - const type = this[kType]; - debug(`[${sessionName(type)}] submitting actual priority`); +function submitPriority(options) { _unrefActive(this); + _unrefActive(this[kSession]); - const ret = this[kHandle].submitPriority(stream[kID], - options.parent | 0, - options.weight | 0, - !!options.exclusive, - !!options.silent); - - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, this, 'error', err); - break; - default: - // Some other unexpected error was reported. - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, stream, 'error', err); - } - } - debug(`[${sessionName(type)}] priority complete`); + this[kHandle].priority(options.parent | 0, + options.weight | 0, + !!options.exclusive, + !!options.silent); } // Submit an RST-STREAM frame to be sent to the remote peer. // This will cause the Http2Stream to be closed. -function submitRstStream(stream, code) { - const type = this[kType]; - debug(`[${sessionName(type)}] submit actual rststream`); +function submitRstStream(code) { _unrefActive(this); - const ret = this[kHandle].submitRstStream(stream[kID], code); - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, this, 'error', err); - break; - default: - // Some other unexpected error was reported. - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, stream, 'error', err); - break; - } - stream.destroy(); + _unrefActive(this[kSession]); + const ret = this[kHandle].rstStream(code); + if (ret < 0) { + const err = new NghttpError(ret); + process.nextTick(emit, this, 'error', err); + return; } - debug(`[${sessionName(type)}] rststream complete`); + this.destroy(); } -function doShutdown(self, options) { - const handle = self[kHandle]; - const state = self[kState]; +function doShutdown(options) { + const handle = this[kHandle]; + const state = this[kState]; if (handle === undefined || state.shutdown) return; // Nothing to do, possibly because the session shutdown already. - const ret = handle.submitGoaway(options.errorCode | 0, - options.lastStreamID | 0, - options.opaqueData); + const ret = handle.goaway(options.errorCode | 0, + options.lastStreamID | 0, + options.opaqueData); state.shuttingDown = false; state.shutdown = true; if (ret < 0) { - debug(`[${sessionName(self[kType])}] shutdown failed! code: ${ret}`); + debug(`[${sessionName(this[kType])}] shutdown failed! code: ${ret}`); const err = new NghttpError(ret); - process.nextTick(emit, self, 'error', err); + process.nextTick(emit, this, 'error', err); return; } - process.nextTick(emit, self, 'shutdown', options); - debug(`[${sessionName(self[kType])}] shutdown is complete`); + process.nextTick(emit, this, 'shutdown', options); + debug(`[${sessionName(this[kType])}] shutdown is complete`); } // Submit a graceful or immediate shutdown request for the Http2Session. function submitShutdown(options) { const type = this[kType]; debug(`[${sessionName(type)}] submitting actual shutdown request`); - if (type === NGHTTP2_SESSION_SERVER && - options.graceful === true) { + if (type === NGHTTP2_SESSION_SERVER && options.graceful === true) { // first send a shutdown notice - this[kHandle].submitShutdownNotice(); + this[kHandle].shutdownNotice(); // then, on flip of the event loop, do the actual shutdown - setImmediate(doShutdown, this, options); + setImmediate(doShutdown.bind(this), options); } else { - doShutdown(this, options); + doShutdown.call(this, options); } } -function finishSessionDestroy(self, socket) { - const state = self[kState]; - +function finishSessionDestroy(socket) { if (!socket.destroyed) socket.destroy(); + const state = this[kState]; state.destroying = false; state.destroyed = true; // Destroy the handle - const handle = self[kHandle]; - if (handle !== undefined) { - handle.destroy(state.skipUnconsume); - debug(`[${sessionName(self[kType])}] nghttp2session handle destroyed`); + if (this[kHandle] !== undefined) { + this[kHandle].destroy(state.skipUnconsume); + this[kHandle] = undefined; } - self[kHandle] = undefined; - process.nextTick(emit, self, 'close'); - debug(`[${sessionName(self[kType])}] nghttp2session destroyed`); + process.nextTick(emit, this, 'close'); } const proxySocketHandler = { @@ -720,13 +643,16 @@ const proxySocketHandler = { } }; +function pingCallback(cb) { + return function(ack, duration, payload) { + const err = ack ? null : new errors.Error('ERR_HTTP2_PING_CANCEL'); + cb(err, duration, payload); + }; +} + // Upon creation, the Http2Session takes ownership of the socket. The session // may not be ready to use immediately if the socket is not yet fully connected. class Http2Session extends EventEmitter { - - // type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT - // options { Object } - // socket { net.Socket | tls.TLSSocket | stream.Duplex } constructor(type, options, socket) { super(); @@ -785,10 +711,39 @@ class Http2Session extends EventEmitter { // to something more reasonable because we may have any number // of concurrent streams (2^31-1 is the upper limit on the number // of streams) - this.setMaxListeners((2 ** 31) - 1); + this.setMaxListeners(kMaxStreams); debug(`[${sessionName(type)}] http2session created`); } + setNextStreamID(id) { + if (typeof id !== 'number') + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'id', 'number'); + if (id <= 0 || id > kMaxStreams) + throw new errors.RangeError('ERR_OUT_OF_RANGE'); + this[kHandle].setNextStreamID(id); + } + + ping(payload, callback) { + const state = this[kState]; + if (state.destroyed || state.destroying) + throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); + if (typeof payload === 'function') { + callback = payload; + payload = undefined; + } + if (payload && !isArrayBufferView(payload)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'payload', + ['Buffer', 'TypedArray', 'DataView']); + } + if (payload && payload.length !== 8) { + throw new errors.RangeError('ERR_HTTP2_PING_LENGTH'); + } + if (typeof callback !== 'function') + throw new errors.TypeError('ERR_INVALID_CALLBACK'); + return this[kHandle].ping(payload, pingCallback(callback)); + } + [kInspect](depth, opts) { const state = this[kState]; const obj = { @@ -830,9 +785,7 @@ class Http2Session extends EventEmitter { // Retrieves state information for the Http2Session get state() { const handle = this[kHandle]; - return handle !== undefined ? - getSessionState(handle) : - Object.create(null); + return handle === undefined ? {} : getSessionState(handle); } // The settings currently in effect for the local peer. These will @@ -844,7 +797,7 @@ class Http2Session extends EventEmitter { const handle = this[kHandle]; if (handle === undefined) - return Object.create(null); + return {}; settings = getSettings(handle, false); // Local this[kLocalSettings] = settings; @@ -859,7 +812,7 @@ class Http2Session extends EventEmitter { const handle = this[kHandle]; if (handle === undefined) - return Object.create(null); + return {}; settings = getSettings(handle, true); // Remote this[kRemoteSettings] = settings; @@ -886,7 +839,7 @@ class Http2Session extends EventEmitter { 16384, 2 ** 24 - 1); assertWithinRange('maxConcurrentStreams', settings.maxConcurrentStreams, - 0, 2 ** 31 - 1); + 0, kMaxStreams); assertWithinRange('maxHeaderListSize', settings.maxHeaderListSize, 0, 2 ** 32 - 1); @@ -913,83 +866,6 @@ class Http2Session extends EventEmitter { submitSettings.call(this, settings); } - // Submits a PRIORITY frame to be sent to the remote peer. - priority(stream, options) { - const state = this[kState]; - if (state.destroyed || state.destroying) - throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); - - if (!(stream instanceof Http2Stream)) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'stream', - 'Http2Stream'); - } - assertIsObject(options, 'options'); - options = Object.assign(Object.create(null), options); - validatePriorityOptions(options); - - const id = stream[kID]; - debug(`[${sessionName(this[kType])}] sending priority for stream ` + - `${id}`); - - // A stream cannot be made to depend on itself - if (options.parent === id) { - throw new errors.TypeError('ERR_INVALID_OPT_VALUE', - 'parent', - options.parent); - } - - if (id === undefined) { - debug(`[${sessionName(this[kType])}] session still connecting. queue ` + - 'priority'); - stream.once('ready', submitPriority.bind(this, stream, options)); - return; - } - submitPriority.call(this, stream, options); - } - - // Submits an RST-STREAM frame to be sent to the remote peer. This will - // cause the stream to be closed. - rstStream(stream, code = NGHTTP2_NO_ERROR) { - // Do not check destroying here, as the rstStream may be sent while - // the session is in the middle of being destroyed. - if (this[kState].destroyed) - throw new errors.Error('ERR_HTTP2_INVALID_SESSION'); - - if (!(stream instanceof Http2Stream)) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'stream', - 'Http2Stream'); - } - - if (typeof code !== 'number') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'code', - 'number'); - } - - const state = stream[kState]; - if (state.rst) { - // rst has already been called by self or peer, - // do not call again - return; - } - state.rst = true; - state.rstCode = code; - - const id = stream[kID]; - debug(`[${sessionName(this[kType])}] initiating rststream for stream ` + - `${id}: ${code}`); - - if (id === undefined) { - debug(`[${sessionName(this[kType])}] session still connecting, queue ` + - 'rststream'); - stream.once('ready', submitRstStream.bind(this, stream, code)); - return; - } - submitRstStream.call(this, stream, code); - } - // Destroy the Http2Session destroy() { const state = this[kState]; @@ -1016,7 +892,7 @@ class Http2Session extends EventEmitter { if (this[kHandle] !== undefined) this[kHandle].destroying(); - setImmediate(finishSessionDestroy, this, socket); + setImmediate(finishSessionDestroy.bind(this), socket); } // Graceful or immediate shutdown of the Http2Session. Graceful shutdown @@ -1040,7 +916,7 @@ class Http2Session extends EventEmitter { options = Object.assign(Object.create(null), options); if (options.opaqueData !== undefined && - !isUint8Array(options.opaqueData)) { + !isArrayBufferView(options.opaqueData)) { throw new errors.TypeError('ERR_INVALID_OPT_VALUE', 'opaqueData', options.opaqueData); @@ -1177,7 +1053,7 @@ class ClientHttp2Session extends Http2Session { options.getTrailers); } - const stream = new ClientHttp2Stream(this, undefined, {}); + const stream = new ClientHttp2Stream(this, undefined, undefined, {}); const onConnect = requestOnConnect.bind(stream, headers, options); @@ -1228,55 +1104,48 @@ function trackWriteState(stream, bytes) { } function afterDoStreamWrite(status, handle, req) { - const session = handle[kOwner]; - _unrefActive(session); + const stream = handle[kOwner]; + const session = stream[kSession]; + + _unrefActive(stream); - const state = session[kState]; const { bytes } = req; - state.writeQueueSize -= bytes; + stream[kState].writeQueueSize -= bytes; - const stream = state.streams.get(req.stream); - if (stream !== undefined) { - _unrefActive(stream); - stream[kState].writeQueueSize -= bytes; + if (session !== undefined) { + _unrefActive(session); + session[kState].writeQueueSize -= bytes; } if (typeof req.callback === 'function') req.callback(); - this.handle = undefined; + req.handle = undefined; } function onHandleFinish() { - const session = this[kSession]; - if (session === undefined) return; if (this[kID] === undefined) { this.once('ready', onHandleFinish); } else { - const handle = session[kHandle]; + const handle = this[kHandle]; if (handle !== undefined) { - // Shutdown on the next tick of the event loop just in case there is - // still data pending in the outbound queue. - assert(handle.shutdownStream(this[kID]) === undefined, - `The stream ${this[kID]} does not exist. Please report this as ` + - 'a bug in Node.js'); + const req = new ShutdownWrap(); + req.oncomplete = () => {}; + req.handle = handle; + handle.shutdown(req); } } } function onSessionClose(hadError, code) { abort(this); - // Close the readable side - this.push(null); - // Close the writable side - this.end(); + this.push(null); // Close the readable side + this.end(); // Close the writable side } function onStreamClosed(code) { abort(this); - // Close the readable side - this.push(null); - // Close the writable side - this.end(); + this.push(null); // Close the readable side + this.end(); // Close the writable side } function streamOnResume() { @@ -1284,27 +1153,15 @@ function streamOnResume() { this.once('ready', streamOnResume); return; } - const session = this[kSession]; - if (session) { - assert(session[kHandle].streamReadStart(this[kID]) === undefined, - `HTTP/2 Stream ${this[kID]} does not exist. Please report this as ` + - 'a bug in Node.js'); - } + this[kHandle].readStart(); } function streamOnPause() { - const session = this[kSession]; - if (session) { - assert(session[kHandle].streamReadStop(this[kID]) === undefined, - `HTTP/2 Stream ${this[kID]} does not exist. Please report this as ' + - 'a bug in Node.js`); - } + this[kHandle].readStop(); } -function handleFlushData(handle, streamID) { - assert(handle.flushData(streamID) === undefined, - `HTTP/2 Stream ${streamID} does not exist. Please report this as ` + - 'a bug in Node.js'); +function handleFlushData(handle) { + handle.flushData(); } function streamOnSessionConnect() { @@ -1329,16 +1186,14 @@ function abort(stream) { } } -// An Http2Stream is a Duplex stream. On the server-side, the Readable side -// provides access to the received request data. On the client-side, the -// Readable side provides access to the received response data. On the -// server side, the writable side is used to transmit response data, while -// on the client side it is used to transmit request data. +// An Http2Stream is a Duplex stream that is backed by a +// node::http2::Http2Stream handle implementing StreamBase. class Http2Stream extends Duplex { constructor(session, options) { options.allowHalfOpen = true; options.decodeStrings = false; super(options); + this[async_id_symbol] = -1; this.cork(); this[kSession] = session; @@ -1368,8 +1223,14 @@ class Http2Stream extends Duplex { debug(`[${sessionName(session[kType])}] http2stream created`); } - [kInit](id) { + [kInit](id, handle) { this[kID] = id; + this[async_id_symbol] = handle.getAsyncId(); + handle[kOwner] = this; + this[kHandle] = handle; + handle.ontrailers = onStreamTrailers; + handle.onstreamclose = onStreamClose; + handle.onread = onStreamRead; this.emit('ready'); } @@ -1434,8 +1295,8 @@ class Http2Stream extends Duplex { get state() { const id = this[kID]; if (this.destroyed || id === undefined) - return Object.create(null); - return getStreamState(this[kSession][kHandle], id); + return {}; + return getStreamState(this[kHandle], id); } [kProceed]() { @@ -1449,12 +1310,14 @@ class Http2Stream extends Duplex { this.once('ready', this._write.bind(this, data, encoding, cb)); return; } + + _unrefActive(this); + _unrefActive(this[kSession]); + if (!this[kState].headersSent) this[kProceed](); - const session = this[kSession]; - _unrefActive(this); - _unrefActive(session); - const handle = session[kHandle]; + + const handle = this[kHandle]; const req = new WriteWrap(); req.stream = this[kID]; req.handle = handle; @@ -1472,12 +1335,14 @@ class Http2Stream extends Duplex { this.once('ready', this._writev.bind(this, data, cb)); return; } + + _unrefActive(this); + _unrefActive(this[kSession]); + if (!this[kState].headersSent) this[kProceed](); - const session = this[kSession]; - _unrefActive(this); - _unrefActive(session); - const handle = session[kHandle]; + + const handle = this[kHandle]; const req = new WriteWrap(); req.stream = this[kID]; req.handle = handle; @@ -1506,7 +1371,7 @@ class Http2Stream extends Duplex { return; } _unrefActive(this); - process.nextTick(handleFlushData, this[kSession][kHandle], this[kID]); + process.nextTick(handleFlushData, this[kHandle]); } // Submits an RST-STREAM frame to shutdown this stream. @@ -1515,19 +1380,32 @@ class Http2Stream extends Duplex { // After sending the rstStream, this.destroy() will be called making // the stream object no longer usable. rstStream(code = NGHTTP2_NO_ERROR) { - if (this.destroyed) - throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); - const session = this[kSession]; + if (typeof code !== 'number') + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number'); + if (this[kID] === undefined) { - debug( - `[${sessionName(session[kType])}] queuing rstStream for new stream`); this.once('ready', this.rstStream.bind(this, code)); return; } - debug(`[${sessionName(session[kType])}] sending rstStream for stream ` + - `${this[kID]}: ${code}`); + + const state = this[kState]; + if (state.rst) { + // rst has already been set by self or peer, do not set again + return; + } + state.rst = true; + state.rstCode = code; + _unrefActive(this); - session.rstStream(this, code); + _unrefActive(this[kSession]); + + const id = this[kID]; + + if (id === undefined) { + this.once('ready', submitRstStream.bind(this, code)); + return; + } + submitRstStream.call(this, code); } rstWithNoError() { @@ -1550,12 +1428,6 @@ class Http2Stream extends Duplex { this.rstStream(NGHTTP2_INTERNAL_ERROR); } - // Note that this (and other methods like additionalHeaders and rstStream) - // cause nghttp to queue frames up in its internal buffer that are not - // actually sent on the wire until the next tick of the event loop. The - // semantics of this method then are: queue a priority frame to be sent and - // not immediately send the priority frame. There is current no callback - // triggered when the data is actually sent. priority(options) { if (this.destroyed) throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); @@ -1568,7 +1440,27 @@ class Http2Stream extends Duplex { debug(`[${sessionName(session[kType])}] sending priority for stream ` + `${this[kID]}`); _unrefActive(this); - session.priority(this, options); + + assertIsObject(options, 'options'); + options = Object.assign(Object.create(null), options); + validatePriorityOptions(options); + + const id = this[kID]; + debug(`[${sessionName(session[kType])}] sending priority for stream ` + + `${id}`); + + // A stream cannot be made to depend on itself + if (options.parent === id) { + throw new errors.TypeError('ERR_INVALID_OPT_VALUE', + 'parent', + options.parent); + } + + if (id === undefined) { + this.once('ready', submitPriority.bind(this, options)); + return; + } + submitPriority.call(this, options); } // Called by this.destroy(). @@ -1594,15 +1486,15 @@ class Http2Stream extends Duplex { server.emit('streamError', err, this); } - process.nextTick(continueStreamDestroy, this, err, callback); + process.nextTick(continueStreamDestroy.bind(this), err, callback); } } -function continueStreamDestroy(self, err, callback) { - const session = self[kSession]; - const state = self[kState]; +function continueStreamDestroy(err, callback) { + const session = this[kSession]; + const state = this[kState]; - debug(`[${sessionName(session[kType])}] destroying stream ${self[kID]}`); + debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`); // Submit RST-STREAM frame if one hasn't been sent already and the // stream hasn't closed normally... @@ -1610,16 +1502,16 @@ function continueStreamDestroy(self, err, callback) { let code = state.rstCode; if (!rst && !session.destroyed) { code = err instanceof Error ? NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR; - session.rstStream(self, code); + this.rstStream(code); } // Remove the close handler on the session session.removeListener('close', state.closeHandler); // Unenroll the timer - self.setTimeout(0); + this.setTimeout(0); - setImmediate(finishStreamDestroy, self, session[kHandle]); + setImmediate(finishStreamDestroy.bind(this)); // RST code 8 not emitted as an error as its used by clients to signify // abort and is already covered by aborted event, also allows more @@ -1628,17 +1520,20 @@ function continueStreamDestroy(self, err, callback) { err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code); } callback(err); - process.nextTick(emit, self, 'streamClosed', code); - debug(`[${sessionName(session[kType])}] stream ${self[kID]} destroyed`); + process.nextTick(emit, this, 'streamClosed', code); + debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`); } -function finishStreamDestroy(self, handle) { - const id = self[kID]; - self[kSession][kState].streams.delete(id); - delete self[kSession]; - if (handle !== undefined) - handle.destroyStream(id); - self.emit('destroy'); +function finishStreamDestroy() { + const id = this[kID]; + this[kSession][kState].streams.delete(id); + this[kSession] = undefined; + const handle = this[kHandle]; + if (handle !== undefined) { + this[kHandle] = undefined; + handle.destroy(); + } + this.emit('destroy'); } function processHeaders(headers) { @@ -1664,32 +1559,25 @@ function processHeaders(headers) { function processRespondWithFD(fd, headers, offset = 0, length = -1, streamOptions = 0) { - const session = this[kSession]; const state = this[kState]; state.headersSent = true; // Close the writable side of the stream this.end(); - const ret = session[kHandle].submitFile(this[kID], fd, headers, - offset, length, streamOptions); - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, session, 'error', err); - break; - default: - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, this, 'error', err); - break; - } - // exact length of the file doesn't matter here, since the - // stream is closing anyway — just use 1 to signify that - // a write does exist - trackWriteState(this, 1); + const ret = this[kHandle].respondFD(fd, headers, + offset, length, + streamOptions); + + if (ret < 0) { + const err = new NghttpError(ret); + process.nextTick(emit, this, 'error', err); + return; } + // exact length of the file doesn't matter here, since the + // stream is closing anyway — just use 1 to signify that + // a write does exist + trackWriteState(this, 1); } function doSendFD(session, options, fd, headers, streamOptions, err, stat) { @@ -1813,9 +1701,9 @@ function streamOnError(err) { class ServerHttp2Stream extends Http2Stream { - constructor(session, id, options, headers) { + constructor(session, handle, id, options, headers) { super(session, options); - this[kInit](id); + this[kInit](id, handle); this[kProtocol] = headers[HTTP2_HEADER_SCHEME]; this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY]; this.on('error', streamOnError); @@ -1839,6 +1727,9 @@ class ServerHttp2Stream extends Http2Stream { throw new errors.Error('ERR_HTTP2_INVALID_STREAM'); const session = this[kSession]; + if (!session.remoteSettings.enablePush) + throw new errors.Error('ERR_HTTP2_PUSH_DISABLED'); + debug(`[${sessionName(session[kType])}] initiating push stream for stream` + ` ${this[kID]}`); @@ -1846,9 +1737,6 @@ class ServerHttp2Stream extends Http2Stream { const state = session[kState]; const streams = state.streams; - if (!session.remoteSettings.enablePush) - throw new errors.Error('ERR_HTTP2_PUSH_DISABLED'); - if (typeof options === 'function') { callback = options; options = undefined; @@ -1887,45 +1775,40 @@ class ServerHttp2Stream extends Http2Stream { const streamOptions = options.endStream ? STREAM_OPTION_EMPTY_PAYLOAD : 0; - const ret = session[kHandle].submitPushPromise(this[kID], - headersList, - streamOptions); + const ret = this[kHandle].pushPromise(headersList, streamOptions); let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, session, 'error', err); - break; - case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: - err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS'); - process.nextTick(emit, session, 'error', err); - break; - case NGHTTP2_ERR_STREAM_CLOSED: - err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); - process.nextTick(emit, this, 'error', err); - break; - default: - if (ret <= 0) { + if (typeof ret === 'number') { + switch (ret) { + case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: + err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS'); + break; + case NGHTTP2_ERR_STREAM_CLOSED: + err = new errors.Error('ERR_HTTP2_STREAM_CLOSED'); + break; + default: err = new NghttpError(ret); - process.nextTick(emit, this, 'error', err); break; - } - debug(`[${sessionName(session[kType])}] push stream ${ret} created`); - options.readable = !options.endStream; + } + process.nextTick(emit, this, 'error', err); + return; + } - const stream = new ServerHttp2Stream(session, ret, options, headers); + const id = ret.id(); + debug(`[${sessionName(session[kType])}] push stream ${id} created`); + options.readable = !options.endStream; - // If the push stream is a head request, close the writable side of - // the stream immediately as there won't be any data sent. - if (headRequest) { - stream.end(); - const state = stream[kState]; - state.headRequest = true; - } + const stream = new ServerHttp2Stream(session, ret, id, options, headers); + streams.set(id, stream); - streams.set(ret, stream); - process.nextTick(callback, stream, headers, 0); + // If the push stream is a head request, close the writable side of + // the stream immediately as there won't be any data sent. + if (headRequest) { + stream.end(); + const state = stream[kState]; + state.headRequest = true; } + + process.nextTick(callback, stream, headers, 0); } // Initiate a response on this Http2Stream @@ -1983,20 +1866,10 @@ class ServerHttp2Stream extends Http2Stream { if (options.endStream) this.end(); - const ret = session[kHandle].submitResponse(this[kID], - headersList, - streamOptions); - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, session, 'error', err); - break; - default: - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, this, 'error', err); - } + const ret = this[kHandle].respond(headersList, streamOptions); + if (ret < 0) { + const err = new NghttpError(ret); + process.nextTick(emit, this, 'error', err); } } @@ -2182,18 +2055,10 @@ class ServerHttp2Stream extends Http2Stream { throw headersList; } - const ret = session[kHandle].sendHeaders(this[kID], headersList); - let err; - switch (ret) { - case NGHTTP2_ERR_NOMEM: - err = new errors.Error('ERR_OUTOFMEMORY'); - process.nextTick(emit, session, 'error', err); - break; - default: - if (ret < 0) { - err = new NghttpError(ret); - process.nextTick(emit, this, 'error', err); - } + const ret = this[kHandle].info(headersList); + if (ret < 0) { + const err = new NghttpError(ret); + process.nextTick(emit, this, 'error', err); } } } @@ -2201,11 +2066,11 @@ class ServerHttp2Stream extends Http2Stream { ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond; class ClientHttp2Stream extends Http2Stream { - constructor(session, id, options) { + constructor(session, handle, id, options) { super(session, options); this[kState].headersSent = true; if (id !== undefined) - this[kInit](id); + this[kInit](id, handle); this.on('headers', handleHeaderContinue); debug(`[${sessionName(session[kType])}] clienthttp2stream created`); } @@ -2452,7 +2317,7 @@ function setupCompat(ev) { } } -function socketOnClose(hadError) { +function socketOnClose() { const session = this[kSession]; if (session !== undefined && !session.destroyed) { // Skip unconsume because the socket is destroyed. @@ -2464,7 +2329,7 @@ function socketOnClose(hadError) { // If the session emits an error, forward it to the socket as a sessionError; // failing that, destroy the session, remove the listener and re-emit the error function clientSessionOnError(error) { - debug(`client session error: ${error.message}`); + debug(`[${sessionName(this[kType])}] session error: ${error.message}`); if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error)) return; this.destroy(); @@ -2571,7 +2436,7 @@ function getPackedSettings(settings) { 16384, 2 ** 24 - 1); assertWithinRange('maxConcurrentStreams', settings.maxConcurrentStreams, - 0, 2 ** 31 - 1); + 0, kMaxStreams); assertWithinRange('maxHeaderListSize', settings.maxHeaderListSize, 0, 2 ** 32 - 1); @@ -2587,9 +2452,9 @@ function getPackedSettings(settings) { } function getUnpackedSettings(buf, options = {}) { - if (!isUint8Array(buf)) { + if (!isArrayBufferView(buf)) { throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buf', - ['Buffer', 'Uint8Array']); + ['Buffer', 'TypedArray', 'DataView']); } if (buf.length % 6 !== 0) throw new errors.RangeError('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH'); @@ -2637,7 +2502,7 @@ function getUnpackedSettings(buf, options = {}) { 16384, 2 ** 24 - 1); assertWithinRange('maxConcurrentStreams', settings.maxConcurrentStreams, - 0, 2 ** 31 - 1); + 0, kMaxStreams); assertWithinRange('maxHeaderListSize', settings.maxHeaderListSize, 0, 2 ** 32 - 1); diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index e6da4293b51..1f9853aa1fd 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -173,7 +173,8 @@ const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2; const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3; const IDX_OPTIONS_PADDING_STRATEGY = 4; const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5; -const IDX_OPTIONS_FLAGS = 6; +const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6; +const IDX_OPTIONS_FLAGS = 7; function updateOptionsBuffer(options) { var flags = 0; @@ -207,6 +208,11 @@ function updateOptionsBuffer(options) { optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS] = options.maxHeaderListPairs; } + if (typeof options.maxOutstandingPings === 'number') { + flags |= (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS); + optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] = + options.maxOutstandingPings; + } optionsBuffer[IDX_OPTIONS_FLAGS] = flags; } @@ -259,25 +265,19 @@ function getDefaultSettings() { // remote is a boolean. true to fetch remote settings, false to fetch local. // this is only called internally function getSettings(session, remote) { - const holder = Object.create(null); if (remote) - session.refreshRemoteSettings(); + session.remoteSettings(); else - session.refreshLocalSettings(); - - holder.headerTableSize = - settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; - holder.enablePush = - !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH]; - holder.initialWindowSize = - settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; - holder.maxFrameSize = - settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE]; - holder.maxConcurrentStreams = - settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; - holder.maxHeaderListSize = - settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; - return holder; + session.localSettings(); + + return { + headerTableSize: settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE], + enablePush: !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH], + initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE], + maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE], + maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS], + maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] + }; } function updateSettingsBuffer(settings) { @@ -316,45 +316,39 @@ function updateSettingsBuffer(settings) { } function getSessionState(session) { - const holder = Object.create(null); - binding.refreshSessionState(session); - holder.effectiveLocalWindowSize = - sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE]; - holder.effectiveRecvDataLength = - sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH]; - holder.nextStreamID = - sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID]; - holder.localWindowSize = - sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE]; - holder.lastProcStreamID = - sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID]; - holder.remoteWindowSize = - sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE]; - holder.outboundQueueSize = - sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE]; - holder.deflateDynamicTableSize = - sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE]; - holder.inflateDynamicTableSize = - sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE]; - return holder; + session.refreshState(); + return { + effectiveLocalWindowSize: + sessionState[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE], + effectiveRecvDataLength: + sessionState[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH], + nextStreamID: + sessionState[IDX_SESSION_STATE_NEXT_STREAM_ID], + localWindowSize: + sessionState[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE], + lastProcStreamID: + sessionState[IDX_SESSION_STATE_LAST_PROC_STREAM_ID], + remoteWindowSize: + sessionState[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE], + outboundQueueSize: + sessionState[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE], + deflateDynamicTableSize: + sessionState[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE], + inflateDynamicTableSize: + sessionState[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE] + }; } -function getStreamState(session, stream) { - const holder = Object.create(null); - binding.refreshStreamState(session, stream); - holder.state = - streamState[IDX_STREAM_STATE]; - holder.weight = - streamState[IDX_STREAM_STATE_WEIGHT]; - holder.sumDependencyWeight = - streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT]; - holder.localClose = - streamState[IDX_STREAM_STATE_LOCAL_CLOSE]; - holder.remoteClose = - streamState[IDX_STREAM_STATE_REMOTE_CLOSE]; - holder.localWindowSize = - streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE]; - return holder; +function getStreamState(stream) { + stream.refreshState(); + return { + state: streamState[IDX_STREAM_STATE], + weight: streamState[IDX_STREAM_STATE_WEIGHT], + sumDependencyWeight: streamState[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT], + localClose: streamState[IDX_STREAM_STATE_LOCAL_CLOSE], + remoteClose: streamState[IDX_STREAM_STATE_REMOTE_CLOSE], + localWindowSize: streamState[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] + }; } function isIllegalConnectionSpecificHeader(name, value) { diff --git a/lib/internal/inspector_async_hook.js b/lib/internal/inspector_async_hook.js index 4ccf8626977..b190ff90b8e 100644 --- a/lib/internal/inspector_async_hook.js +++ b/lib/internal/inspector_async_hook.js @@ -16,22 +16,33 @@ const hook = createHook({ // in https://github.com/nodejs/node/pull/13870#discussion_r124515293, // this should be fine as long as we call asyncTaskCanceled() too. const recurring = true; - inspector.asyncTaskScheduled(type, asyncId, recurring); + if (type === 'PROMISE') + this.promiseIds.add(asyncId); + else + inspector.asyncTaskScheduled(type, asyncId, recurring); }, before(asyncId) { + if (this.promiseIds.has(asyncId)) + return; inspector.asyncTaskStarted(asyncId); }, after(asyncId) { + if (this.promiseIds.has(asyncId)) + return; inspector.asyncTaskFinished(asyncId); }, destroy(asyncId) { + if (this.promiseIds.has(asyncId)) + return this.promiseIds.delete(asyncId); inspector.asyncTaskCanceled(asyncId); }, }); +hook.promiseIds = new Set(); + function enable() { if (config.bits < 64) { // V8 Inspector stores task ids as (void*) pointers. diff --git a/lib/module.js b/lib/module.js index cc8d5097bb8..77e5e3fef66 100644 --- a/lib/module.js +++ b/lib/module.js @@ -521,11 +521,14 @@ Module._resolveFilename = function(request, parent, isMain, options) { if (typeof options === 'object' && options !== null && Array.isArray(options.paths)) { + const fakeParent = new Module('', null); + paths = []; for (var i = 0; i < options.paths.length; i++) { const path = options.paths[i]; - const lookupPaths = Module._resolveLookupPaths(path, parent, true); + fakeParent.paths = Module._nodeModulePaths(path); + const lookupPaths = Module._resolveLookupPaths(request, fakeParent, true); if (!paths.includes(path)) paths.push(path); diff --git a/node.gyp b/node.gyp index d144f850f89..4d4fcb48a35 100644 --- a/node.gyp +++ b/node.gyp @@ -16,6 +16,7 @@ 'node_shared_http_parser%': 'false', 'node_shared_cares%': 'false', 'node_shared_libuv%': 'false', + 'node_shared_nghttp2%': 'false', 'node_use_openssl%': 'true', 'node_shared_openssl%': 'false', 'node_v8_options%': '', @@ -179,7 +180,6 @@ 'dependencies': [ 'node_js2c#host', - 'deps/nghttp2/nghttp2.gyp:nghttp2' ], 'includes': [ @@ -189,8 +189,7 @@ 'include_dirs': [ 'src', 'tools/msvs/genfiles', - '<(SHARED_INTERMEDIATE_DIR)', # for node_natives.h - 'deps/nghttp2/lib/includes' + '<(SHARED_INTERMEDIATE_DIR)' # for node_natives.h ], 'sources': [ @@ -260,8 +259,6 @@ 'src/node.h', 'src/node_api.h', 'src/node_api_types.h', - 'src/node_http2_core.h', - 'src/node_http2_core-inl.h', 'src/node_buffer.h', 'src/node_constants.h', 'src/node_debug_options.h', @@ -989,6 +986,14 @@ 'deps/uv/uv.gyp:libuv' ] }], + [ 'node_shared_nghttp2=="false"', { + 'dependencies': [ + 'deps/nghttp2/nghttp2.gyp:nghttp2' + ], + 'include_dirs': [ + 'deps/nghttp2/lib/includes' + ] + }], [ 'node_use_v8_platform=="true" and node_engine=="v8"', { 'dependencies': [ 'deps/v8/src/v8.gyp:v8_libplatform', diff --git a/node.gypi b/node.gypi index c7c278b2e0b..796592446d8 100644 --- a/node.gypi +++ b/node.gypi @@ -151,6 +151,10 @@ 'dependencies': [ 'deps/uv/uv.gyp:libuv' ], }], + [ 'node_shared_nghttp2=="false"', { + 'dependencies': [ 'deps/nghttp2/nghttp2.gyp:nghttp2' ], + }], + [ 'OS=="mac"', { # linking Corefoundation is needed since certain OSX debugging tools # like Instruments require it for some features diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 7410557d2eb..b1a8f689ab3 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -137,11 +137,7 @@ RetainedObjectInfo* WrapperInfo(uint16_t class_id, Local wrapper) { // end RetainedAsyncInfo -static void DestroyAsyncIdsCallback(uv_timer_t* handle) { - Environment* env = Environment::from_destroy_async_ids_timer_handle(handle); - - HandleScope handle_scope(env->isolate()); - Context::Scope context_scope(env->context()); +static void DestroyAsyncIdsCallback(Environment* env, void* data) { Local fn = env->async_hooks_destroy_function(); TryCatch try_catch(env->isolate()); @@ -689,8 +685,7 @@ void AsyncWrap::EmitDestroy(Environment* env, double async_id) { return; if (env->destroy_async_id_list()->empty()) { - uv_timer_start(env->destroy_async_ids_timer_handle(), - DestroyAsyncIdsCallback, 0, 0); + env->SetImmediate(DestroyAsyncIdsCallback, nullptr); } env->destroy_async_id_list()->push_back(async_id); diff --git a/src/async_wrap.h b/src/async_wrap.h index f58f9443272..98451ead3bc 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -41,7 +41,8 @@ namespace node { V(GETADDRINFOREQWRAP) \ V(GETNAMEINFOREQWRAP) \ V(HTTP2SESSION) \ - V(HTTP2SESSIONSHUTDOWNWRAP) \ + V(HTTP2STREAM) \ + V(HTTP2PING) \ V(HTTPPARSER) \ V(JSSTREAM) \ V(PIPECONNECTWRAP) \ diff --git a/src/env-inl.h b/src/env-inl.h index f7bf57ac53c..a69703fced1 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -345,15 +345,6 @@ inline uv_idle_t* Environment::immediate_idle_handle() { return &immediate_idle_handle_; } -inline Environment* Environment::from_destroy_async_ids_timer_handle( - uv_timer_t* handle) { - return ContainerOf(&Environment::destroy_async_ids_timer_handle_, handle); -} - -inline uv_timer_t* Environment::destroy_async_ids_timer_handle() { - return &destroy_async_ids_timer_handle_; -} - inline void Environment::RegisterHandleCleanup(uv_handle_t* handle, HandleCleanupCb cb, void *arg) { @@ -494,6 +485,13 @@ Environment::scheduled_immediate_count() { return scheduled_immediate_count_; } +void Environment::SetImmediate(native_immediate_callback cb, void* data) { + native_immediate_callbacks_.push_back({ cb, data }); + if (scheduled_immediate_count_[0] == 0) + ActivateImmediateCheck(); + scheduled_immediate_count_[0] = scheduled_immediate_count_[0] + 1; +} + inline performance::performance_state* Environment::performance_state() { return performance_state_; } diff --git a/src/env.cc b/src/env.cc index d4ca34aa74b..82bdb4f900d 100644 --- a/src/env.cc +++ b/src/env.cc @@ -4,12 +4,6 @@ #include "node_buffer.h" #include "node_platform.h" -#if defined(_MSC_VER) -#define getpid GetCurrentProcessId -#else -#include -#endif - #include #include @@ -102,8 +96,6 @@ void Environment::Start(int argc, uv_unref(reinterpret_cast(&idle_prepare_handle_)); uv_unref(reinterpret_cast(&idle_check_handle_)); - uv_timer_init(event_loop(), destroy_async_ids_timer_handle()); - auto close_and_finish = [](Environment* env, uv_handle_t* handle, void* arg) { handle->data = env; @@ -128,10 +120,6 @@ void Environment::Start(int argc, reinterpret_cast(&idle_check_handle_), close_and_finish, nullptr); - RegisterHandleCleanup( - reinterpret_cast(&destroy_async_ids_timer_handle_), - close_and_finish, - nullptr); if (start_profiler_idle_notifier) { StartProfilerIdleNotifier(); @@ -184,7 +172,8 @@ void Environment::PrintSyncTrace() const { Local stack = StackTrace::CurrentStackTrace(isolate(), 10, StackTrace::kDetailed); - fprintf(stderr, "(node:%d) WARNING: Detected use of sync API\n", getpid()); + fprintf(stderr, "(node:%u) WARNING: Detected use of sync API\n", + GetProcessId()); for (int i = 0; i < stack->GetFrameCount() - 1; i++) { Local stack_frame = stack->GetFrame(i); @@ -282,6 +271,59 @@ void Environment::EnvPromiseHook(v8::PromiseHookType type, } } +void Environment::RunAndClearNativeImmediates() { + size_t count = native_immediate_callbacks_.size(); + if (count > 0) { + std::vector list; + native_immediate_callbacks_.swap(list); + for (const auto& cb : list) { + cb.cb_(this, cb.data_); + } + +#ifdef DEBUG + CHECK_GE(scheduled_immediate_count_[0], count); +#endif + scheduled_immediate_count_[0] = scheduled_immediate_count_[0] - count; + } +} + +static bool MaybeStopImmediate(Environment* env) { + if (env->scheduled_immediate_count()[0] == 0) { + uv_check_stop(env->immediate_check_handle()); + uv_idle_stop(env->immediate_idle_handle()); + return true; + } + return false; +} + + +void Environment::CheckImmediate(uv_check_t* handle) { + Environment* env = Environment::from_immediate_check_handle(handle); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + if (MaybeStopImmediate(env)) + return; + + env->RunAndClearNativeImmediates(); + + MakeCallback(env->isolate(), + env->process_object(), + env->immediate_callback_string(), + 0, + nullptr, + {0, 0}).ToLocalChecked(); + + MaybeStopImmediate(env); +} + +void Environment::ActivateImmediateCheck() { + uv_check_start(&immediate_check_handle_, CheckImmediate); + // Idle handle is needed only to stop the event loop from blocking in poll. + uv_idle_start(&immediate_idle_handle_, [](uv_idle_t*){ }); +} + + void CollectExceptionInfo(Environment* env, v8::Local obj, int errorno, diff --git a/src/env.h b/src/env.h index 69cb030783d..6cae5beaeb5 100644 --- a/src/env.h +++ b/src/env.h @@ -310,6 +310,8 @@ class ModuleWrap; V(buffer_prototype_object, v8::Object) \ V(context, v8::Context) \ V(domains_stack_array, v8::Array) \ + V(http2ping_constructor_template, v8::ObjectTemplate) \ + V(http2stream_constructor_template, v8::ObjectTemplate) \ V(inspector_console_api_object, v8::Object) \ V(module_load_list_array, v8::Array) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ @@ -543,11 +545,8 @@ class Environment { inline uint32_t watched_providers() const; static inline Environment* from_immediate_check_handle(uv_check_t* handle); - static inline Environment* from_destroy_async_ids_timer_handle( - uv_timer_t* handle); inline uv_check_t* immediate_check_handle(); inline uv_idle_t* immediate_idle_handle(); - inline uv_timer_t* destroy_async_ids_timer_handle(); // Register clean-up cb to be called on environment destruction. inline void RegisterHandleCleanup(uv_handle_t* handle, @@ -687,6 +686,11 @@ class Environment { bool RemovePromiseHook(promise_hook_func fn, void* arg); bool EmitNapiWarning(); + typedef void (*native_immediate_callback)(Environment* env, void* data); + inline void SetImmediate(native_immediate_callback cb, void* data); + // This needs to be available for the JS-land setImmediate(). + void ActivateImmediateCheck(); + private: inline void ThrowError(v8::Local (*fun)(v8::Local), const char* errmsg); @@ -695,7 +699,6 @@ class Environment { IsolateData* const isolate_data_; uv_check_t immediate_check_handle_; uv_idle_t immediate_idle_handle_; - uv_timer_t destroy_async_ids_timer_handle_; uv_prepare_t idle_prepare_handle_; uv_check_t idle_check_handle_; @@ -747,6 +750,14 @@ class Environment { }; std::vector promise_hooks_; + struct NativeImmediateCallback { + native_immediate_callback cb_; + void* data_; + }; + std::vector native_immediate_callbacks_; + void RunAndClearNativeImmediates(); + static void CheckImmediate(uv_check_t* handle); + static void EnvPromiseHook(v8::PromiseHookType type, v8::Local promise, v8::Local parent); diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 215e91873a3..875c12efa02 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -13,8 +13,8 @@ #include #ifdef __POSIX__ -#include -#include // setuid, getuid +#include // PTHREAD_STACK_MIN +#include #endif // __POSIX__ namespace node { @@ -108,7 +108,8 @@ static int StartDebugSignalHandler() { CHECK_EQ(0, pthread_sigmask(SIG_SETMASK, &sigmask, nullptr)); CHECK_EQ(0, pthread_attr_destroy(&attr)); if (err != 0) { - fprintf(stderr, "node[%d]: pthread_create: %s\n", getpid(), strerror(err)); + fprintf(stderr, "node[%u]: pthread_create: %s\n", + GetProcessId(), strerror(err)); fflush(stderr); // Leave SIGUSR1 blocked. We don't install a signal handler, // receiving the signal would terminate the process. @@ -301,7 +302,8 @@ class NodeInspectorClient : public V8InspectorClient { : env_(env), platform_(platform), terminated_(false), running_nested_loop_(false) { client_ = V8Inspector::create(env->isolate(), this); - contextCreated(env->context(), "Node.js Main Context"); + // TODO(bnoordhuis) Make name configurable from src/node.cc. + contextCreated(env->context(), GetHumanReadableProcessName()); } void runMessageLoopOnPause(int context_group_id) override { diff --git a/src/inspector_io.cc b/src/inspector_io.cc index a870c0a2634..538cbab3c9f 100644 --- a/src/inspector_io.cc +++ b/src/inspector_io.cc @@ -26,17 +26,6 @@ using v8_inspector::StringView; template using TransportAndIo = std::pair; -std::string GetProcessTitle() { - char title[2048]; - int err = uv_get_process_title(title, sizeof(title)); - if (err == 0) { - return title; - } else { - // Title is too long, or could not be retrieved. - return "Node.js"; - } -} - std::string ScriptPath(uv_loop_t* loop, const std::string& script_name) { std::string script_path; @@ -484,7 +473,7 @@ std::vector InspectorIoDelegate::GetTargetIds() { } std::string InspectorIoDelegate::GetTargetTitle(const std::string& id) { - return script_name_.empty() ? GetProcessTitle() : script_name_; + return script_name_.empty() ? GetHumanReadableProcessName() : script_name_; } std::string InspectorIoDelegate::GetTargetUrl(const std::string& id) { diff --git a/src/node.cc b/src/node.cc index 6d478b2ebde..e169de78379 100644 --- a/src/node.cc +++ b/src/node.cc @@ -67,7 +67,6 @@ #if NODE_USE_V8_PLATFORM #include "libplatform/libplatform.h" #endif // NODE_USE_V8_PLATFORM -#include "v8-debug.h" #include "v8-profiler.h" #include "zlib.h" @@ -99,7 +98,6 @@ #if defined(_MSC_VER) #include #include -#define getpid GetCurrentProcessId #define umask _umask typedef int mode_t; #else @@ -899,7 +897,6 @@ void SetupDomainUse(const FunctionCallbackInfo& args) { env->set_using_domains(true); HandleScope scope(env->isolate()); - Local process_object = env->process_object(); CHECK(args[0]->IsArray()); env->set_domains_stack_array(args[0].As()); @@ -1664,19 +1661,11 @@ NO_RETURN void Assert(const char* const (*args)[4]) { auto message = (*args)[2]; auto function = (*args)[3]; - char exepath[256]; - size_t exepath_size = sizeof(exepath); - if (uv_exepath(exepath, &exepath_size)) - snprintf(exepath, sizeof(exepath), "node"); - - char pid[12] = {0}; -#ifndef _WIN32 - snprintf(pid, sizeof(pid), "[%u]", getpid()); -#endif + char name[1024]; + GetHumanReadableProcessName(&name); - fprintf(stderr, "%s%s: %s:%s:%s%s Assertion `%s' failed.\n", - exepath, pid, filename, linenum, - function, *function ? ":" : "", message); + fprintf(stderr, "%s: %s:%s:%s%s Assertion `%s' failed.\n", + name, filename, linenum, function, *function ? ":" : "", message); fflush(stderr); Abort(); @@ -2973,45 +2962,13 @@ static void DebugPortSetter(Local property, static void DebugProcess(const FunctionCallbackInfo& args); -static void DebugPause(const FunctionCallbackInfo& args); static void DebugEnd(const FunctionCallbackInfo& args); namespace { -bool MaybeStopImmediate(Environment* env) { - if (env->scheduled_immediate_count()[0] == 0) { - uv_check_stop(env->immediate_check_handle()); - uv_idle_stop(env->immediate_idle_handle()); - return true; - } - return false; -} - -void CheckImmediate(uv_check_t* handle) { - Environment* env = Environment::from_immediate_check_handle(handle); - HandleScope scope(env->isolate()); - Context::Scope context_scope(env->context()); - - if (MaybeStopImmediate(env)) - return; - - MakeCallback(env->isolate(), - env->process_object(), - env->immediate_callback_string(), - 0, - nullptr, - {0, 0}).ToLocalChecked(); - - MaybeStopImmediate(env); -} - - void ActivateImmediateCheck(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - uv_check_start(env->immediate_check_handle(), CheckImmediate); - // Idle handle is needed only to stop the event loop from blocking in poll. - uv_idle_start(env->immediate_idle_handle(), - [](uv_idle_t*){ /* do nothing, just keep the loop running */ }); + env->ActivateImmediateCheck(); } @@ -3235,7 +3192,8 @@ void SetupProcessObject(Environment* env, process_env_template->NewInstance(env->context()).ToLocalChecked(); process->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "env"), process_env); - READONLY_PROPERTY(process, "pid", Integer::New(env->isolate(), getpid())); + READONLY_PROPERTY(process, "pid", + Integer::New(env->isolate(), GetProcessId())); READONLY_PROPERTY(process, "features", GetFeatures(env)); CHECK(process->SetAccessor(env->context(), @@ -3409,7 +3367,6 @@ void SetupProcessObject(Environment* env, env->SetMethod(process, "_kill", Kill); env->SetMethod(process, "_debugProcess", DebugProcess); - env->SetMethod(process, "_debugPause", DebugPause); env->SetMethod(process, "_debugEnd", DebugEnd); env->SetMethod(process, "hrtime", Hrtime); @@ -4205,11 +4162,6 @@ static void DebugProcess(const FunctionCallbackInfo& args) { #endif // _WIN32 -static void DebugPause(const FunctionCallbackInfo& args) { - v8::Debug::DebugBreak(args.GetIsolate()); -} - - static void DebugEnd(const FunctionCallbackInfo& args) { #if HAVE_INSPECTOR Environment* env = Environment::GetCurrent(args); @@ -4465,6 +4417,15 @@ void RunAtExit(Environment* env) { } +uv_loop_t* GetCurrentEventLoop(v8::Isolate* isolate) { + HandleScope handle_scope(isolate); + auto context = isolate->GetCurrentContext(); + if (context.IsEmpty()) + return nullptr; + return Environment::GetCurrent(context)->event_loop(); +} + + static uv_key_t thread_local_env; diff --git a/src/node.h b/src/node.h index 3372d2ce58e..b036a77c6e4 100644 --- a/src/node.h +++ b/src/node.h @@ -250,6 +250,10 @@ NODE_EXTERN void EmitBeforeExit(Environment* env); NODE_EXTERN int EmitExit(Environment* env); NODE_EXTERN void RunAtExit(Environment* env); +// This may return nullptr if the current v8::Context is not associated +// with a Node instance. +NODE_EXTERN struct uv_loop_s* GetCurrentEventLoop(v8::Isolate* isolate); + /* Converts a unixtime to V8 Date */ #define NODE_UNIXTIME_V8(t) v8::Date::New(v8::Isolate::GetCurrent(), \ 1000 * static_cast(t)) diff --git a/src/node_api.cc b/src/node_api.cc index 8cc426c0da2..28924d442df 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -26,8 +26,10 @@ static napi_status napi_clear_last_error(napi_env env); struct napi_env__ { - explicit napi_env__(v8::Isolate* _isolate): isolate(_isolate), - last_error() {} + explicit napi_env__(v8::Isolate* _isolate, uv_loop_t* _loop) + : isolate(_isolate), + last_error(), + loop(_loop) {} ~napi_env__() { last_exception.Reset(); wrap_template.Reset(); @@ -41,6 +43,7 @@ struct napi_env__ { v8::Persistent accessor_data_template; napi_extended_error_info last_error; int open_handle_scopes = 0; + uv_loop_t* loop = nullptr; }; #define ENV_OBJECT_TEMPLATE(env, prefix, destination, field_count) \ @@ -769,7 +772,7 @@ napi_env GetEnv(v8::Local context) { if (value->IsExternal()) { result = static_cast(value.As()->Value()); } else { - result = new napi_env__(isolate); + result = new napi_env__(isolate, node::GetCurrentEventLoop(isolate)); auto external = v8::External::New(isolate, result); // We must also stop hard if the result of assigning the env to the global @@ -3399,15 +3402,22 @@ napi_status napi_delete_async_work(napi_env env, napi_async_work work) { return napi_clear_last_error(env); } +napi_status napi_get_uv_event_loop(napi_env env, uv_loop_t** loop) { + CHECK_ENV(env); + CHECK_ARG(env, loop); + *loop = env->loop; + return napi_clear_last_error(env); +} + napi_status napi_queue_async_work(napi_env env, napi_async_work work) { CHECK_ENV(env); CHECK_ARG(env, work); - // Consider: Encapsulate the uv_loop_t into an opaque pointer parameter. - // Currently the environment event loop is the same as the UV default loop. - // Someday (if node ever supports multiple isolates), it may be better to get - // the loop from node::Environment::GetCurrent(env->isolate)->event_loop(); - uv_loop_t* event_loop = uv_default_loop(); + napi_status status; + uv_loop_t* event_loop = nullptr; + status = napi_get_uv_event_loop(env, &event_loop); + if (status != napi_ok) + return napi_set_last_error(env, status); uvimpl::Work* w = reinterpret_cast(work); diff --git a/src/node_api.h b/src/node_api.h index fbfef501436..34a61f8d173 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -14,7 +14,9 @@ #include #include "node_api_types.h" -#define NAPI_VERSION 1 +struct uv_loop_s; // Forward declaration. + +#define NAPI_VERSION 2 #ifdef _WIN32 #ifdef BUILDING_NODE_EXTENSION @@ -583,6 +585,10 @@ NAPI_EXTERN napi_status napi_run_script(napi_env env, napi_value script, napi_value* result); +// Return the current libuv event loop for a given environment +NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env, + struct uv_loop_s** loop); + EXTERN_C_END #endif // SRC_NODE_API_H_ diff --git a/src/node_http2.cc b/src/node_http2.cc index bdf0d31b477..b439ae588a7 100644 --- a/src/node_http2.cc +++ b/src/node_http2.cc @@ -14,6 +14,8 @@ using v8::Context; using v8::Float64Array; using v8::Function; using v8::Integer; +using v8::Number; +using v8::ObjectTemplate; using v8::String; using v8::Uint32; using v8::Uint32Array; @@ -21,10 +23,11 @@ using v8::Undefined; namespace http2 { -Nghttp2Session::Callbacks Nghttp2Session::callback_struct_saved[2] = { +const Http2Session::Callbacks Http2Session::callback_struct_saved[2] = { Callbacks(false), Callbacks(true)}; + Http2Options::Http2Options(Environment* env) { nghttp2_option_new(&options_); @@ -70,8 +73,13 @@ Http2Options::Http2Options(Environment* env) { if (flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)) { SetMaxHeaderPairs(buffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS]); } + + if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)) { + SetMaxOutstandingPings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS]); + } } + Http2Settings::Http2Settings(Environment* env) : env_(env) { entries_.AllocateSufficientStorage(IDX_SETTINGS_COUNT); AliasedBuffer& buffer = @@ -82,7 +90,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_HEADER_TABLE_SIZE]; - DEBUG_HTTP2("Setting header table size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting header table size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE; entries_[n].value = val; n++; @@ -90,7 +98,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) { uint32_t val = buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS]; - DEBUG_HTTP2("Setting max concurrent streams: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting max concurrent streams: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; entries_[n].value = val; n++; @@ -98,7 +106,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_MAX_FRAME_SIZE]; - DEBUG_HTTP2("Setting max frame size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting max frame size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE; entries_[n].value = val; n++; @@ -106,7 +114,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE]; - DEBUG_HTTP2("Setting initial window size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting initial window size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE; entries_[n].value = val; n++; @@ -114,7 +122,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) { uint32_t val = buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE]; - DEBUG_HTTP2("Setting max header list size: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting max header list size: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE; entries_[n].value = val; n++; @@ -122,7 +130,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { if (flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) { uint32_t val = buffer[IDX_SETTINGS_ENABLE_PUSH]; - DEBUG_HTTP2("Setting enable push: %d\n", val); + DEBUG_HTTP2("Http2Settings: setting enable push: %d\n", val); entries_[n].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH; entries_[n].value = val; n++; @@ -131,6 +139,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) { count_ = n; } + inline Local Http2Settings::Pack() { const size_t len = count_ * 6; Local buf = Buffer::New(env_, len).ToLocalChecked(); @@ -144,6 +153,7 @@ inline Local Http2Settings::Pack() { return Undefined(env_->isolate()); } + inline void Http2Settings::Update(Environment* env, Http2Session* session, get_setting fn) { @@ -186,6 +196,7 @@ inline void Http2Settings::RefreshDefaults(Environment* env) { (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE); } + Http2Priority::Http2Priority(Environment* env, Local parent, Local weight, @@ -199,27 +210,162 @@ Http2Priority::Http2Priority(Environment* env, nghttp2_priority_spec_init(&spec, parent_, weight_, exclusive_ ? 1 : 0); } + +inline const char* Http2Session::TypeName() { + switch (session_type_) { + case NGHTTP2_SESSION_SERVER: return "server"; + case NGHTTP2_SESSION_CLIENT: return "client"; + default: + // This should never happen + ABORT(); + } +} + + +Headers::Headers(Isolate* isolate, + Local context, + Local headers) { + Local header_string = headers->Get(context, 0).ToLocalChecked(); + Local header_count = headers->Get(context, 1).ToLocalChecked(); + count_ = header_count.As()->Value(); + int header_string_len = header_string.As()->Length(); + + if (count_ == 0) { + CHECK_EQ(header_string_len, 0); + return; + } + + // Allocate a single buffer with count_ nghttp2_nv structs, followed + // by the raw header data as passed from JS. This looks like: + // | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents | + buf_.AllocateSufficientStorage((alignof(nghttp2_nv) - 1) + + count_ * sizeof(nghttp2_nv) + + header_string_len); + // Make sure the start address is aligned appropriately for an nghttp2_nv*. + char* start = reinterpret_cast( + ROUND_UP(reinterpret_cast(*buf_), alignof(nghttp2_nv))); + char* header_contents = start + (count_ * sizeof(nghttp2_nv)); + nghttp2_nv* const nva = reinterpret_cast(start); + + CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length()); + CHECK_EQ(header_string.As() + ->WriteOneByte(reinterpret_cast(header_contents), + 0, header_string_len, + String::NO_NULL_TERMINATION), + header_string_len); + + size_t n = 0; + char* p; + for (p = header_contents; p < header_contents + header_string_len; n++) { + if (n >= count_) { + // This can happen if a passed header contained a null byte. In that + // case, just provide nghttp2 with an invalid header to make it reject + // the headers list. + static uint8_t zero = '\0'; + nva[0].name = nva[0].value = &zero; + nva[0].namelen = nva[0].valuelen = 1; + count_ = 1; + return; + } + + nva[n].flags = NGHTTP2_NV_FLAG_NONE; + nva[n].name = reinterpret_cast(p); + nva[n].namelen = strlen(p); + p += nva[n].namelen + 1; + nva[n].value = reinterpret_cast(p); + nva[n].valuelen = strlen(p); + p += nva[n].valuelen + 1; + } +} + + +Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { + CHECK_EQ(nghttp2_session_callbacks_new(&callbacks), 0); + nghttp2_session_callbacks_set_on_begin_headers_callback( + callbacks, OnBeginHeadersCallback); + nghttp2_session_callbacks_set_on_header_callback2( + callbacks, OnHeaderCallback); + nghttp2_session_callbacks_set_on_frame_recv_callback( + callbacks, OnFrameReceive); + nghttp2_session_callbacks_set_on_stream_close_callback( + callbacks, OnStreamClose); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback( + callbacks, OnDataChunkReceived); + nghttp2_session_callbacks_set_on_frame_not_send_callback( + callbacks, OnFrameNotSent); + nghttp2_session_callbacks_set_on_invalid_header_callback2( + callbacks, OnInvalidHeader); + nghttp2_session_callbacks_set_error_callback( + callbacks, OnNghttpError); + + if (kHasGetPaddingCallback) { + nghttp2_session_callbacks_set_select_padding_callback( + callbacks, OnSelectPadding); + } +} + + +Http2Session::Callbacks::~Callbacks() { + nghttp2_session_callbacks_del(callbacks); +} + + Http2Session::Http2Session(Environment* env, Local wrap, nghttp2_session_type type) : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION), - StreamBase(env) { + session_type_(type) { MakeWeak(this); Http2Options opts(env); + int32_t maxHeaderPairs = opts.GetMaxHeaderPairs(); + max_header_pairs_ = + type == NGHTTP2_SESSION_SERVER + ? std::max(maxHeaderPairs, 4) // minimum # of request headers + : std::max(maxHeaderPairs, 1); // minimum # of response headers + + max_outstanding_pings_ = opts.GetMaxOutstandingPings(); + padding_strategy_ = opts.GetPaddingStrategy(); - int32_t maxHeaderPairs = opts.GetMaxHeaderPairs(); - maxHeaderPairs = type == NGHTTP2_SESSION_SERVER ? - std::max(maxHeaderPairs, 4) : std::max(maxHeaderPairs, 1); - Init(type, *opts, nullptr, maxHeaderPairs); + bool hasGetPaddingCallback = + padding_strategy_ == PADDING_STRATEGY_MAX || + padding_strategy_ == PADDING_STRATEGY_CALLBACK; - // For every node::Http2Session instance, there is a uv_prepare_t handle - // whose callback is triggered on every tick of the event loop. When - // run, nghttp2 is prompted to send any queued data it may have stored. + nghttp2_session_callbacks* callbacks + = callback_struct_saved[hasGetPaddingCallback ? 1 : 0].callbacks; + + auto fn = type == NGHTTP2_SESSION_SERVER ? + nghttp2_session_server_new2 : + nghttp2_session_client_new2; + + // This should fail only if the system is out of memory, which + // is going to cause lots of other problems anyway, or if any + // of the options are out of acceptable range, which we should + // be catching before it gets this far. Either way, crash if this + // fails. + CHECK_EQ(fn(&session_, callbacks, this, *opts), 0); + + Start(); +} + + +Http2Session::~Http2Session() { + CHECK(persistent().IsEmpty()); + Close(); +} + +// For every node::Http2Session instance, there is a uv_prepare_t handle +// whose callback is triggered on every tick of the event loop. When +// run, nghttp2 is prompted to send any queued data it may have stored. +// TODO(jasnell): Currently, this creates one uv_prepare_t per Http2Session, +// we should investigate to see if it's faster to create a +// single uv_prepare_t for all Http2Sessions, then iterate +// over each. +void Http2Session::Start() { prep_ = new uv_prepare_t(); - uv_prepare_init(env->event_loop(), prep_); + uv_prepare_init(env()->event_loop(), prep_); prep_->data = static_cast(this); uv_prepare_start(prep_, [](uv_prepare_t* t) { Http2Session* session = static_cast(t->data); @@ -233,39 +379,69 @@ Http2Session::Http2Session(Environment* env, }); } -Http2Session::~Http2Session() { - CHECK(persistent().IsEmpty()); - Close(); +// Stop the uv_prep_t from further activity, destroy the handle +void Http2Session::Stop() { + DEBUG_HTTP2SESSION(this, "stopping uv_prep_t handle"); + CHECK_EQ(uv_prepare_stop(prep_), 0); + auto prep_close = [](uv_handle_t* handle) { + delete reinterpret_cast(handle); + }; + uv_close(reinterpret_cast(prep_), prep_close); + prep_ = nullptr; } + void Http2Session::Close() { + DEBUG_HTTP2SESSION(this, "closing session"); if (!object().IsEmpty()) ClearWrap(object()); persistent().Reset(); - this->Nghttp2Session::Close(); - // Stop the loop - CHECK_EQ(uv_prepare_stop(prep_), 0); - auto prep_close = [](uv_handle_t* handle) { - delete reinterpret_cast(handle); - }; - uv_close(reinterpret_cast(prep_), prep_close); - prep_ = nullptr; + if (session_ == nullptr) + return; + + CHECK_EQ(nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR), 0); + nghttp2_session_del(session_); + session_ = nullptr; + + while (!outstanding_pings_.empty()) { + Http2Session::Http2Ping* ping = PopPing(); + ping->Done(false); + } + + Stop(); } -ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, - size_t maxPayloadLen) { - DEBUG_HTTP2("Http2Session: using max frame size padding\n"); + +inline Http2Stream* Http2Session::FindStream(int32_t id) { + auto s = streams_.find(id); + return s != streams_.end() ? s->second : nullptr; +} + + +inline void Http2Session::AddStream(Http2Stream* stream) { + streams_[stream->id()] = stream; +} + + +inline void Http2Session::RemoveStream(int32_t id) { + streams_.erase(id); +} + + +inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2SESSION2(this, "using max frame size padding: %d", maxPayloadLen); return maxPayloadLen; } -ssize_t Http2Session::OnCallbackPadding(size_t frameLen, - size_t maxPayloadLen) { - DEBUG_HTTP2("Http2Session: using callback padding\n"); - Isolate* isolate = env()->isolate(); - Local context = env()->context(); +inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen, + size_t maxPayloadLen) { + DEBUG_HTTP2SESSION(this, "using callback to determine padding"); + Isolate* isolate = env()->isolate(); HandleScope handle_scope(isolate); + Local context = env()->context(); Context::Scope context_scope(context); #if defined(DEBUG) && DEBUG @@ -281,427 +457,1308 @@ ssize_t Http2Session::OnCallbackPadding(size_t frameLen, uint32_t retval = buffer[PADDING_BUF_RETURN_VALUE]; retval = std::min(retval, static_cast(maxPayloadLen)); retval = std::max(retval, static_cast(frameLen)); + DEBUG_HTTP2SESSION2(this, "using padding size %d", retval); return retval; } -void Http2Session::SetNextStreamID(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - nghttp2_session* s = session->session(); - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: setting next stream id to %d\n", id); - nghttp2_session_set_next_stream_id(s, id); -} -void HttpErrorString(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - uint32_t val = args[0]->Uint32Value(env->context()).ToChecked(); - args.GetReturnValue().Set( - OneByteString(env->isolate(), nghttp2_strerror(val))); +// Submits a graceful shutdown notice to nghttp +// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html +inline void Http2Session::SubmitShutdownNotice() { + // Only an HTTP2 Server is permitted to send a shutdown notice + if (session_type_ == NGHTTP2_SESSION_CLIENT) + return; + DEBUG_HTTP2SESSION(this, "sending shutdown notice"); + // The only situation where this should fail is if the system is + // out of memory, which will cause other problems. Go ahead and crash + // in that case. + CHECK_EQ(nghttp2_submit_shutdown_notice(session_), 0); } -// Serializes the settings object into a Buffer instance that -// would be suitable, for instance, for creating the Base64 -// output for an HTTP2-Settings header field. -void PackSettings(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Http2Settings settings(env); - args.GetReturnValue().Set(settings.Pack()); -} -// Used to fill in the spec defined initial values for each setting. -void RefreshDefaultSettings(const FunctionCallbackInfo& args) { - DEBUG_HTTP2("Http2Session: refreshing default settings\n"); - Environment* env = Environment::GetCurrent(args); - Http2Settings::RefreshDefaults(env); +// Note: This *must* send a SETTINGS frame even if niv == 0 +inline void Http2Session::Settings(const nghttp2_settings_entry iv[], + size_t niv) { + DEBUG_HTTP2SESSION2(this, "submitting %d settings", niv); + // This will fail either if the system is out of memory, or if the settings + // values are not within the appropriate range. We should be catching the + // latter before it gets this far so crash in either case. + CHECK_EQ(nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv), 0); } -template -void Http2Session::RefreshSettings(const FunctionCallbackInfo& args) { - DEBUG_HTTP2("Http2Session: refreshing settings for session\n"); - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Http2Settings::Update(env, session, fn); + +// Write data received from the i/o stream to the underlying nghttp2_session. +inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) { + size_t total = 0; + // Note that nghttp2_session_mem_recv is a synchronous operation that + // will trigger a number of other callbacks. Those will, in turn have + // multiple side effects. + for (size_t n = 0; n < nbufs; n++) { + ssize_t ret = + nghttp2_session_mem_recv(session_, + reinterpret_cast(bufs[n].base), + bufs[n].len); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + + if (ret < 0) + return ret; + + total += ret; + } + // Send any data that was queued up while processing the received data. + SendPendingData(); + return total; } -// Used to fill in the spec defined initial values for each setting. -void RefreshSessionState(const FunctionCallbackInfo& args) { - DEBUG_HTTP2("Http2Session: refreshing session state\n"); - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsObject()); -#endif - AliasedBuffer& buffer = - env->http2_state()->session_state_buffer; - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); - nghttp2_session* s = session->session(); - buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] = - nghttp2_session_get_effective_local_window_size(s); - buffer[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH] = - nghttp2_session_get_effective_recv_data_length(s); - buffer[IDX_SESSION_STATE_NEXT_STREAM_ID] = - nghttp2_session_get_next_stream_id(s); - buffer[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE] = - nghttp2_session_get_local_window_size(s); - buffer[IDX_SESSION_STATE_LAST_PROC_STREAM_ID] = - nghttp2_session_get_last_proc_stream_id(s); - buffer[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE] = - nghttp2_session_get_remote_window_size(s); - buffer[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE] = - nghttp2_session_get_outbound_queue_size(s); - buffer[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE] = - nghttp2_session_get_hd_deflate_dynamic_table_size(s); - buffer[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE] = - nghttp2_session_get_hd_inflate_dynamic_table_size(s); +inline int32_t GetFrameID(const nghttp2_frame* frame) { + // If this is a push promise, we want to grab the id of the promised stream + return (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? + frame->push_promise.promised_stream_id : + frame->hd.stream_id; } -void RefreshStreamState(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 2); - CHECK(args[0]->IsObject()); - CHECK(args[1]->IsNumber()); -#endif - int32_t id = args[1]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: refreshing stream %d state\n", id); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); - nghttp2_session* s = session->session(); - Nghttp2Stream* stream; - AliasedBuffer& buffer = - env->http2_state()->stream_state_buffer; +inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle, + const nghttp2_frame* frame, + void* user_data) { + Http2Session* session = static_cast(user_data); + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(session, "beginning headers for stream %d", id); - if ((stream = session->FindStream(id)) == nullptr) { - buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; - buffer[IDX_STREAM_STATE_WEIGHT] = - buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = - buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = - buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = - buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; - return; + Http2Stream* stream = session->FindStream(id); + if (stream == nullptr) { + new Http2Stream(session, id, frame->headers.cat); + } else { + stream->StartHeaders(frame->headers.cat); } - nghttp2_stream* str = - nghttp2_session_find_stream(s, stream->id()); + return 0; +} - if (str == nullptr) { - buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; - buffer[IDX_STREAM_STATE_WEIGHT] = - buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = - buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = - buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = - buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; - } else { - buffer[IDX_STREAM_STATE] = - nghttp2_stream_get_state(str); - buffer[IDX_STREAM_STATE_WEIGHT] = - nghttp2_stream_get_weight(str); - buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = - nghttp2_stream_get_sum_dependency_weight(str); - buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = - nghttp2_session_get_stream_local_close(s, id); - buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = - nghttp2_session_get_stream_remote_close(s, id); - buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = - nghttp2_session_get_stream_local_window_size(s, id); + +inline int Http2Session::OnHeaderCallback(nghttp2_session* handle, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data) { + Http2Session* session = static_cast(user_data); + int32_t id = GetFrameID(frame); + Http2Stream* stream = session->FindStream(id); + if (!stream->AddHeader(name, value, flags)) { + // This will only happen if the connected peer sends us more + // than the allowed number of header items at any given time + stream->SubmitRstStream(NGHTTP2_ENHANCE_YOUR_CALM); + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } + return 0; } -void Http2Session::New(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args.IsConstructCall()); -#endif - int val = args[0]->IntegerValue(env->context()).ToChecked(); - nghttp2_session_type type = static_cast(val); - DEBUG_HTTP2("Http2Session: creating a session of type: %d\n", type); - new Http2Session(env, args.This(), type); + +inline int Http2Session::OnFrameReceive(nghttp2_session* handle, + const nghttp2_frame* frame, + void* user_data) { + Http2Session* session = static_cast(user_data); + DEBUG_HTTP2SESSION2(session, "complete frame received: type: %d", + frame->hd.type); + switch (frame->hd.type) { + case NGHTTP2_DATA: + session->HandleDataFrame(frame); + break; + case NGHTTP2_PUSH_PROMISE: + // Intentional fall-through, handled just like headers frames + case NGHTTP2_HEADERS: + session->HandleHeadersFrame(frame); + break; + case NGHTTP2_SETTINGS: + session->HandleSettingsFrame(frame); + break; + case NGHTTP2_PRIORITY: + session->HandlePriorityFrame(frame); + break; + case NGHTTP2_GOAWAY: + session->HandleGoawayFrame(frame); + break; + case NGHTTP2_PING: + session->HandlePingFrame(frame); + default: + break; + } + return 0; } -// Capture the stream that this session will use to send and receive data -void Http2Session::Consume(const FunctionCallbackInfo& args) { - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsExternal()); -#endif - session->Consume(args[0].As()); +inline int Http2Session::OnFrameNotSent(nghttp2_session* handle, + const nghttp2_frame* frame, + int error_code, + void* user_data) { + Http2Session* session = static_cast(user_data); + Environment* env = session->env(); + DEBUG_HTTP2SESSION2(session, "frame type %d was not sent, code: %d", + frame->hd.type, error_code); + // Do not report if the frame was not sent due to the session closing + if (error_code != NGHTTP2_ERR_SESSION_CLOSING && + error_code != NGHTTP2_ERR_STREAM_CLOSED && + error_code != NGHTTP2_ERR_STREAM_CLOSING) { + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + Local context = env->context(); + Context::Scope context_scope(context); + + Local argv[3] = { + Integer::New(isolate, frame->hd.stream_id), + Integer::New(isolate, frame->hd.type), + Integer::New(isolate, error_code) + }; + session->MakeCallback(env->onframeerror_string(), arraysize(argv), argv); + } + return 0; } -void Http2Session::Destroy(const FunctionCallbackInfo& args) { - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - DEBUG_HTTP2("Http2Session: destroying session %d\n", session->type()); - Environment* env = Environment::GetCurrent(args); + +inline int Http2Session::OnStreamClose(nghttp2_session* handle, + int32_t id, + uint32_t code, + void* user_data) { + Http2Session* session = static_cast(user_data); + Environment* env = session->env(); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); Local context = env->context(); + Context::Scope context_scope(context); + DEBUG_HTTP2SESSION2(session, "stream %d closed with code: %d", id, code); + Http2Stream* stream = session->FindStream(id); + // Intentionally ignore the callback if the stream does not exist + if (stream != nullptr) { + stream->Close(code); + // It is possible for the stream close to occur before the stream is + // ever passed on to the javascript side. If that happens, ignore this. + Local fn = + stream->object()->Get(context, env->onstreamclose_string()) + .ToLocalChecked(); + if (fn->IsFunction()) { + Local argv[1] = { Integer::NewFromUnsigned(isolate, code) }; + stream->MakeCallback(fn.As(), arraysize(argv), argv); + } + } + return 0; +} - bool skipUnconsume = args[0]->BooleanValue(context).ToChecked(); - if (!skipUnconsume) - session->Unconsume(); - session->Close(); +inline int Http2Session::OnInvalidHeader(nghttp2_session* session, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data) { + // Ignore invalid header fields by default. + return 0; } -void Http2Session::Destroying(const FunctionCallbackInfo& args) { - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - DEBUG_HTTP2("Http2Session: preparing to destroy session %d\n", - session->type()); - session->MarkDestroying(); + +inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle, + uint8_t flags, + int32_t id, + const uint8_t* data, + size_t len, + void* user_data) { + Http2Session* session = static_cast(user_data); + DEBUG_HTTP2SESSION2(session, "buffering data chunk for stream %d, size: " + "%d, flags: %d", id, len, flags); + // We should never actually get a 0-length chunk so this check is + // only a precaution at this point. + if (len > 0) { + CHECK_EQ(nghttp2_session_consume_connection(handle, len), 0); + Http2Stream* stream = session->FindStream(id); + stream->AddChunk(data, len); + } + return 0; } -void Http2Session::SubmitPriority(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Local context = env->context(); - int32_t id = args[0]->Int32Value(context).ToChecked(); - Http2Priority priority(env, args[1], args[2], args[3]); - bool silent = args[4]->BooleanValue(context).ToChecked(); - DEBUG_HTTP2("Http2Session: submitting priority for stream %d", id); +inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* session, + const nghttp2_frame* frame, + size_t maxPayloadLen, + void* user_data) { + Http2Session* handle = static_cast(user_data); + ssize_t padding = frame->hd.length; + + return handle->padding_strategy_ == PADDING_STRATEGY_MAX + ? handle->OnMaxFrameSizePadding(padding, maxPayloadLen) + : handle->OnCallbackPadding(padding, maxPayloadLen); +} + +#define BAD_PEER_MESSAGE "Remote peer returned unexpected data while we " \ + "expected SETTINGS frame. Perhaps, peer does not " \ + "support HTTP/2 properly." + +inline int Http2Session::OnNghttpError(nghttp2_session* handle, + const char* message, + size_t len, + void* user_data) { + // Unfortunately, this is currently the only way for us to know if + // the session errored because the peer is not an http2 peer. + Http2Session* session = static_cast(user_data); + DEBUG_HTTP2SESSION2(session, "Error '%.*s'", len, message); + if (strncmp(message, BAD_PEER_MESSAGE, len) == 0) { + Environment* env = session->env(); + Isolate* isolate = env->isolate(); + HandleScope scope(isolate); + Local context = env->context(); + Context::Scope context_scope(context); + + Local argv[1] = { + Integer::New(isolate, NGHTTP2_ERR_PROTO), + }; + session->MakeCallback(env->error_string(), arraysize(argv), argv); + } + return 0; +} + - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - // invalid stream - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); +inline void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) { + if (stream->HasTrailers()) { + Http2Stream::SubmitTrailers submit_trailers{this, stream, flags}; + stream->OnTrailers(submit_trailers); } +} + - args.GetReturnValue().Set(stream->SubmitPriority(*priority, silent)); +Http2Stream::SubmitTrailers::SubmitTrailers( + Http2Session* session, + Http2Stream* stream, + uint32_t* flags) + : session_(session), stream_(stream), flags_(flags) { } + + +inline void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers, + size_t length) const { + if (length == 0) + return; + DEBUG_HTTP2SESSION2(session_, "sending trailers for stream %d, count: %d", + stream_->id(), length); + *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + CHECK_EQ( + nghttp2_submit_trailer(**session_, stream_->id(), trailers, length), 0); } -void Http2Session::SubmitSettings(const FunctionCallbackInfo& args) { - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); - Http2Settings settings(env); +inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(this, "handle headers frame for stream %d", id); + Http2Stream* stream = FindStream(id); + + nghttp2_header* headers = stream->headers(); + size_t count = stream->headers_count(); + + Local name_str; + Local value_str; + + Local holder = Array::New(isolate); + Local fn = env()->push_values_to_array_function(); + Local argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; + + // The headers are passed in above as a queue of nghttp2_header structs. + // The following converts that into a JS array with the structure: + // [name1, value1, name2, value2, name3, value3, name3, value4] and so on. + // That array is passed up to the JS layer and converted into an Object form + // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it + // this way for performance reasons (it's faster to generate and pass an + // array than it is to generate and pass the object). + size_t n = 0; + while (count > 0) { + size_t j = 0; + while (count > 0 && j < arraysize(argv) / 2) { + nghttp2_header item = headers[n++]; + // The header name and value are passed as external one-byte strings + name_str = + ExternalHeader::New(env(), item.name).ToLocalChecked(); + value_str = + ExternalHeader::New(env(), item.value).ToLocalChecked(); + argv[j * 2] = name_str; + argv[j * 2 + 1] = value_str; + count--; + j++; + } + // For performance, we pass name and value pairs to array.protototype.push + // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no + // more items to push. + if (j > 0) { + fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked(); + } + } + + Local args[5] = { + stream->object(), + Integer::New(isolate, id), + Integer::New(isolate, stream->headers_category()), + Integer::New(isolate, frame->hd.flags), + holder + }; + MakeCallback(env()->onheaders_string(), arraysize(args), args); +} + + +inline void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + + nghttp2_priority priority_frame = frame->priority; + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(this, "handle priority frame for stream %d", id); + // Priority frame stream ID should never be <= 0. nghttp2 handles this for us + nghttp2_priority_spec spec = priority_frame.pri_spec; + + Local argv[4] = { + Integer::New(isolate, id), + Integer::New(isolate, spec.stream_id), + Integer::New(isolate, spec.weight), + Boolean::New(isolate, spec.exclusive) + }; + MakeCallback(env()->onpriority_string(), arraysize(argv), argv); +} + + +inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) { + int32_t id = GetFrameID(frame); + DEBUG_HTTP2SESSION2(this, "handling data frame for stream %d", id); + Http2Stream* stream = FindStream(id); + + if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { + stream->AddChunk(nullptr, 0); + } + + if (stream->IsReading()) + stream->FlushDataChunks(); +} + + +inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + + nghttp2_goaway goaway_frame = frame->goaway; + DEBUG_HTTP2SESSION(this, "handling goaway frame"); + + Local argv[3] = { + Integer::NewFromUnsigned(isolate, goaway_frame.error_code), + Integer::New(isolate, goaway_frame.last_stream_id), + Undefined(isolate) + }; + + size_t length = goaway_frame.opaque_data_len; + if (length > 0) { + argv[2] = Buffer::Copy(isolate, + reinterpret_cast(goaway_frame.opaque_data), + length).ToLocalChecked(); + } + + MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); +} + +inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) { + bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; + if (ack) { + Http2Ping* ping = PopPing(); + if (ping != nullptr) + ping->Done(true, frame->ping.opaque_data); + } +} + + +inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + + bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK; + + Local argv[1] = { Boolean::New(isolate, ack) }; + MakeCallback(env()->onsettings_string(), arraysize(argv), argv); +} + + +inline void Http2Session::SendPendingData() { + DEBUG_HTTP2SESSION(this, "sending pending data"); + // Do not attempt to send data on the socket if the destroying flag has + // been set. That means everything is shutting down and the socket + // will not be usable. + if (IsDestroying()) + return; + + WriteWrap* req = nullptr; + char* dest = nullptr; + size_t destRemaining = 0; + size_t destLength = 0; // amount of data stored in dest + size_t destOffset = 0; // current write offset of dest + + const uint8_t* src; // pointer to the serialized data + ssize_t srcLength = 0; // length of serialized data chunk + + // While srcLength is greater than zero + while ((srcLength = nghttp2_session_mem_send(session_, &src)) > 0) { + if (req == nullptr) { + req = AllocateSend(); + destRemaining = req->ExtraSize(); + dest = req->Extra(); + } + DEBUG_HTTP2SESSION2(this, "nghttp2 has %d bytes to send", srcLength); + size_t srcRemaining = srcLength; + size_t srcOffset = 0; + + // The amount of data we have to copy is greater than the space + // remaining. Copy what we can into the remaining space, send it, + // the proceed with the rest. + while (srcRemaining > destRemaining) { + DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", + destLength + destRemaining); + memcpy(dest + destOffset, src + srcOffset, destRemaining); + destLength += destRemaining; + Send(req, dest, destLength); + destOffset = 0; + destLength = 0; + srcRemaining -= destRemaining; + srcOffset += destRemaining; + req = AllocateSend(); + destRemaining = req->ExtraSize(); + dest = req->Extra(); + } + + if (srcRemaining > 0) { + memcpy(dest + destOffset, src + srcOffset, srcRemaining); + destLength += srcRemaining; + destOffset += srcRemaining; + destRemaining -= srcRemaining; + srcRemaining = 0; + srcOffset = 0; + } + } + CHECK_NE(srcLength, NGHTTP2_ERR_NOMEM); + + if (destLength > 0) { + DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", destLength); + Send(req, dest, destLength); + } +} + + +inline Http2Stream* Http2Session::SubmitRequest( + nghttp2_priority_spec* prispec, + nghttp2_nv* nva, + size_t len, + int32_t* ret, + int options) { + DEBUG_HTTP2SESSION(this, "submitting request"); + Http2Stream* stream = nullptr; + Http2Stream::Provider::Stream prov(options); + *ret = nghttp2_submit_request(session_, prispec, nva, len, *prov, nullptr); + CHECK_NE(*ret, NGHTTP2_ERR_NOMEM); + if (*ret > 0) + stream = new Http2Stream(this, *ret, NGHTTP2_HCAT_HEADERS, options); + return stream; +} + +inline void Http2Session::SetChunksSinceLastWrite(size_t n) { + chunks_sent_since_last_write_ = n; +} + + +WriteWrap* Http2Session::AllocateSend() { + HandleScope scope(env()->isolate()); + auto AfterWrite = [](WriteWrap* req, int status) { + req->Dispose(); + }; + Local obj = + env()->write_wrap_constructor_function() + ->NewInstance(env()->context()).ToLocalChecked(); + // Base the amount allocated on the remote peers max frame size + uint32_t size = + nghttp2_session_get_remote_settings( + session(), + NGHTTP2_SETTINGS_MAX_FRAME_SIZE); + // Max frame size + 9 bytes for the header + return WriteWrap::New(env(), obj, stream_, AfterWrite, size + 9); +} + +void Http2Session::Send(WriteWrap* req, char* buf, size_t length) { + DEBUG_HTTP2SESSION(this, "attempting to send data"); + if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { + return; + } + + chunks_sent_since_last_write_++; + uv_buf_t actual = uv_buf_init(buf, length); + if (stream_->DoWrite(req, &actual, 1, nullptr)) { + req->Dispose(); + } +} + + +void Http2Session::OnStreamAllocImpl(size_t suggested_size, + uv_buf_t* buf, + void* ctx) { + Http2Session* session = static_cast(ctx); + buf->base = session->stream_alloc(); + buf->len = kAllocBufferSize; +} + + +void Http2Session::OnStreamReadImpl(ssize_t nread, + const uv_buf_t* bufs, + uv_handle_type pending, + void* ctx) { + Http2Session* session = static_cast(ctx); + if (nread < 0) { + uv_buf_t tmp_buf; + tmp_buf.base = nullptr; + tmp_buf.len = 0; + session->prev_read_cb_.fn(nread, + &tmp_buf, + pending, + session->prev_read_cb_.ctx); + return; + } + if (nread > 0) { + // Only pass data on if nread > 0 + uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; + ssize_t ret = session->Write(buf, 1); + if (ret < 0) { + DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret); + CHECK_EQ(nghttp2_session_terminate_session(session->session(), + NGHTTP2_PROTOCOL_ERROR), 0); + } + } +} + + +void Http2Session::Consume(Local external) { + StreamBase* stream = static_cast(external->Value()); + stream->Consume(); + stream_ = stream; + prev_alloc_cb_ = stream->alloc_cb(); + prev_read_cb_ = stream->read_cb(); + stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this }); + stream->set_read_cb({ Http2Session::OnStreamReadImpl, this }); + DEBUG_HTTP2SESSION(this, "i/o stream consumed"); +} + + +void Http2Session::Unconsume() { + if (prev_alloc_cb_.is_empty()) + return; + stream_->set_alloc_cb(prev_alloc_cb_); + stream_->set_read_cb(prev_read_cb_); + prev_alloc_cb_.clear(); + prev_read_cb_.clear(); + stream_ = nullptr; + DEBUG_HTTP2SESSION(this, "i/o stream unconsumed"); +} + + + + +Http2Stream::Http2Stream( + Http2Session* session, + int32_t id, + nghttp2_headers_category category, + int options) : AsyncWrap(session->env(), + session->env()->http2stream_constructor_template() + ->NewInstance(session->env()->context()) + .ToLocalChecked(), + AsyncWrap::PROVIDER_HTTP2STREAM), + StreamBase(session->env()), + session_(session), + id_(id), + current_headers_category_(category) { + MakeWeak(this); + + // Limit the number of header pairs + max_header_pairs_ = session->GetMaxHeaderPairs(); + if (max_header_pairs_ == 0) + max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; + current_headers_.reserve(max_header_pairs_); + + // Limit the number of header octets + max_header_length_ = + std::min( + nghttp2_session_get_local_settings( + session->session(), + NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE), + MAX_MAX_HEADER_LIST_SIZE); + + if (options & STREAM_OPTION_GET_TRAILERS) + flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS; + + if (options & STREAM_OPTION_EMPTY_PAYLOAD) + Shutdown(); + session->AddStream(this); +} + + +Http2Stream::~Http2Stream() { + CHECK(persistent().IsEmpty()); + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); +} + +void Http2Stream::StartHeaders(nghttp2_headers_category category) { + DEBUG_HTTP2STREAM2(this, "starting headers, category: %d", id_, category); + current_headers_length_ = 0; + current_headers_.clear(); + current_headers_category_ = category; +} + +nghttp2_stream* Http2Stream::operator*() { + return nghttp2_session_find_stream(**session_, id_); +} + + +void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) { + DEBUG_HTTP2STREAM(this, "prompting for trailers"); + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + Local context = env()->context(); + Context::Scope context_scope(context); + + Local ret = + MakeCallback(env()->ontrailers_string(), 0, nullptr).ToLocalChecked(); + if (!ret.IsEmpty()) { + if (ret->IsArray()) { + Local headers = ret.As(); + if (headers->Length() > 0) { + Headers trailers(isolate, context, headers); + submit_trailers.Submit(*trailers, trailers.length()); + } + } + } +} + + +inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) { + char* buf = nullptr; + if (len > 0) { + buf = Malloc(len); + memcpy(buf, data, len); + } + data_chunks_.emplace(uv_buf_init(buf, len)); +} + + +int Http2Stream::DoWrite(WriteWrap* req_wrap, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) { + session_->SetChunksSinceLastWrite(); + + nghttp2_stream_write_t* req = new nghttp2_stream_write_t; + req->data = req_wrap; + + auto AfterWrite = [](nghttp2_stream_write_t* req, int status) { + WriteWrap* wrap = static_cast(req->data); + wrap->Done(status); + delete req; + }; + req_wrap->Dispatched(); + Write(req, bufs, count, AfterWrite); + return 0; +} + + +inline void Http2Stream::Close(int32_t code) { + flags_ |= NGHTTP2_STREAM_FLAG_CLOSED; + code_ = code; + DEBUG_HTTP2STREAM2(this, "closed with code %d", code); +} + + +inline void Http2Stream::Shutdown() { + flags_ |= NGHTTP2_STREAM_FLAG_SHUT; + CHECK_NE(nghttp2_session_resume_data(session_->session(), id_), + NGHTTP2_ERR_NOMEM); + DEBUG_HTTP2STREAM(this, "writable side shutdown"); +} + +int Http2Stream::DoShutdown(ShutdownWrap* req_wrap) { + req_wrap->Dispatched(); + Shutdown(); + req_wrap->Done(0); + return 0; +} + +inline void Http2Stream::Destroy() { + DEBUG_HTTP2STREAM(this, "destroying stream"); + // Do nothing if this stream instance is already destroyed + if (IsDestroyed()) + return; + + flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED; + Http2Session* session = this->session_; + + if (session != nullptr) { + session_->RemoveStream(id_); + session_ = nullptr; + } + + // Free any remaining incoming data chunks. + while (!data_chunks_.empty()) { + uv_buf_t buf = data_chunks_.front(); + free(buf.base); + data_chunks_.pop(); + } + + // Free any remaining outgoing data chunks. + while (!queue_.empty()) { + nghttp2_stream_write* head = queue_.front(); + head->cb(head->req, UV_ECANCELED); + delete head; + queue_.pop(); + } + + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); + + delete this; +} + + +void Http2Stream::OnDataChunk( + uv_buf_t* chunk) { + Isolate* isolate = env()->isolate(); + HandleScope scope(isolate); + ssize_t len = -1; + Local buf; + if (chunk != nullptr) { + len = chunk->len; + buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked(); + } + EmitData(len, buf, this->object()); +} + + +inline void Http2Stream::FlushDataChunks() { + if (!data_chunks_.empty()) { + uv_buf_t buf = data_chunks_.front(); + data_chunks_.pop(); + if (buf.len > 0) { + CHECK_EQ(nghttp2_session_consume_stream(session_->session(), + id_, buf.len), 0); + OnDataChunk(&buf); + } else { + OnDataChunk(nullptr); + } + } +} + + +inline int Http2Stream::SubmitResponse(nghttp2_nv* nva, + size_t len, + int options) { + DEBUG_HTTP2STREAM(this, "submitting response"); + if (options & STREAM_OPTION_GET_TRAILERS) + flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS; + + if (!IsWritable()) + options |= STREAM_OPTION_EMPTY_PAYLOAD; + + Http2Stream::Provider::Stream prov(this, options); + int ret = nghttp2_submit_response(session_->session(), id_, nva, len, *prov); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +// Initiate a response that contains data read from a file descriptor. +inline int Http2Stream::SubmitFile(int fd, + nghttp2_nv* nva, size_t len, + int64_t offset, + int64_t length, + int options) { + DEBUG_HTTP2STREAM(this, "submitting file"); + if (options & STREAM_OPTION_GET_TRAILERS) + flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS; + + if (offset > 0) fd_offset_ = offset; + if (length > -1) fd_length_ = length; + + Http2Stream::Provider::FD prov(this, options, fd); + int ret = nghttp2_submit_response(session_->session(), id_, nva, len, *prov); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +// Submit informational headers for a stream. +inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { + DEBUG_HTTP2STREAM2(this, "sending %d informational headers", len); + int ret = nghttp2_submit_headers(session_->session(), + NGHTTP2_FLAG_NONE, + id_, nullptr, + nva, len, nullptr); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec, + bool silent) { + DEBUG_HTTP2STREAM(this, "sending priority spec"); + int ret = silent ? + nghttp2_session_change_stream_priority(session_->session(), + id_, prispec) : + nghttp2_submit_priority(session_->session(), + NGHTTP2_FLAG_NONE, + id_, prispec); + CHECK_NE(ret, NGHTTP2_ERR_NOMEM); + return ret; +} + + +inline int Http2Stream::SubmitRstStream(const uint32_t code) { + DEBUG_HTTP2STREAM2(this, "sending rst-stream with code %d", code); + session_->SendPendingData(); + CHECK_EQ(nghttp2_submit_rst_stream(session_->session(), + NGHTTP2_FLAG_NONE, + id_, + code), 0); + return 0; +} + + +// Submit a push promise. +inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva, + size_t len, + int32_t* ret, + int options) { + DEBUG_HTTP2STREAM(this, "sending push promise"); + *ret = nghttp2_submit_push_promise(session_->session(), NGHTTP2_FLAG_NONE, + id_, nva, len, nullptr); + CHECK_NE(*ret, NGHTTP2_ERR_NOMEM); + Http2Stream* stream = nullptr; + if (*ret > 0) + stream = new Http2Stream(session_, *ret, NGHTTP2_HCAT_HEADERS, options); + + return stream; +} + +inline int Http2Stream::ReadStart() { + flags_ |= NGHTTP2_STREAM_FLAG_READ_START; + flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED; + + // Flush any queued data chunks immediately out to the JS layer + FlushDataChunks(); + DEBUG_HTTP2STREAM(this, "reading starting"); + return 0; +} + + +inline int Http2Stream::ReadStop() { + if (!IsReading()) + return 0; + flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED; + DEBUG_HTTP2STREAM(this, "reading stopped"); + return 0; +} + +// Queue the given set of uv_but_t handles for writing to an +// nghttp2_stream. The callback will be invoked once the chunks +// of data have been flushed to the underlying nghttp2_session. +// Note that this does *not* mean that the data has been flushed +// to the socket yet. +inline int Http2Stream::Write(nghttp2_stream_write_t* req, + const uv_buf_t bufs[], + unsigned int nbufs, + nghttp2_stream_write_cb cb) { + if (!IsWritable()) { + if (cb != nullptr) + cb(req, UV_EOF); + return 0; + } + DEBUG_HTTP2STREAM2(this, "queuing %d buffers to send", id_, nbufs); + nghttp2_stream_write* item = new nghttp2_stream_write; + item->cb = cb; + item->req = req; + item->nbufs = nbufs; + item->bufs.AllocateSufficientStorage(nbufs); + memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs)); + queue_.push(item); + CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM); + return 0; +} + +inline size_t GetBufferLength(nghttp2_rcbuf* buf) { + return nghttp2_rcbuf_get_buf(buf).len; +} + +inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags) { + size_t length = GetBufferLength(name) + GetBufferLength(value) + 32; + if (current_headers_.size() == max_header_pairs_ || + current_headers_length_ + length > max_header_length_) { + return false; + } + nghttp2_header header; + header.name = name; + header.value = value; + header.flags = flags; + current_headers_.push_back(header); + nghttp2_rcbuf_incref(name); + nghttp2_rcbuf_incref(value); + current_headers_length_ += length; + return true; +} + + +Http2Stream* GetStream(Http2Session* session, + int32_t id, + nghttp2_data_source* source) { + Http2Stream* stream = static_cast(source->ptr); + if (stream == nullptr) + stream = session->FindStream(id); + CHECK_NE(stream, nullptr); + CHECK_EQ(id, stream->id()); + return stream; +} + +Http2Stream::Provider::Provider(Http2Stream* stream, int options) { + provider_.source.ptr = stream; + empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD; +} + +Http2Stream::Provider::Provider(int options) { + provider_.source.ptr = nullptr; + empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD; +} + +Http2Stream::Provider::~Provider() { + provider_.source.ptr = nullptr; +} + +Http2Stream::Provider::FD::FD(Http2Stream* stream, int options, int fd) + : Http2Stream::Provider(stream, options) { + provider_.source.fd = fd; + provider_.read_callback = Http2Stream::Provider::FD::OnRead; +} + +Http2Stream::Provider::FD::FD(int options, int fd) + : Http2Stream::Provider(options) { + provider_.source.fd = fd; + provider_.read_callback = Http2Stream::Provider::FD::OnRead; +} + +ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data) { + Http2Session* session = static_cast(user_data); + Http2Stream* stream = session->FindStream(id); + DEBUG_HTTP2SESSION2(session, "reading outbound file data for stream %d", id); + CHECK_EQ(id, stream->id()); + + int fd = source->fd; + int64_t offset = stream->fd_offset_; + ssize_t numchars = 0; + + if (stream->fd_length_ >= 0 && + stream->fd_length_ < static_cast(length)) + length = stream->fd_length_; + + uv_buf_t data; + data.base = reinterpret_cast(buf); + data.len = length; + + uv_fs_t read_req; + + if (length > 0) { + // TODO(addaleax): Never use synchronous I/O on the main thread. + numchars = uv_fs_read(session->event_loop(), + &read_req, + fd, &data, 1, + offset, nullptr); + uv_fs_req_cleanup(&read_req); + } + + // Close the stream with an error if reading fails + if (numchars < 0) + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + + // Update the read offset for the next read + stream->fd_offset_ += numchars; + stream->fd_length_ -= numchars; + + // if numchars < length, assume that we are done. + if (static_cast(numchars) < length || length <= 0) { + DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id); + *flags |= NGHTTP2_DATA_FLAG_EOF; + session->GetTrailers(stream, flags); + } + + return numchars; +} + +Http2Stream::Provider::Stream::Stream(int options) + : Http2Stream::Provider(options) { + provider_.read_callback = Http2Stream::Provider::Stream::OnRead; +} + +Http2Stream::Provider::Stream::Stream(Http2Stream* stream, int options) + : Http2Stream::Provider(stream, options) { + provider_.read_callback = Http2Stream::Provider::Stream::OnRead; +} + +ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data) { + Http2Session* session = static_cast(user_data); + DEBUG_HTTP2SESSION2(session, "reading outbound data for stream %d", id); + Http2Stream* stream = GetStream(session, id, source); + CHECK_EQ(id, stream->id()); + + size_t amount = 0; // amount of data being sent in this data frame. + + uv_buf_t current; + + if (!stream->queue_.empty()) { + DEBUG_HTTP2SESSION2(session, "stream %d has pending outbound data", id); + nghttp2_stream_write* head = stream->queue_.front(); + current = head->bufs[stream->queue_index_]; + size_t clen = current.len - stream->queue_offset_; + amount = std::min(clen, length); + DEBUG_HTTP2SESSION2(session, "sending %d bytes for data frame on stream %d", + amount, id); + if (amount > 0) { + memcpy(buf, current.base + stream->queue_offset_, amount); + stream->queue_offset_ += amount; + } + if (stream->queue_offset_ == current.len) { + stream->queue_index_++; + stream->queue_offset_ = 0; + } + if (stream->queue_index_ == head->nbufs) { + head->cb(head->req, 0); + delete head; + stream->queue_.pop(); + stream->queue_offset_ = 0; + stream->queue_index_ = 0; + } + } + + if (amount == 0 && stream->IsWritable() && stream->queue_.empty()) { + DEBUG_HTTP2SESSION2(session, "deferring stream %d", id); + return NGHTTP2_ERR_DEFERRED; + } + + if (stream->queue_.empty() && !stream->IsWritable()) { + DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id); + *flags |= NGHTTP2_DATA_FLAG_EOF; + + session->GetTrailers(stream, flags); + } + + return amount; +} + + + +// Implementation of the JavaScript API + +void HttpErrorString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + uint32_t val = args[0]->Uint32Value(env->context()).ToChecked(); args.GetReturnValue().Set( - session->Nghttp2Session::SubmitSettings(*settings, settings.length())); + String::NewFromOneByte( + env->isolate(), + reinterpret_cast(nghttp2_strerror(val)), + v8::NewStringType::kInternalized).ToLocalChecked()); +} + + +// Serializes the settings object into a Buffer instance that +// would be suitable, for instance, for creating the Base64 +// output for an HTTP2-Settings header field. +void PackSettings(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Settings settings(env); + args.GetReturnValue().Set(settings.Pack()); } -void Http2Session::SubmitRstStream(const FunctionCallbackInfo& args) { + +void RefreshDefaultSettings(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - Local context = env->context(); + Http2Settings::RefreshDefaults(env); +} -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsNumber()); -#endif +void Http2Session::SetNextStreamID(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + int32_t id = args[0]->Int32Value(env->context()).ToChecked(); + if (nghttp2_session_set_next_stream_id(**session, id) < 0) { + DEBUG_HTTP2SESSION2(session, "failed to set next stream id to %d", id); + return args.GetReturnValue().Set(false); + } + args.GetReturnValue().Set(true); + DEBUG_HTTP2SESSION2(session, "set next stream id to %d", id); +} - int32_t id = args[0]->Int32Value(context).ToChecked(); - uint32_t code = args[1]->Uint32Value(context).ToChecked(); - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - // invalid stream - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - DEBUG_HTTP2("Http2Session: sending rst_stream for stream %d, code: %d\n", - id, code); - args.GetReturnValue().Set(stream->SubmitRstStream(code)); +template +void Http2Session::RefreshSettings(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Http2Settings::Update(env, session, fn); + DEBUG_HTTP2SESSION(session, "settings refreshed for session"); } -void Http2Session::SubmitRequest(const FunctionCallbackInfo& args) { - // args[0] Array of headers - // args[1] options int - // args[2] parentStream ID (for priority spec) - // args[3] weight (for priority spec) - // args[4] exclusive boolean (for priority spec) -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsArray()); -#endif +void Http2Session::RefreshState(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); - Local context = env->context(); - Isolate* isolate = env->isolate(); + DEBUG_HTTP2SESSION(session, "refreshing state"); + + AliasedBuffer& buffer = + env->http2_state()->session_state_buffer; + + nghttp2_session* s = **session; + + buffer[IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_effective_local_window_size(s); + buffer[IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH] = + nghttp2_session_get_effective_recv_data_length(s); + buffer[IDX_SESSION_STATE_NEXT_STREAM_ID] = + nghttp2_session_get_next_stream_id(s); + buffer[IDX_SESSION_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_local_window_size(s); + buffer[IDX_SESSION_STATE_LAST_PROC_STREAM_ID] = + nghttp2_session_get_last_proc_stream_id(s); + buffer[IDX_SESSION_STATE_REMOTE_WINDOW_SIZE] = + nghttp2_session_get_remote_window_size(s); + buffer[IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE] = + nghttp2_session_get_outbound_queue_size(s); + buffer[IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE] = + nghttp2_session_get_hd_deflate_dynamic_table_size(s); + buffer[IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE] = + nghttp2_session_get_hd_inflate_dynamic_table_size(s); +} - Local headers = args[0].As(); - int options = args[1]->IntegerValue(context).ToChecked(); - Http2Priority priority(env, args[2], args[3], args[4]); - DEBUG_HTTP2("Http2Session: submitting request: headers: %d, options: %d\n", - headers->Length(), options); +void Http2Session::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args.IsConstructCall()); + int val = args[0]->IntegerValue(env->context()).ToChecked(); + nghttp2_session_type type = static_cast(val); + Http2Session* session = new Http2Session(env, args.This(), type); + session->get_async_id(); // avoid compiler warning + DEBUG_HTTP2SESSION(session, "session created"); +} - Headers list(isolate, context, headers); - int32_t ret = session->Nghttp2Session::SubmitRequest(*priority, - *list, list.length(), - nullptr, options); - DEBUG_HTTP2("Http2Session: request submitted, response: %d\n", ret); - args.GetReturnValue().Set(ret); +void Http2Session::Consume(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsExternal()); + session->Consume(args[0].As()); } -void Http2Session::SubmitResponse(const FunctionCallbackInfo& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsArray()); -#endif +void Http2Session::Destroy(const FunctionCallbackInfo& args) { Http2Session* session; - Nghttp2Stream* stream; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Environment* env = session->env(); - Local context = env->context(); - Isolate* isolate = env->isolate(); + DEBUG_HTTP2SESSION(session, "destroying session"); - int32_t id = args[0]->Int32Value(context).ToChecked(); - Local headers = args[1].As(); - int options = args[2]->IntegerValue(context).ToChecked(); + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); - DEBUG_HTTP2("Http2Session: submitting response for stream %d: headers: %d, " - "options: %d\n", id, headers->Length(), options); + bool skipUnconsume = args[0]->BooleanValue(context).ToChecked(); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } + if (!skipUnconsume) + session->Unconsume(); + session->Close(); +} - Headers list(isolate, context, headers); - args.GetReturnValue().Set( - stream->SubmitResponse(*list, list.length(), options)); +void Http2Session::Destroying(const FunctionCallbackInfo& args) { + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->MarkDestroying(); + DEBUG_HTTP2SESSION(session, "preparing to destroy session"); } -void Http2Session::SubmitFile(const FunctionCallbackInfo& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); // Stream ID - CHECK(args[1]->IsNumber()); // File Descriptor - CHECK(args[2]->IsArray()); // Headers - CHECK(args[3]->IsNumber()); // Offset - CHECK(args[4]->IsNumber()); // Length -#endif +void Http2Session::Settings(const FunctionCallbackInfo& args) { Http2Session* session; - Nghttp2Stream* stream; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); Environment* env = session->env(); - Local context = env->context(); - Isolate* isolate = env->isolate(); - - int32_t id = args[0]->Int32Value(context).ToChecked(); - int fd = args[1]->Int32Value(context).ToChecked(); - Local headers = args[2].As(); - - int64_t offset = args[3]->IntegerValue(context).ToChecked(); - int64_t length = args[4]->IntegerValue(context).ToChecked(); - int options = args[5]->IntegerValue(context).ToChecked(); - -#if defined(DEBUG) && DEBUG - CHECK_GE(offset, 0); -#endif - DEBUG_HTTP2("Http2Session: submitting file %d for stream %d: headers: %d, " - "end-stream: %d\n", fd, id, headers->Length()); - - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - - session->chunks_sent_since_last_write_ = 0; - - Headers list(isolate, context, headers); - - args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(), - offset, length, options)); + Http2Settings settings(env); + session->Http2Session::Settings(*settings, settings.length()); + DEBUG_HTTP2SESSION(session, "settings submitted"); } -void Http2Session::SendHeaders(const FunctionCallbackInfo& args) { -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); - CHECK(args[1]->IsArray()); -#endif +void Http2Session::Request(const FunctionCallbackInfo& args) { Http2Session* session; - Nghttp2Stream* stream; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); Environment* env = session->env(); Local context = env->context(); Isolate* isolate = env->isolate(); - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - Local headers = args[1].As(); - - DEBUG_HTTP2("Http2Session: sending informational headers for stream %d, " - "count: %d\n", id, headers->Length()); - - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } + Local headers = args[0].As(); + int options = args[1]->IntegerValue(context).ToChecked(); + Http2Priority priority(env, args[2], args[3], args[4]); Headers list(isolate, context, headers); - args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); -} + DEBUG_HTTP2SESSION(session, "request submitted"); -void Http2Session::ShutdownStream(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); -#endif - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Nghttp2Stream* stream; - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: shutting down stream %d\n", id); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->Shutdown(); -} + int32_t ret = 0; + Http2Stream* stream = + session->Http2Session::SubmitRequest(*priority, *list, list.length(), + &ret, options); -void Http2Session::StreamReadStart(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); -#endif - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Nghttp2Stream* stream; - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); + if (ret <= 0) { + DEBUG_HTTP2SESSION2(session, "could not submit request: %s", + nghttp2_strerror(ret)); + return args.GetReturnValue().Set(ret); } - stream->ReadStart(); -} - -void Http2Session::StreamReadStop(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); -#endif - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - Nghttp2Stream* stream; - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->ReadStop(); + DEBUG_HTTP2SESSION2(session, "request submitted, new stream id %d", + stream->id()); + args.GetReturnValue().Set(stream->object()); } -void Http2Session::SendShutdownNotice( - const FunctionCallbackInfo& args) { + +void Http2Session::ShutdownNotice(const FunctionCallbackInfo& args) { Http2Session* session; ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); session->SubmitShutdownNotice(); + DEBUG_HTTP2SESSION(session, "shutdown notice sent"); } -void Http2Session::SubmitGoaway(const FunctionCallbackInfo& args) { + +void Http2Session::Goaway(const FunctionCallbackInfo& args) { Http2Session* session; Environment* env = Environment::GetCurrent(args); Local context = env->context(); @@ -721,487 +1778,288 @@ void Http2Session::SubmitGoaway(const FunctionCallbackInfo& args) { length = buf_length; } - DEBUG_HTTP2("Http2Session: initiating immediate shutdown. " - "last-stream-id: %d, code: %d, opaque-data: %d\n", - lastStreamID, errorCode, length); int status = nghttp2_submit_goaway(session->session(), NGHTTP2_FLAG_NONE, lastStreamID, errorCode, data, length); + CHECK_NE(status, NGHTTP2_ERR_NOMEM); args.GetReturnValue().Set(status); + DEBUG_HTTP2SESSION2(session, "immediate shutdown initiated with " + "last stream id %d, code %d, and opaque-data length %d", + lastStreamID, errorCode, length); } -void Http2Session::DestroyStream(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsNumber()); -#endif - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: destroy stream %d\n", id); - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->Destroy(); -} - -void Http2Session::FlushData(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Http2Session* session; - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); -#if defined(DEBUG) && DEBUG - CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsNumber()); -#endif - int32_t id = args[0]->Int32Value(env->context()).ToChecked(); - DEBUG_HTTP2("Http2Session: flushing data to js for stream %d\n", id); - Nghttp2Stream* stream; - if (!(stream = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - stream->ReadResume(); -} void Http2Session::UpdateChunksSent(const FunctionCallbackInfo& args) { - Http2Session* session; Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - HandleScope scope(isolate); - - uint32_t length = session->chunks_sent_since_last_write_; - - session->object()->Set(env->context(), - env->chunks_sent_since_last_write_string(), - Integer::NewFromUnsigned(isolate, length)).FromJust(); - - args.GetReturnValue().Set(length); -} - -void Http2Session::SubmitPushPromise(const FunctionCallbackInfo& args) { Http2Session* session; - Environment* env = Environment::GetCurrent(args); - Local context = env->context(); - Isolate* isolate = env->isolate(); - ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - -#if defined(DEBUG) && DEBUG - CHECK(args[0]->IsNumber()); // parent stream ID - CHECK(args[1]->IsArray()); // headers array -#endif - - Nghttp2Stream* parent; - int32_t id = args[0]->Int32Value(context).ToChecked(); - Local headers = args[1].As(); - int options = args[2]->IntegerValue(context).ToChecked(); - - DEBUG_HTTP2("Http2Session: submitting push promise for stream %d: " - "options: %d, headers: %d\n", id, options, - headers->Length()); - - if (!(parent = session->FindStream(id))) { - return args.GetReturnValue().Set(NGHTTP2_ERR_INVALID_STREAM_ID); - } - - Headers list(isolate, context, headers); - - int32_t ret = parent->SubmitPushPromise(*list, list.length(), - nullptr, options); - DEBUG_HTTP2("Http2Session: push promise submitted, ret: %d\n", ret); - args.GetReturnValue().Set(ret); -} - -int Http2Session::DoWrite(WriteWrap* req_wrap, - uv_buf_t* bufs, - size_t count, - uv_stream_t* send_handle) { - Environment* env = req_wrap->env(); - Local req_wrap_obj = req_wrap->object(); - Local context = env->context(); - - Nghttp2Stream* stream; - { - Local val = - req_wrap_obj->Get(context, env->stream_string()).ToLocalChecked(); - int32_t id = val->Int32Value(context).ToChecked(); - if (!val->IsNumber() || !(stream = FindStream(id))) { - // invalid stream - req_wrap->Dispatched(); - req_wrap->Done(0); - return NGHTTP2_ERR_INVALID_STREAM_ID; - } - } - - chunks_sent_since_last_write_ = 0; - - nghttp2_stream_write_t* req = new nghttp2_stream_write_t; - req->data = req_wrap; - - auto AfterWrite = [](nghttp2_stream_write_t* req, int status) { - WriteWrap* wrap = static_cast(req->data); - wrap->Done(status); - delete req; - }; - req_wrap->Dispatched(); - stream->Write(req, bufs, count, AfterWrite); - return 0; -} - -WriteWrap* Http2Session::AllocateSend() { - HandleScope scope(env()->isolate()); - auto AfterWrite = [](WriteWrap* req, int status) { - req->Dispose(); - }; - Local obj = - env()->write_wrap_constructor_function() - ->NewInstance(env()->context()).ToLocalChecked(); - // Base the amount allocated on the remote peers max frame size - uint32_t size = - nghttp2_session_get_remote_settings( - session(), - NGHTTP2_SETTINGS_MAX_FRAME_SIZE); - // Max frame size + 9 bytes for the header - return WriteWrap::New(env(), obj, this, AfterWrite, size + 9); -} - -void Http2Session::Send(WriteWrap* req, char* buf, size_t length) { - DEBUG_HTTP2("Http2Session: Attempting to send data\n"); - if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) { - return; - } - - chunks_sent_since_last_write_++; - uv_buf_t actual = uv_buf_init(buf, length); - if (stream_->DoWrite(req, &actual, 1, nullptr)) { - req->Dispose(); - } -} - -void Http2Session::OnTrailers(Nghttp2Stream* stream, - const SubmitTrailers& submit_trailers) { - DEBUG_HTTP2("Http2Session: prompting for trailers on stream %d\n", - stream->id()); - Local context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - - Local argv[1] = { - Integer::New(isolate, stream->id()) - }; - - Local ret = MakeCallback(env()->ontrailers_string(), - arraysize(argv), argv).ToLocalChecked(); - if (!ret.IsEmpty()) { - if (ret->IsArray()) { - Local headers = ret.As(); - if (headers->Length() > 0) { - Headers trailers(isolate, context, headers); - submit_trailers.Submit(*trailers, trailers.length()); - } - } - } -} - -void Http2Session::OnHeaders( - Nghttp2Stream* stream, - nghttp2_header* headers, - size_t count, - nghttp2_headers_category cat, - uint8_t flags) { - Local context = env()->context(); - Isolate* isolate = env()->isolate(); - Context::Scope context_scope(context); - HandleScope scope(isolate); - Local name_str; - Local value_str; - - Local holder = Array::New(isolate); - Local fn = env()->push_values_to_array_function(); - Local argv[NODE_PUSH_VAL_TO_ARRAY_MAX * 2]; - -#if defined(DEBUG) && DEBUG - CHECK_LE(cat, NGHTTP2_HCAT_HEADERS); -#endif - - // The headers are passed in above as a queue of nghttp2_header structs. - // The following converts that into a JS array with the structure: - // [name1, value1, name2, value2, name3, value3, name3, value4] and so on. - // That array is passed up to the JS layer and converted into an Object form - // like {name1: value1, name2: value2, name3: [value3, value4]}. We do it - // this way for performance reasons (it's faster to generate and pass an - // array than it is to generate and pass the object). - size_t n = 0; - while (count > 0) { - size_t j = 0; - while (count > 0 && j < arraysize(argv) / 2) { - nghttp2_header item = headers[n++]; - // The header name and value are passed as external one-byte strings - name_str = - ExternalHeader::New(env(), item.name).ToLocalChecked(); - value_str = - ExternalHeader::New(env(), item.value).ToLocalChecked(); - argv[j * 2] = name_str; - argv[j * 2 + 1] = value_str; - count--; - j++; - } - // For performance, we pass name and value pairs to array.protototype.push - // in batches of size NODE_PUSH_VAL_TO_ARRAY_MAX * 2 until there are no - // more items to push. - if (j > 0) { - fn->Call(env()->context(), holder, j * 2, argv).ToLocalChecked(); - } - } - - Local args[4] = { - Integer::New(isolate, stream->id()), - Integer::New(isolate, cat), - Integer::New(isolate, flags), - holder - }; - MakeCallback(env()->onheaders_string(), arraysize(args), args); -} + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + uint32_t length = session->chunks_sent_since_last_write_; -void Http2Session::OnStreamClose(int32_t id, uint32_t code) { - Isolate* isolate = env()->isolate(); - Local context = env()->context(); - HandleScope scope(isolate); - Context::Scope context_scope(context); + session->object()->Set(env->context(), + env->chunks_sent_since_last_write_string(), + Integer::NewFromUnsigned(isolate, length)).FromJust(); - Local argv[2] = { - Integer::New(isolate, id), - Integer::NewFromUnsigned(isolate, code) - }; - MakeCallback(env()->onstreamclose_string(), arraysize(argv), argv); + args.GetReturnValue().Set(length); } -void Http2Session::OnDataChunk( - Nghttp2Stream* stream, - uv_buf_t* chunk) { - Isolate* isolate = env()->isolate(); - Local context = env()->context(); - HandleScope scope(isolate); - Local obj = Object::New(isolate); - obj->Set(context, - env()->id_string(), - Integer::New(isolate, stream->id())).FromJust(); - ssize_t len = -1; - Local buf; - if (chunk != nullptr) { - len = chunk->len; - buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked(); - } - EmitData(len, buf, obj); + +void Http2Stream::RstStream(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + uint32_t code = args[0]->Uint32Value(context).ToChecked(); + args.GetReturnValue().Set(stream->SubmitRstStream(code)); + DEBUG_HTTP2STREAM2(stream, "rst_stream code %d sent", code); } -void Http2Session::OnSettings(bool ack) { - Local context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - Local argv[1] = { Boolean::New(isolate, ack) }; - MakeCallback(env()->onsettings_string(), arraysize(argv), argv); +void Http2Stream::Respond(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + + Local headers = args[0].As(); + int options = args[1]->IntegerValue(context).ToChecked(); + + Headers list(isolate, context, headers); + + args.GetReturnValue().Set( + stream->SubmitResponse(*list, list.length(), options)); + DEBUG_HTTP2STREAM(stream, "response submitted"); } -void Http2Session::OnFrameError(int32_t id, uint8_t type, int error_code) { - Local context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - Local argv[3] = { - Integer::New(isolate, id), - Integer::New(isolate, type), - Integer::New(isolate, error_code) - }; - MakeCallback(env()->onframeerror_string(), arraysize(argv), argv); +void Http2Stream::RespondFD(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + + int fd = args[0]->Int32Value(context).ToChecked(); + Local headers = args[1].As(); + + int64_t offset = args[2]->IntegerValue(context).ToChecked(); + int64_t length = args[3]->IntegerValue(context).ToChecked(); + int options = args[4]->IntegerValue(context).ToChecked(); + + stream->session()->SetChunksSinceLastWrite(); + + Headers list(isolate, context, headers); + args.GetReturnValue().Set(stream->SubmitFile(fd, *list, list.length(), + offset, length, options)); + DEBUG_HTTP2STREAM2(stream, "file response submitted for fd %d", fd); } -void Http2Session::OnPriority(int32_t stream, - int32_t parent, - int32_t weight, - int8_t exclusive) { - Local context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - Local argv[4] = { - Integer::New(isolate, stream), - Integer::New(isolate, parent), - Integer::New(isolate, weight), - Boolean::New(isolate, exclusive) - }; - MakeCallback(env()->onpriority_string(), arraysize(argv), argv); +void Http2Stream::Info(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + + Local headers = args[0].As(); + + Headers list(isolate, context, headers); + args.GetReturnValue().Set(stream->SubmitInfo(*list, list.length())); + DEBUG_HTTP2STREAM2(stream, "%d informational headers sent", + headers->Length()); } -void Http2Session::OnGoAway(int32_t lastStreamID, - uint32_t errorCode, - uint8_t* data, - size_t length) { - Local context = env()->context(); - Isolate* isolate = env()->isolate(); - HandleScope scope(isolate); - Context::Scope context_scope(context); - Local argv[3] = { - Integer::NewFromUnsigned(isolate, errorCode), - Integer::New(isolate, lastStreamID), - Undefined(isolate) - }; +void Http2Stream::GetID(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + args.GetReturnValue().Set(stream->id()); +} - if (length > 0) { - argv[2] = Buffer::Copy(isolate, - reinterpret_cast(data), - length).ToLocalChecked(); - } - MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv); +void Http2Stream::Destroy(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + DEBUG_HTTP2STREAM(stream, "destroying stream"); + stream->Destroy(); } -void Http2Session::OnStreamAllocImpl(size_t suggested_size, - uv_buf_t* buf, - void* ctx) { - Http2Session* session = static_cast(ctx); - buf->base = session->stream_alloc(); - buf->len = kAllocBufferSize; + +void Http2Stream::FlushData(const FunctionCallbackInfo& args) { + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + stream->ReadStart(); + DEBUG_HTTP2STREAM(stream, "data flushed to js"); } -void Http2Session::OnStreamReadImpl(ssize_t nread, - const uv_buf_t* bufs, - uv_handle_type pending, - void* ctx) { - Http2Session* session = static_cast(ctx); - if (nread < 0) { - uv_buf_t tmp_buf; - tmp_buf.base = nullptr; - tmp_buf.len = 0; - session->prev_read_cb_.fn(nread, - &tmp_buf, - pending, - session->prev_read_cb_.ctx); - return; - } - if (nread > 0) { - // Only pass data on if nread > 0 - uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) }; - ssize_t ret = session->Write(buf, 1); - if (ret < 0) { - DEBUG_HTTP2("Http2Session: fatal error receiving data: %d\n", ret); - nghttp2_session_terminate_session(session->session(), - NGHTTP2_PROTOCOL_ERROR); - } +void Http2Stream::PushPromise(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Isolate* isolate = env->isolate(); + Http2Stream* parent; + ASSIGN_OR_RETURN_UNWRAP(&parent, args.Holder()); + + Local headers = args[0].As(); + int options = args[1]->IntegerValue(context).ToChecked(); + + Headers list(isolate, context, headers); + + DEBUG_HTTP2STREAM(parent, "creating push promise"); + + int32_t ret = 0; + Http2Stream* stream = parent->SubmitPushPromise(*list, list.length(), + &ret, options); + if (ret <= 0) { + DEBUG_HTTP2STREAM2(parent, "failed to create push stream: %d", ret); + return args.GetReturnValue().Set(ret); } + DEBUG_HTTP2STREAM2(parent, "push stream %d created", stream->id()); + args.GetReturnValue().Set(stream->object()); } -void Http2Session::Consume(Local external) { - DEBUG_HTTP2("Http2Session: consuming socket\n"); -#if defined(DEBUG) && DEBUG - CHECK(prev_alloc_cb_.is_empty()); -#endif - StreamBase* stream = static_cast(external->Value()); -#if defined(DEBUG) && DEBUG - CHECK_NE(stream, nullptr); -#endif - stream->Consume(); - stream_ = stream; - prev_alloc_cb_ = stream->alloc_cb(); - prev_read_cb_ = stream->read_cb(); - stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this }); - stream->set_read_cb({ Http2Session::OnStreamReadImpl, this }); -} +void Http2Stream::Priority(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Local context = env->context(); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + Http2Priority priority(env, args[0], args[1], args[2]); + bool silent = args[3]->BooleanValue(context).ToChecked(); -void Http2Session::Unconsume() { - DEBUG_HTTP2("Http2Session: unconsuming socket\n"); - if (prev_alloc_cb_.is_empty()) - return; - stream_->set_alloc_cb(prev_alloc_cb_); - stream_->set_read_cb(prev_read_cb_); - prev_alloc_cb_.clear(); - prev_read_cb_.clear(); - stream_ = nullptr; + CHECK_EQ(stream->SubmitPriority(*priority, silent), 0); + DEBUG_HTTP2STREAM(stream, "priority submitted"); } -Headers::Headers(Isolate* isolate, - Local context, - Local headers) { -#if defined(DEBUG) && DEBUG - CHECK_EQ(headers->Length(), 2); -#endif - Local header_string = headers->Get(context, 0).ToLocalChecked(); - Local header_count = headers->Get(context, 1).ToLocalChecked(); -#if defined(DEBUG) && DEBUG - CHECK(header_string->IsString()); - CHECK(header_count->IsUint32()); -#endif - count_ = header_count.As()->Value(); - int header_string_len = header_string.As()->Length(); +void Http2Stream::RefreshState(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Stream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); - if (count_ == 0) { - CHECK_EQ(header_string_len, 0); - return; + DEBUG_HTTP2STREAM(stream, "refreshing state"); + + AliasedBuffer& buffer = + env->http2_state()->stream_state_buffer; + + nghttp2_stream* str = **stream; + nghttp2_session* s = **(stream->session()); + + if (str == nullptr) { + buffer[IDX_STREAM_STATE] = NGHTTP2_STREAM_STATE_IDLE; + buffer[IDX_STREAM_STATE_WEIGHT] = + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = 0; + } else { + buffer[IDX_STREAM_STATE] = + nghttp2_stream_get_state(str); + buffer[IDX_STREAM_STATE_WEIGHT] = + nghttp2_stream_get_weight(str); + buffer[IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT] = + nghttp2_stream_get_sum_dependency_weight(str); + buffer[IDX_STREAM_STATE_LOCAL_CLOSE] = + nghttp2_session_get_stream_local_close(s, stream->id()); + buffer[IDX_STREAM_STATE_REMOTE_CLOSE] = + nghttp2_session_get_stream_remote_close(s, stream->id()); + buffer[IDX_STREAM_STATE_LOCAL_WINDOW_SIZE] = + nghttp2_session_get_stream_local_window_size(s, stream->id()); } +} - // Allocate a single buffer with count_ nghttp2_nv structs, followed - // by the raw header data as passed from JS. This looks like: - // | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents | - buf_.AllocateSufficientStorage((alignof(nghttp2_nv) - 1) + - count_ * sizeof(nghttp2_nv) + - header_string_len); - // Make sure the start address is aligned appropriately for an nghttp2_nv*. - char* start = reinterpret_cast( - ROUND_UP(reinterpret_cast(*buf_), alignof(nghttp2_nv))); - char* header_contents = start + (count_ * sizeof(nghttp2_nv)); - nghttp2_nv* const nva = reinterpret_cast(start); +void Http2Session::Ping(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Http2Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); - CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length()); - CHECK_EQ(header_string.As() - ->WriteOneByte(reinterpret_cast(header_contents), - 0, header_string_len, - String::NO_NULL_TERMINATION), - header_string_len); + uint8_t* payload = nullptr; + if (Buffer::HasInstance(args[0])) { + payload = reinterpret_cast(Buffer::Data(args[0])); + CHECK_EQ(Buffer::Length(args[0]), 8); + } - size_t n = 0; - char* p; - for (p = header_contents; p < header_contents + header_string_len; n++) { - if (n >= count_) { - // This can happen if a passed header contained a null byte. In that - // case, just provide nghttp2 with an invalid header to make it reject - // the headers list. - static uint8_t zero = '\0'; - nva[0].name = nva[0].value = &zero; - nva[0].namelen = nva[0].valuelen = 1; - count_ = 1; - return; - } + Http2Session::Http2Ping* ping = new Http2Ping(session); + Local obj = ping->object(); + obj->Set(env->context(), env->ondone_string(), args[1]).FromJust(); - nva[n].flags = NGHTTP2_NV_FLAG_NONE; - nva[n].name = reinterpret_cast(p); - nva[n].namelen = strlen(p); - p += nva[n].namelen + 1; - nva[n].value = reinterpret_cast(p); - nva[n].valuelen = strlen(p); - p += nva[n].valuelen + 1; + if (!session->AddPing(ping)) { + ping->Done(false); + return args.GetReturnValue().Set(false); } -#if defined(DEBUG) && DEBUG - CHECK_EQ(p, header_contents + header_string_len); - CHECK_EQ(n, count_); -#endif + ping->Send(payload); + args.GetReturnValue().Set(true); +} + +Http2Session::Http2Ping* Http2Session::PopPing() { + Http2Ping* ping = nullptr; + if (!outstanding_pings_.empty()) { + ping = outstanding_pings_.front(); + outstanding_pings_.pop(); + } + return ping; +} + +bool Http2Session::AddPing(Http2Session::Http2Ping* ping) { + if (outstanding_pings_.size() == max_outstanding_pings_) + return false; + outstanding_pings_.push(ping); + return true; +} + +Http2Session::Http2Ping::Http2Ping( + Http2Session* session) + : AsyncWrap(session->env(), + session->env()->http2ping_constructor_template() + ->NewInstance(session->env()->context()) + .ToLocalChecked(), + AsyncWrap::PROVIDER_HTTP2PING), + session_(session), + startTime_(uv_hrtime()) { } + +Http2Session::Http2Ping::~Http2Ping() { + if (!object().IsEmpty()) + ClearWrap(object()); + persistent().Reset(); + CHECK(persistent().IsEmpty()); +} + +void Http2Session::Http2Ping::Send(uint8_t* payload) { + uint8_t data[8]; + if (payload == nullptr) { + memcpy(&data, &startTime_, arraysize(data)); + payload = data; + } + CHECK_EQ(nghttp2_submit_ping(**session_, NGHTTP2_FLAG_NONE, payload), 0); } +void Http2Session::Http2Ping::Done(bool ack, const uint8_t* payload) { + uint64_t end = uv_hrtime(); + double duration = (end - startTime_) / 1e6; + + Local buf = Undefined(env()->isolate()); + if (payload != nullptr) { + buf = Buffer::Copy(env()->isolate(), + reinterpret_cast(payload), + 8).ToLocalChecked(); + } + + Local argv[3] = { + Boolean::New(env()->isolate(), ack), + Number::New(env()->isolate(), duration), + buf + }; + MakeCallback(env()->ondone_string(), arraysize(argv), argv); + delete this; +} void Initialize(Local target, Local unused, @@ -1245,60 +2103,58 @@ void Initialize(Local target, Local http2SessionClassName = FIXED_ONE_BYTE_STRING(isolate, "Http2Session"); + Local ping = FunctionTemplate::New(env->isolate()); + ping->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Ping")); + AsyncWrap::AddWrapMethods(env, ping); + Local pingt = ping->InstanceTemplate(); + pingt->SetInternalFieldCount(1); + env->set_http2ping_constructor_template(pingt); + + Local stream = FunctionTemplate::New(env->isolate()); + stream->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream")); + env->SetProtoMethod(stream, "id", Http2Stream::GetID); + env->SetProtoMethod(stream, "destroy", Http2Stream::Destroy); + env->SetProtoMethod(stream, "flushData", Http2Stream::FlushData); + env->SetProtoMethod(stream, "priority", Http2Stream::Priority); + env->SetProtoMethod(stream, "pushPromise", Http2Stream::PushPromise); + env->SetProtoMethod(stream, "info", Http2Stream::Info); + env->SetProtoMethod(stream, "respondFD", Http2Stream::RespondFD); + env->SetProtoMethod(stream, "respond", Http2Stream::Respond); + env->SetProtoMethod(stream, "rstStream", Http2Stream::RstStream); + env->SetProtoMethod(stream, "refreshState", Http2Stream::RefreshState); + AsyncWrap::AddWrapMethods(env, stream); + StreamBase::AddMethods(env, stream, StreamBase::kFlagHasWritev); + Local streamt = stream->InstanceTemplate(); + streamt->SetInternalFieldCount(1); + env->set_http2stream_constructor_template(streamt); + target->Set(context, + FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream"), + stream->GetFunction()).FromJust(); + Local session = env->NewFunctionTemplate(Http2Session::New); session->SetClassName(http2SessionClassName); session->InstanceTemplate()->SetInternalFieldCount(1); AsyncWrap::AddWrapMethods(env, session); - env->SetProtoMethod(session, "consume", - Http2Session::Consume); - env->SetProtoMethod(session, "destroy", - Http2Session::Destroy); - env->SetProtoMethod(session, "destroying", - Http2Session::Destroying); - env->SetProtoMethod(session, "sendHeaders", - Http2Session::SendHeaders); - env->SetProtoMethod(session, "submitShutdownNotice", - Http2Session::SendShutdownNotice); - env->SetProtoMethod(session, "submitGoaway", - Http2Session::SubmitGoaway); - env->SetProtoMethod(session, "submitSettings", - Http2Session::SubmitSettings); - env->SetProtoMethod(session, "submitPushPromise", - Http2Session::SubmitPushPromise); - env->SetProtoMethod(session, "submitRstStream", - Http2Session::SubmitRstStream); - env->SetProtoMethod(session, "submitResponse", - Http2Session::SubmitResponse); - env->SetProtoMethod(session, "submitFile", - Http2Session::SubmitFile); - env->SetProtoMethod(session, "submitRequest", - Http2Session::SubmitRequest); - env->SetProtoMethod(session, "submitPriority", - Http2Session::SubmitPriority); - env->SetProtoMethod(session, "shutdownStream", - Http2Session::ShutdownStream); - env->SetProtoMethod(session, "streamReadStart", - Http2Session::StreamReadStart); - env->SetProtoMethod(session, "streamReadStop", - Http2Session::StreamReadStop); + env->SetProtoMethod(session, "ping", Http2Session::Ping); + env->SetProtoMethod(session, "consume", Http2Session::Consume); + env->SetProtoMethod(session, "destroy", Http2Session::Destroy); + env->SetProtoMethod(session, "destroying", Http2Session::Destroying); + env->SetProtoMethod(session, "shutdownNotice", Http2Session::ShutdownNotice); + env->SetProtoMethod(session, "goaway", Http2Session::Goaway); + env->SetProtoMethod(session, "settings", Http2Session::Settings); + env->SetProtoMethod(session, "request", Http2Session::Request); env->SetProtoMethod(session, "setNextStreamID", Http2Session::SetNextStreamID); - env->SetProtoMethod(session, "destroyStream", - Http2Session::DestroyStream); - env->SetProtoMethod(session, "flushData", - Http2Session::FlushData); env->SetProtoMethod(session, "updateChunksSent", Http2Session::UpdateChunksSent); + env->SetProtoMethod(session, "refreshState", Http2Session::RefreshState); env->SetProtoMethod( - session, "refreshLocalSettings", + session, "localSettings", Http2Session::RefreshSettings); env->SetProtoMethod( - session, "refreshRemoteSettings", + session, "remoteSettings", Http2Session::RefreshSettings); - StreamBase::AddMethods(env, session, - StreamBase::kFlagHasWritev | - StreamBase::kFlagNoShutdown); target->Set(context, http2SessionClassName, session->GetFunction()).FromJust(); @@ -1335,7 +2191,6 @@ void Initialize(Local target, NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NONE); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_NV_FLAG_NO_INDEX); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_DEFERRED); - NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_NOMEM); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_INVALID_ARGUMENT); NODE_DEFINE_HIDDEN_CONSTANT(constants, NGHTTP2_ERR_STREAM_CLOSED); @@ -1386,8 +2241,6 @@ HTTP_STATUS_CODES(V) #undef V env->SetMethod(target, "refreshDefaultSettings", RefreshDefaultSettings); - env->SetMethod(target, "refreshSessionState", RefreshSessionState); - env->SetMethod(target, "refreshStreamState", RefreshStreamState); env->SetMethod(target, "packSettings", PackSettings); target->Set(context, diff --git a/src/node_http2.h b/src/node_http2.h index 8e9f8c536b5..e4b6226e82f 100644 --- a/src/node_http2.h +++ b/src/node_http2.h @@ -3,7 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#include "node_http2_core-inl.h" +#include "nghttp2/nghttp2.h" #include "node_http2_state.h" #include "stream_base-inl.h" #include "string_bytes.h" @@ -19,6 +19,129 @@ using v8::EscapableHandleScope; using v8::Isolate; using v8::MaybeLocal; +#ifdef NODE_DEBUG_HTTP2 + +// Adapted from nghttp2 own debug printer +static inline void _debug_vfprintf(const char* fmt, va_list args) { + vfprintf(stderr, fmt, args); +} + +void inline debug_vfprintf(const char* format, ...) { + va_list args; + va_start(args, format); + _debug_vfprintf(format, args); + va_end(args); +} + +#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__); +#define DEBUG_HTTP2SESSION(session, message) \ + do { \ + DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n", \ + session->TypeName(), \ + session->get_async_id()); \ + } while (0) +#define DEBUG_HTTP2SESSION2(session, message, ...) \ + do { \ + DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n", \ + session->TypeName(), \ + session->get_async_id(), \ + __VA_ARGS__); \ + } while (0) +#define DEBUG_HTTP2STREAM(stream, message) \ + do { \ + DEBUG_HTTP2("Http2Stream %d (%.0lf) [Http2Session %s (%.0lf)] " message \ + "\n", stream->id(), stream->get_async_id(), \ + stream->session()->TypeName(), \ + stream->session()->get_async_id()); \ + } while (0) +#define DEBUG_HTTP2STREAM2(stream, message, ...) \ + do { \ + DEBUG_HTTP2("Http2Stream %d (%.0lf) [Http2Session %s (%.0lf)] " message \ + "\n", stream->id(), stream->get_async_id(), \ + stream->session()->TypeName(), \ + stream->session()->get_async_id(), \ + __VA_ARGS__); \ + } while (0) +#else +#define DEBUG_HTTP2(...) do {} while (0) +#define DEBUG_HTTP2SESSION(...) do {} while (0) +#define DEBUG_HTTP2SESSION2(...) do {} while (0) +#define DEBUG_HTTP2STREAM(...) do {} while (0) +#define DEBUG_HTTP2STREAM2(...) do {} while (0) +#endif + +#define DEFAULT_MAX_PINGS 10 +#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096 +#define DEFAULT_SETTINGS_ENABLE_PUSH 1 +#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535 +#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384 +#define DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE 65535 +#define MAX_MAX_FRAME_SIZE 16777215 +#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE +#define MAX_INITIAL_WINDOW_SIZE 2147483647 + +#define MAX_MAX_HEADER_LIST_SIZE 16777215u +#define DEFAULT_MAX_HEADER_LIST_PAIRS 128u + +struct nghttp2_stream_write_t; + +#define MAX_BUFFER_COUNT 16 + +enum nghttp2_session_type { + NGHTTP2_SESSION_SERVER, + NGHTTP2_SESSION_CLIENT +}; + +enum nghttp2_shutdown_flags { + NGHTTP2_SHUTDOWN_FLAG_GRACEFUL +}; + +enum nghttp2_stream_flags { + NGHTTP2_STREAM_FLAG_NONE = 0x0, + // Writable side has ended + NGHTTP2_STREAM_FLAG_SHUT = 0x1, + // Reading has started + NGHTTP2_STREAM_FLAG_READ_START = 0x2, + // Reading is paused + NGHTTP2_STREAM_FLAG_READ_PAUSED = 0x4, + // Stream is closed + NGHTTP2_STREAM_FLAG_CLOSED = 0x8, + // Stream is destroyed + NGHTTP2_STREAM_FLAG_DESTROYED = 0x10, + // Stream has trailers + NGHTTP2_STREAM_FLAG_TRAILERS = 0x20 +}; + +enum nghttp2_stream_options { + STREAM_OPTION_EMPTY_PAYLOAD = 0x1, + STREAM_OPTION_GET_TRAILERS = 0x2, +}; + +// Callbacks +typedef void (*nghttp2_stream_write_cb)( + nghttp2_stream_write_t* req, + int status); + +struct nghttp2_stream_write { + unsigned int nbufs = 0; + nghttp2_stream_write_t* req = nullptr; + nghttp2_stream_write_cb cb = nullptr; + MaybeStackBuffer bufs; +}; + +struct nghttp2_header { + nghttp2_rcbuf* name = nullptr; + nghttp2_rcbuf* value = nullptr; + uint8_t flags = 0; +}; + + + +struct nghttp2_stream_write_t { + void* data; + int status; +}; + // Unlike the HTTP/1 implementation, the HTTP/2 implementation is not limited // to a fixed number of known supported HTTP methods. These constants, therefore // are provided strictly as a convenience to users and are exposed via the @@ -292,6 +415,11 @@ const char* nghttp2_errname(int rv) { } } +enum session_state_flags { + SESSION_STATE_NONE = 0x0, + SESSION_STATE_DESTROYING = 0x1 +}; + // This allows for 4 default-sized frames with their frame headers static const size_t kAllocBufferSize = 4 * (16384 + 9); @@ -299,6 +427,7 @@ typedef uint32_t(*get_setting)(nghttp2_session* session, nghttp2_settings_id id); class Http2Session; +class Http2Stream; // The Http2Options class is used to parse the options object passed in to // a Http2Session object and convert those into an appropriate nghttp2_option @@ -332,10 +461,19 @@ class Http2Options { return padding_strategy_; } + void SetMaxOutstandingPings(size_t max) { + max_outstanding_pings_ = max; + } + + size_t GetMaxOutstandingPings() { + return max_outstanding_pings_; + } + private: nghttp2_option* options_; uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE; + size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS; }; // The Http2Settings class is used to parse the settings passed in for @@ -382,82 +520,133 @@ class Http2Priority { nghttp2_priority_spec spec; }; -class Http2Session : public AsyncWrap, - public StreamBase, - public Nghttp2Session { +class Http2Stream : public AsyncWrap, + public StreamBase { public: - Http2Session(Environment* env, - Local wrap, - nghttp2_session_type type); - ~Http2Session() override; + Http2Stream(Http2Session* session, + int32_t id, + nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS, + int options = 0); + ~Http2Stream() override; - static void OnStreamAllocImpl(size_t suggested_size, - uv_buf_t* buf, - void* ctx); - static void OnStreamReadImpl(ssize_t nread, - const uv_buf_t* bufs, - uv_handle_type pending, - void* ctx); + nghttp2_stream* operator*(); - protected: - ssize_t OnMaxFrameSizePadding(size_t frameLength, - size_t maxPayloadLen); + Http2Session* session() { return session_; } + + // Queue outbound chunks of data to be sent on this stream + inline int Write( + nghttp2_stream_write_t* req, + const uv_buf_t bufs[], + unsigned int nbufs, + nghttp2_stream_write_cb cb); + + inline void AddChunk(const uint8_t* data, size_t len); + + inline void FlushDataChunks(); + + // Process a Data Chunk + void OnDataChunk(uv_buf_t* chunk); + + + // Required for StreamBase + int ReadStart() override; + + // Required for StreamBase + int ReadStop() override; + + // Required for StreamBase + int DoShutdown(ShutdownWrap* req_wrap) override; + + // Initiate a response on this stream. + inline int SubmitResponse(nghttp2_nv* nva, + size_t len, + int options); + + // Send data read from a file descriptor as the response on this stream. + inline int SubmitFile(int fd, + nghttp2_nv* nva, size_t len, + int64_t offset, + int64_t length, + int options); + + // Submit informational headers for this stream + inline int SubmitInfo(nghttp2_nv* nva, size_t len); + + // Submit a PRIORITY frame for this stream + inline int SubmitPriority(nghttp2_priority_spec* prispec, + bool silent = false); + + // Submits an RST_STREAM frame using the given code + inline int SubmitRstStream(const uint32_t code); + + // Submits a PUSH_PROMISE frame with this stream as the parent. + inline Http2Stream* SubmitPushPromise( + nghttp2_nv* nva, + size_t len, + int32_t* ret, + int options = 0); + + + inline void Close(int32_t code); - ssize_t OnCallbackPadding(size_t frame, - size_t maxPayloadLen); + // Shutdown the writable side of the stream + inline void Shutdown(); - bool HasGetPaddingCallback() override { - return padding_strategy_ == PADDING_STRATEGY_MAX || - padding_strategy_ == PADDING_STRATEGY_CALLBACK; + // Destroy this stream instance and free all held memory. + inline void Destroy(); + + inline bool IsDestroyed() const { + return flags_ & NGHTTP2_STREAM_FLAG_DESTROYED; + } + + inline bool IsWritable() const { + return !(flags_ & NGHTTP2_STREAM_FLAG_SHUT); } - ssize_t GetPadding(size_t frameLength, size_t maxPayloadLen) override { - if (padding_strategy_ == PADDING_STRATEGY_MAX) { - return OnMaxFrameSizePadding(frameLength, maxPayloadLen); + inline bool IsPaused() const { + return flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED; + } + + inline bool IsClosed() const { + return flags_ & NGHTTP2_STREAM_FLAG_CLOSED; } -#if defined(DEBUG) && DEBUG - CHECK_EQ(padding_strategy_, PADDING_STRATEGY_CALLBACK); -#endif + inline bool HasTrailers() const { + return flags_ & NGHTTP2_STREAM_FLAG_TRAILERS; + } - return OnCallbackPadding(frameLength, maxPayloadLen); - } - - void OnHeaders( - Nghttp2Stream* stream, - nghttp2_header* headers, - size_t count, - nghttp2_headers_category cat, - uint8_t flags) override; - void OnStreamClose(int32_t id, uint32_t code) override; - void OnDataChunk(Nghttp2Stream* stream, uv_buf_t* chunk) override; - void OnSettings(bool ack) override; - void OnPriority(int32_t stream, - int32_t parent, - int32_t weight, - int8_t exclusive) override; - void OnGoAway(int32_t lastStreamID, - uint32_t errorCode, - uint8_t* data, - size_t length) override; - void OnFrameError(int32_t id, uint8_t type, int error_code) override; - void OnTrailers(Nghttp2Stream* stream, - const SubmitTrailers& submit_trailers) override; - - void Send(WriteWrap* req, char* buf, size_t length) override; - WriteWrap* AllocateSend() override; + // Returns true if this stream is in the reading state, which occurs when + // the NGHTTP2_STREAM_FLAG_READ_START flag has been set and the + // NGHTTP2_STREAM_FLAG_READ_PAUSED flag is *not* set. + inline bool IsReading() const { + return flags_ & NGHTTP2_STREAM_FLAG_READ_START && + !(flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED); + } - int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count, - uv_stream_t* send_handle) override; + // Returns the RST_STREAM code used to close this stream + inline int32_t code() const { return code_; } + + // Returns the stream identifier for this stream + inline int32_t id() const { return id_; } - AsyncWrap* GetAsyncWrap() override { - return static_cast(this); + inline bool AddHeader(nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags); + + inline nghttp2_header* headers() { + return current_headers_.data(); + } + + inline nghttp2_headers_category headers_category() const { + return current_headers_category_; } - void* Cast() override { - return reinterpret_cast(this); + inline size_t headers_count() const { + return current_headers_.size(); } + void StartHeaders(nghttp2_headers_category category); + // Required for StreamBase bool IsAlive() override { return true; @@ -468,47 +657,215 @@ class Http2Session : public AsyncWrap, return false; } - // Required for StreamBase - int ReadStart() override { return 0; } + AsyncWrap* GetAsyncWrap() override { return static_cast(this); } + void* Cast() override { return reinterpret_cast(this); } - // Required for StreamBase - int ReadStop() override { return 0; } + int DoWrite(WriteWrap* w, uv_buf_t* bufs, size_t count, + uv_stream_t* send_handle) override; - // Required for StreamBase - int DoShutdown(ShutdownWrap* req_wrap) override { - return 0; - } + size_t self_size() const override { return sizeof(*this); } - uv_loop_t* event_loop() const override { - return env()->event_loop(); + // Handling Trailer Headers + class SubmitTrailers { + public: + inline void Submit(nghttp2_nv* trailers, size_t length) const; + + inline SubmitTrailers(Http2Session* sesion, + Http2Stream* stream, + uint32_t* flags); + + private: + Http2Session* const session_; + Http2Stream* const stream_; + uint32_t* const flags_; + + friend class Http2Stream; + }; + + void OnTrailers(const SubmitTrailers& submit_trailers); + + // JavaScript API + static void GetID(const FunctionCallbackInfo& args); + static void Destroy(const FunctionCallbackInfo& args); + static void FlushData(const FunctionCallbackInfo& args); + static void Priority(const FunctionCallbackInfo& args); + static void PushPromise(const FunctionCallbackInfo& args); + static void RefreshState(const FunctionCallbackInfo& args); + static void Info(const FunctionCallbackInfo& args); + static void RespondFD(const FunctionCallbackInfo& args); + static void Respond(const FunctionCallbackInfo& args); + static void RstStream(const FunctionCallbackInfo& args); + + class Provider; + + private: + Http2Session* session_; // The Parent HTTP/2 Session + int32_t id_; // The Stream Identifier + int32_t code_ = NGHTTP2_NO_ERROR; // The RST_STREAM code (if any) + int flags_ = NGHTTP2_STREAM_FLAG_NONE; // Internal state flags + + uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; + uint32_t max_header_length_ = DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE; + + // The Current Headers block... As headers are received for this stream, + // they are temporarily stored here until the OnFrameReceived is called + // signalling the end of the HEADERS frame + nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; + uint32_t current_headers_length_ = 0; // total number of octets + std::vector current_headers_; + + // Inbound Data... This is the data received via DATA frames for this stream. + std::queue data_chunks_; + + // Outbound Data... This is the data written by the JS layer that is + // waiting to be written out to the socket. + std::queue queue_; + unsigned int queue_index_ = 0; + size_t queue_offset_ = 0; + int64_t fd_offset_ = 0; + int64_t fd_length_ = -1; +}; + +class Http2Stream::Provider { + public: + Provider(Http2Stream* stream, int options); + explicit Provider(int options); + virtual ~Provider(); + + nghttp2_data_provider* operator*() { + return !empty_ ? &provider_ : nullptr; } + + class FD; + class Stream; + protected: + nghttp2_data_provider provider_; + + private: + bool empty_ = false; +}; + +class Http2Stream::Provider::FD : public Http2Stream::Provider { + public: + FD(int options, int fd); + FD(Http2Stream* stream, int options, int fd); + + static ssize_t OnRead(nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data); +}; + +class Http2Stream::Provider::Stream : public Http2Stream::Provider { public: + Stream(Http2Stream* stream, int options); + explicit Stream(int options); + + static ssize_t OnRead(nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data); +}; + + +class Http2Session : public AsyncWrap { + public: + Http2Session(Environment* env, + Local wrap, + nghttp2_session_type type = NGHTTP2_SESSION_SERVER); + ~Http2Session() override; + + class Http2Ping; + + void Start(); + void Stop(); + void Close(); void Consume(Local external); void Unconsume(); + bool Ping(v8::Local function); + + inline void SendPendingData(); + + // Submits a new request. If the request is a success, assigned + // will be a pointer to the Http2Stream instance assigned. + // This only works if the session is a client session. + inline Http2Stream* SubmitRequest( + nghttp2_priority_spec* prispec, + nghttp2_nv* nva, + size_t len, + int32_t* ret, + int options = 0); + + nghttp2_session_type type() const { return session_type_; } + + inline nghttp2_session* session() const { return session_; } + + nghttp2_session* operator*() { return session_; } + + uint32_t GetMaxHeaderPairs() const { return max_header_pairs_; } + + inline const char* TypeName(); + + inline void MarkDestroying() { flags_ |= SESSION_STATE_DESTROYING; } + inline bool IsDestroying() { return flags_ & SESSION_STATE_DESTROYING; } + + // Returns pointer to the stream, or nullptr if stream does not exist + inline Http2Stream* FindStream(int32_t id); + + // Adds a stream instance to this session + inline void AddStream(Http2Stream* stream); + + // Removes a stream instance from this session + inline void RemoveStream(int32_t id); + + // Sends a notice to the connected peer that the session is shutting down. + inline void SubmitShutdownNotice(); + + // Submits a SETTINGS frame to the connected peer. + inline void Settings(const nghttp2_settings_entry iv[], size_t niv); + + // Write data to the session + inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs); + + inline void SetChunksSinceLastWrite(size_t n = 0); + + size_t self_size() const override { return sizeof(*this); } + + char* stream_alloc() { + return stream_buf_; + } + + inline void GetTrailers(Http2Stream* stream, uint32_t* flags); + + static void OnStreamAllocImpl(size_t suggested_size, + uv_buf_t* buf, + void* ctx); + static void OnStreamReadImpl(ssize_t nread, + const uv_buf_t* bufs, + uv_handle_type pending, + void* ctx); + + // The JavaScript API static void New(const FunctionCallbackInfo& args); static void Consume(const FunctionCallbackInfo& args); static void Unconsume(const FunctionCallbackInfo& args); static void Destroying(const FunctionCallbackInfo& args); static void Destroy(const FunctionCallbackInfo& args); - static void SubmitSettings(const FunctionCallbackInfo& args); - static void SubmitRstStream(const FunctionCallbackInfo& args); - static void SubmitResponse(const FunctionCallbackInfo& args); - static void SubmitFile(const FunctionCallbackInfo& args); - static void SubmitRequest(const FunctionCallbackInfo& args); - static void SubmitPushPromise(const FunctionCallbackInfo& args); - static void SubmitPriority(const FunctionCallbackInfo& args); - static void SendHeaders(const FunctionCallbackInfo& args); - static void ShutdownStream(const FunctionCallbackInfo& args); - static void StreamWrite(const FunctionCallbackInfo& args); - static void StreamReadStart(const FunctionCallbackInfo& args); - static void StreamReadStop(const FunctionCallbackInfo& args); + static void Settings(const FunctionCallbackInfo& args); + static void Request(const FunctionCallbackInfo& args); static void SetNextStreamID(const FunctionCallbackInfo& args); - static void SendShutdownNotice(const FunctionCallbackInfo& args); - static void SubmitGoaway(const FunctionCallbackInfo& args); - static void DestroyStream(const FunctionCallbackInfo& args); - static void FlushData(const FunctionCallbackInfo& args); + static void ShutdownNotice(const FunctionCallbackInfo& args); + static void Goaway(const FunctionCallbackInfo& args); static void UpdateChunksSent(const FunctionCallbackInfo& args); + static void RefreshState(const FunctionCallbackInfo& args); + static void Ping(const FunctionCallbackInfo& args); template static void RefreshSettings(const FunctionCallbackInfo& args); @@ -516,17 +873,125 @@ class Http2Session : public AsyncWrap, template static void GetSettings(const FunctionCallbackInfo& args); - size_t self_size() const override { - return sizeof(*this); - } + void Send(WriteWrap* req, char* buf, size_t length); + WriteWrap* AllocateSend(); - char* stream_alloc() { - return stream_buf_; + uv_loop_t* event_loop() const { + return env()->event_loop(); } - void Close() override; + Http2Ping* PopPing(); + bool AddPing(Http2Ping* ping); private: + // Frame Padding Strategies + inline ssize_t OnMaxFrameSizePadding(size_t frameLength, + size_t maxPayloadLen); + inline ssize_t OnCallbackPadding(size_t frame, + size_t maxPayloadLen); + + // Frame Handler + inline void HandleDataFrame(const nghttp2_frame* frame); + inline void HandleGoawayFrame(const nghttp2_frame* frame); + inline void HandleHeadersFrame(const nghttp2_frame* frame); + inline void HandlePriorityFrame(const nghttp2_frame* frame); + inline void HandleSettingsFrame(const nghttp2_frame* frame); + inline void HandlePingFrame(const nghttp2_frame* frame); + + // nghttp2 callbacks + static inline int OnBeginHeadersCallback( + nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data); + static inline int OnHeaderCallback( + nghttp2_session* session, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data); + static inline int OnFrameReceive( + nghttp2_session* session, + const nghttp2_frame* frame, + void* user_data); + static inline int OnFrameNotSent( + nghttp2_session* session, + const nghttp2_frame* frame, + int error_code, + void* user_data); + static inline int OnStreamClose( + nghttp2_session* session, + int32_t id, + uint32_t code, + void* user_data); + static inline int OnInvalidHeader( + nghttp2_session* session, + const nghttp2_frame* frame, + nghttp2_rcbuf* name, + nghttp2_rcbuf* value, + uint8_t flags, + void* user_data); + static inline int OnDataChunkReceived( + nghttp2_session* session, + uint8_t flags, + int32_t id, + const uint8_t* data, + size_t len, + void* user_data); + static inline ssize_t OnSelectPadding( + nghttp2_session* session, + const nghttp2_frame* frame, + size_t maxPayloadLen, + void* user_data); + static inline int OnNghttpError( + nghttp2_session* session, + const char* message, + size_t len, + void* user_data); + + + static inline ssize_t OnStreamReadFD( + nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data); + static inline ssize_t OnStreamRead( + nghttp2_session* session, + int32_t id, + uint8_t* buf, + size_t length, + uint32_t* flags, + nghttp2_data_source* source, + void* user_data); + + struct Callbacks { + inline explicit Callbacks(bool kHasGetPaddingCallback); + inline ~Callbacks(); + + nghttp2_session_callbacks* callbacks; + }; + + /* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */ + static const Callbacks callback_struct_saved[2]; + + // The underlying nghttp2_session handle + nghttp2_session* session_; + + // The session type: client or server + nghttp2_session_type session_type_; + + // The maximum number of header pairs permitted for streams on this session + uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; + + // The collection of active Http2Streams associated with this session + std::unordered_map streams_; + + int flags_ = SESSION_STATE_NONE; + + // The StreamBase instance being used for i/o StreamBase* stream_; StreamResource::Callback prev_alloc_cb_; StreamResource::Callback prev_read_cb_; @@ -534,9 +999,27 @@ class Http2Session : public AsyncWrap, // use this to allow timeout tracking during long-lasting writes uint32_t chunks_sent_since_last_write_ = 0; - uv_prepare_t* prep_ = nullptr; + uv_prepare_t* prep_ = nullptr; char stream_buf_[kAllocBufferSize]; + + size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS; + std::queue outstanding_pings_; +}; + +class Http2Session::Http2Ping : public AsyncWrap { + public: + explicit Http2Ping(Http2Session* session); + ~Http2Ping(); + + size_t self_size() const override { return sizeof(*this); } + + void Send(uint8_t* payload); + void Done(bool ack, const uint8_t* payload = nullptr); + + private: + Http2Session* session_; + uint64_t startTime_; }; class ExternalHeader : diff --git a/src/node_http2_core-inl.h b/src/node_http2_core-inl.h deleted file mode 100644 index d7b919f82e2..00000000000 --- a/src/node_http2_core-inl.h +++ /dev/null @@ -1,933 +0,0 @@ -#ifndef SRC_NODE_HTTP2_CORE_INL_H_ -#define SRC_NODE_HTTP2_CORE_INL_H_ - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include "node_http2_core.h" -#include "node_internals.h" // arraysize - -// min and max are defined in the PAL, which interferes -// with 's std::min and std::max -#ifdef NODE_ENGINE_CHAKRACORE -#undef min -#undef max -#endif - -#include - -namespace node { -namespace http2 { - -#ifdef NODE_DEBUG_HTTP2 -inline int Nghttp2Session::OnNghttpError(nghttp2_session* session, - const char* message, - size_t len, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: Error '%.*s'\n", - handle->TypeName(), len, message); - return 0; -} -#endif - -inline int32_t GetFrameID(const nghttp2_frame* frame) { - // If this is a push promise, we want to grab the id of the promised stream - return (frame->hd.type == NGHTTP2_PUSH_PROMISE) ? - frame->push_promise.promised_stream_id : - frame->hd.stream_id; -} - -// nghttp2 calls this at the beginning a new HEADERS or PUSH_PROMISE frame. -// We use it to ensure that an Nghttp2Stream instance is allocated to store -// the state. -inline int Nghttp2Session::OnBeginHeadersCallback(nghttp2_session* session, - const nghttp2_frame* frame, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - int32_t id = GetFrameID(frame); - DEBUG_HTTP2("Nghttp2Session %s: beginning headers for stream %d\n", - handle->TypeName(), id); - - Nghttp2Stream* stream = handle->FindStream(id); - if (stream == nullptr) { - new Nghttp2Stream(id, handle, frame->headers.cat); - } else { - stream->StartHeaders(frame->headers.cat); - } - return 0; -} - -inline size_t GetBufferLength(nghttp2_rcbuf* buf) { - return nghttp2_rcbuf_get_buf(buf).len; -} - -inline bool Nghttp2Stream::AddHeader(nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags) { - size_t length = GetBufferLength(name) + GetBufferLength(value) + 32; - if (current_headers_.size() == max_header_pairs_ || - current_headers_length_ + length > max_header_length_) { - return false; - } - nghttp2_header header; - header.name = name; - header.value = value; - header.flags = flags; - current_headers_.push_back(header); - nghttp2_rcbuf_incref(name); - nghttp2_rcbuf_incref(value); - current_headers_length_ += length; - return true; -} - -// nghttp2 calls this once for every header name-value pair in a HEADERS -// or PUSH_PROMISE block. CONTINUATION frames are handled automatically -// and transparently so we do not need to worry about those at all. -inline int Nghttp2Session::OnHeaderCallback(nghttp2_session* session, - const nghttp2_frame* frame, - nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - int32_t id = GetFrameID(frame); - Nghttp2Stream* stream = handle->FindStream(id); - if (!stream->AddHeader(name, value, flags)) { - // This will only happen if the connected peer sends us more - // than the allowed number of header items at any given time - stream->SubmitRstStream(NGHTTP2_ENHANCE_YOUR_CALM); - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - } - return 0; -} - - -// When nghttp2 has completely processed a frame, it calls OnFrameReceive. -// It is our responsibility to delegate out from there. We can ignore most -// control frames since nghttp2 will handle those for us. -inline int Nghttp2Session::OnFrameReceive(nghttp2_session* session, - const nghttp2_frame* frame, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: complete frame received: type: %d\n", - handle->TypeName(), frame->hd.type); - bool ack; - switch (frame->hd.type) { - case NGHTTP2_DATA: - handle->HandleDataFrame(frame); - break; - case NGHTTP2_PUSH_PROMISE: - // Intentional fall-through, handled just like headers frames - case NGHTTP2_HEADERS: - handle->HandleHeadersFrame(frame); - break; - case NGHTTP2_SETTINGS: - ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) == NGHTTP2_FLAG_ACK; - handle->OnSettings(ack); - break; - case NGHTTP2_PRIORITY: - handle->HandlePriorityFrame(frame); - break; - case NGHTTP2_GOAWAY: - handle->HandleGoawayFrame(frame); - break; - default: - break; - } - return 0; -} - -// nghttp2 will call this if an error occurs attempting to send a frame. -// Unless the stream or session is closed, this really should not happen -// unless there is a serious flaw in our implementation. -inline int Nghttp2Session::OnFrameNotSent(nghttp2_session* session, - const nghttp2_frame* frame, - int error_code, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: frame type %d was not sent, code: %d\n", - handle->TypeName(), frame->hd.type, error_code); - // Do not report if the frame was not sent due to the session closing - if (error_code != NGHTTP2_ERR_SESSION_CLOSING && - error_code != NGHTTP2_ERR_STREAM_CLOSED && - error_code != NGHTTP2_ERR_STREAM_CLOSING) { - handle->OnFrameError(frame->hd.stream_id, - frame->hd.type, - error_code); - } - return 0; -} - -inline int Nghttp2Session::OnInvalidHeader(nghttp2_session* session, - const nghttp2_frame* frame, - nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags, - void* user_data) { - // Ignore invalid header fields by default. - return 0; -} - -// Called when nghttp2 closes a stream, either in response to an RST_STREAM -// frame or the stream closing naturally on it's own -inline int Nghttp2Session::OnStreamClose(nghttp2_session* session, - int32_t id, - uint32_t code, - void* user_data) { - Nghttp2Session*handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: stream %d closed, code: %d\n", - handle->TypeName(), id, code); - Nghttp2Stream* stream = handle->FindStream(id); - // Intentionally ignore the callback if the stream does not exist - if (stream != nullptr) - stream->Close(code); - return 0; -} - -// Called by nghttp2 to collect the data while a file response is sent. -// The buf is the DATA frame buffer that needs to be filled with at most -// length bytes. flags is used to control what nghttp2 does next. -inline ssize_t Nghttp2Session::OnStreamReadFD(nghttp2_session* session, - int32_t id, - uint8_t* buf, - size_t length, - uint32_t* flags, - nghttp2_data_source* source, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: reading outbound file data for stream %d\n", - handle->TypeName(), id); - Nghttp2Stream* stream = handle->FindStream(id); - - int fd = source->fd; - int64_t offset = stream->fd_offset_; - ssize_t numchars = 0; - - if (stream->fd_length_ >= 0 && - stream->fd_length_ < static_cast(length)) - length = stream->fd_length_; - - uv_buf_t data; - data.base = reinterpret_cast(buf); - data.len = length; - - uv_fs_t read_req; - - if (length > 0) { - // TODO(addaleax): Never use synchronous I/O on the main thread. - numchars = uv_fs_read(handle->event_loop(), - &read_req, - fd, &data, 1, - offset, nullptr); - uv_fs_req_cleanup(&read_req); - } - - // Close the stream with an error if reading fails - if (numchars < 0) - return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; - - // Update the read offset for the next read - stream->fd_offset_ += numchars; - stream->fd_length_ -= numchars; - - // if numchars < length, assume that we are done. - if (static_cast(numchars) < length || length <= 0) { - DEBUG_HTTP2("Nghttp2Session %s: no more data for stream %d\n", - handle->TypeName(), id); - *flags |= NGHTTP2_DATA_FLAG_EOF; - GetTrailers(session, handle, stream, flags); - } - - return numchars; -} - -// Called by nghttp2 to collect the data to pack within a DATA frame. -// The buf is the DATA frame buffer that needs to be filled with at most -// length bytes. flags is used to control what nghttp2 does next. -inline ssize_t Nghttp2Session::OnStreamRead(nghttp2_session* session, - int32_t id, - uint8_t* buf, - size_t length, - uint32_t* flags, - nghttp2_data_source* source, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: reading outbound data for stream %d\n", - handle->TypeName(), id); - Nghttp2Stream* stream = handle->FindStream(id); - size_t remaining = length; - size_t offset = 0; - - // While there is data in the queue, copy data into buf until it is full. - // There may be data left over, which will be sent the next time nghttp - // calls this callback. - while (!stream->queue_.empty()) { - DEBUG_HTTP2("Nghttp2Session %s: processing outbound data chunk\n", - handle->TypeName()); - nghttp2_stream_write* head = stream->queue_.front(); - while (stream->queue_index_ < head->nbufs) { - if (remaining == 0) - goto end; - - unsigned int n = stream->queue_index_; - // len is the number of bytes in head->bufs[n] that are yet to be written - size_t len = head->bufs[n].len - stream->queue_offset_; - size_t bytes_to_write = len < remaining ? len : remaining; - memcpy(buf + offset, - head->bufs[n].base + stream->queue_offset_, - bytes_to_write); - offset += bytes_to_write; - remaining -= bytes_to_write; - if (bytes_to_write < len) { - stream->queue_offset_ += bytes_to_write; - } else { - stream->queue_index_++; - stream->queue_offset_ = 0; - } - } - stream->queue_offset_ = 0; - stream->queue_index_ = 0; - head->cb(head->req, 0); - delete head; - stream->queue_.pop(); - } - -end: - // If we are no longer writable and there is no more data in the queue, - // then we need to set the NGHTTP2_DATA_FLAG_EOF flag. - // If we are still writable but there is not yet any data to send, set the - // NGHTTP2_ERR_DEFERRED flag. This will put the stream into a pending state - // that will wait for data to become available. - // If neither of these flags are set, then nghttp2 will call this callback - // again to get the data for the next DATA frame. - int writable = !stream->queue_.empty() || stream->IsWritable(); - if (offset == 0 && writable && stream->queue_.empty()) { - DEBUG_HTTP2("Nghttp2Session %s: deferring stream %d\n", - handle->TypeName(), id); - return NGHTTP2_ERR_DEFERRED; - } - if (!writable) { - DEBUG_HTTP2("Nghttp2Session %s: no more data for stream %d\n", - handle->TypeName(), id); - *flags |= NGHTTP2_DATA_FLAG_EOF; - - GetTrailers(session, handle, stream, flags); - } -#if defined(DEBUG) && DEBUG - CHECK(offset <= length); -#endif - return offset; -} - -// Called by nghttp2 when it needs to determine how much padding to apply -// to a DATA or HEADERS frame -inline ssize_t Nghttp2Session::OnSelectPadding(nghttp2_session* session, - const nghttp2_frame* frame, - size_t maxPayloadLen, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); -#if defined(DEBUG) && DEBUG - CHECK(handle->HasGetPaddingCallback()); -#endif - ssize_t padding = handle->GetPadding(frame->hd.length, maxPayloadLen); - DEBUG_HTTP2("Nghttp2Session %s: using padding, size: %d\n", - handle->TypeName(), padding); - return padding; -} - -// While nghttp2 is processing a DATA frame, it will call the -// OnDataChunkReceived callback multiple times, passing along individual -// chunks of data from the DATA frame payload. These *must* be memcpy'd -// out because the pointer to the data will quickly become invalid. -inline int Nghttp2Session::OnDataChunkReceived(nghttp2_session* session, - uint8_t flags, - int32_t id, - const uint8_t* data, - size_t len, - void* user_data) { - Nghttp2Session* handle = static_cast(user_data); - DEBUG_HTTP2("Nghttp2Session %s: buffering data chunk for stream %d, size: " - "%d, flags: %d\n", handle->TypeName(), - id, len, flags); - // We should never actually get a 0-length chunk so this check is - // only a precaution at this point. - if (len > 0) { - nghttp2_session_consume_connection(session, len); - Nghttp2Stream* stream = handle->FindStream(id); - char* buf = Malloc(len); - memcpy(buf, data, len); - stream->data_chunks_.emplace(uv_buf_init(buf, len)); - } - return 0; -} - -// Only when we are done sending the last chunk of data do we check for -// any trailing headers that are to be sent. This is the only opportunity -// we have to make this check. If there are trailers, then the -// NGHTTP2_DATA_FLAG_NO_END_STREAM flag must be set. -inline void Nghttp2Session::GetTrailers(nghttp2_session* session, - Nghttp2Session* handle, - Nghttp2Stream* stream, - uint32_t* flags) { - if (stream->GetTrailers()) { - SubmitTrailers submit_trailers{handle, stream, flags}; - handle->OnTrailers(stream, submit_trailers); - } -} - -// Submits any trailing header fields that have been collected -inline void Nghttp2Session::SubmitTrailers::Submit(nghttp2_nv* trailers, - size_t length) const { - if (length == 0) - return; - DEBUG_HTTP2("Nghttp2Session %s: sending trailers for stream %d, " - "count: %d\n", handle_->TypeName(), - stream_->id(), length); - *flags_ |= NGHTTP2_DATA_FLAG_NO_END_STREAM; - nghttp2_submit_trailer(handle_->session_, - stream_->id(), - trailers, - length); -} - -// Submits a graceful shutdown notice to nghttp -// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html -inline void Nghttp2Session::SubmitShutdownNotice() { - DEBUG_HTTP2("Nghttp2Session %s: submitting shutdown notice\n", - TypeName()); - nghttp2_submit_shutdown_notice(session_); -} - -// Sends a SETTINGS frame on the current session -// Note that this *should* send a SETTINGS frame even if niv == 0 and there -// are no settings entries to send. -inline int Nghttp2Session::SubmitSettings(const nghttp2_settings_entry iv[], - size_t niv) { - DEBUG_HTTP2("Nghttp2Session %s: submitting settings, count: %d\n", - TypeName(), niv); - return nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv); -} - -// Returns the Nghttp2Stream associated with the given id, or nullptr if none -inline Nghttp2Stream* Nghttp2Session::FindStream(int32_t id) { - auto s = streams_.find(id); - if (s != streams_.end()) { - DEBUG_HTTP2("Nghttp2Session %s: stream %d found\n", - TypeName(), id); - return s->second; - } else { - DEBUG_HTTP2("Nghttp2Session %s: stream %d not found\n", TypeName(), id); - return nullptr; - } -} - -// Flushes one buffered data chunk at a time. -inline void Nghttp2Stream::FlushDataChunks() { - if (!data_chunks_.empty()) { - uv_buf_t buf = data_chunks_.front(); - data_chunks_.pop(); - if (buf.len > 0) { - nghttp2_session_consume_stream(session_->session(), id_, buf.len); - session_->OnDataChunk(this, &buf); - } else { - session_->OnDataChunk(this, nullptr); - } - } -} - -// Called when a DATA frame has been completely processed. Will check to -// see if the END_STREAM flag is set, and will flush the queued data chunks -// to JS if the stream is flowing -inline void Nghttp2Session::HandleDataFrame(const nghttp2_frame* frame) { - int32_t id = GetFrameID(frame); - DEBUG_HTTP2("Nghttp2Session %s: handling data frame for stream %d\n", - TypeName(), id); - Nghttp2Stream* stream = this->FindStream(id); - // If the stream does not exist, something really bad happened -#if defined(DEBUG) && DEBUG - CHECK_NE(stream, nullptr); -#endif - if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) - stream->data_chunks_.emplace(uv_buf_init(0, 0)); - if (stream->IsReading()) - stream->FlushDataChunks(); -} - -// Passes all of the collected headers for a HEADERS frame out to the JS layer. -// The headers are collected as the frame is being processed and sent out -// to the JS side only when the frame is fully processed. -inline void Nghttp2Session::HandleHeadersFrame(const nghttp2_frame* frame) { - int32_t id = GetFrameID(frame); - DEBUG_HTTP2("Nghttp2Session %s: handling headers frame for stream %d\n", - TypeName(), id); - Nghttp2Stream* stream = FindStream(id); - // If the stream does not exist, something really bad happened -#if defined(DEBUG) && DEBUG - CHECK_NE(stream, nullptr); -#endif - OnHeaders(stream, - stream->headers(), - stream->headers_count(), - stream->headers_category(), - frame->hd.flags); -} - -// Notifies the JS layer that a PRIORITY frame has been received -inline void Nghttp2Session::HandlePriorityFrame(const nghttp2_frame* frame) { - nghttp2_priority priority_frame = frame->priority; - int32_t id = GetFrameID(frame); - DEBUG_HTTP2("Nghttp2Session %s: handling priority frame for stream %d\n", - TypeName(), id); - - // Priority frame stream ID should never be <= 0. nghttp2 handles this - // as an error condition that terminates the session, so we should be - // good here - -#if defined(DEBUG) && DEBUG - CHECK_GT(id, 0); -#endif - - nghttp2_priority_spec spec = priority_frame.pri_spec; - OnPriority(id, spec.stream_id, spec.weight, spec.exclusive); -} - -// Notifies the JS layer that a GOAWAY frame has been received -inline void Nghttp2Session::HandleGoawayFrame(const nghttp2_frame* frame) { - nghttp2_goaway goaway_frame = frame->goaway; - DEBUG_HTTP2("Nghttp2Session %s: handling goaway frame\n", TypeName()); - - OnGoAway(goaway_frame.last_stream_id, - goaway_frame.error_code, - goaway_frame.opaque_data, - goaway_frame.opaque_data_len); -} - -// Prompts nghttp2 to flush the queue of pending data frames -inline void Nghttp2Session::SendPendingData() { - DEBUG_HTTP2("Nghttp2Session %s: Sending pending data\n", TypeName()); - // Do not attempt to send data on the socket if the destroying flag has - // been set. That means everything is shutting down and the socket - // will not be usable. - if (IsDestroying()) - return; - - WriteWrap* req = nullptr; - char* dest = nullptr; - size_t destRemaining = 0; - size_t destLength = 0; // amount of data stored in dest - size_t destOffset = 0; // current write offset of dest - - const uint8_t* src; // pointer to the serialized data - ssize_t srcLength = 0; // length of serialized data chunk - - // While srcLength is greater than zero - while ((srcLength = nghttp2_session_mem_send(session_, &src)) > 0) { - if (req == nullptr) { - req = AllocateSend(); - destRemaining = req->ExtraSize(); - dest = req->Extra(); - } - DEBUG_HTTP2("Nghttp2Session %s: nghttp2 has %d bytes to send\n", - TypeName(), srcLength); - size_t srcRemaining = srcLength; - size_t srcOffset = 0; - - // The amount of data we have to copy is greater than the space - // remaining. Copy what we can into the remaining space, send it, - // the proceed with the rest. - while (srcRemaining > destRemaining) { - DEBUG_HTTP2("Nghttp2Session %s: pushing %d bytes to the socket\n", - TypeName(), destLength + destRemaining); - memcpy(dest + destOffset, src + srcOffset, destRemaining); - destLength += destRemaining; - Send(req, dest, destLength); - destOffset = 0; - destLength = 0; - srcRemaining -= destRemaining; - srcOffset += destRemaining; - req = AllocateSend(); - destRemaining = req->ExtraSize(); - dest = req->Extra(); - } - - if (srcRemaining > 0) { - memcpy(dest + destOffset, src + srcOffset, srcRemaining); - destLength += srcRemaining; - destOffset += srcRemaining; - destRemaining -= srcRemaining; - srcRemaining = 0; - srcOffset = 0; - } - } - - if (destLength > 0) { - DEBUG_HTTP2("Nghttp2Session %s: pushing %d bytes to the socket\n", - TypeName(), destLength); - Send(req, dest, destLength); - } -} - -// Initialize the Nghttp2Session handle by creating and -// assigning the Nghttp2Session instance and associated -// uv_loop_t. -inline int Nghttp2Session::Init(const nghttp2_session_type type, - nghttp2_option* options, - nghttp2_mem* mem, - uint32_t maxHeaderPairs) { - session_type_ = type; - DEBUG_HTTP2("Nghttp2Session %s: initializing session\n", TypeName()); - destroying_ = false; - - max_header_pairs_ = maxHeaderPairs; - - nghttp2_session_callbacks* callbacks - = callback_struct_saved[HasGetPaddingCallback() ? 1 : 0].callbacks; - - CHECK_NE(options, nullptr); - - typedef int (*init_fn)(nghttp2_session** session, - const nghttp2_session_callbacks* callbacks, - void* user_data, - const nghttp2_option* options, - nghttp2_mem* mem); - init_fn fn = type == NGHTTP2_SESSION_SERVER ? - nghttp2_session_server_new3 : - nghttp2_session_client_new3; - - return fn(&session_, callbacks, this, options, mem); -} - -inline void Nghttp2Session::MarkDestroying() { - destroying_ = true; -} - -inline Nghttp2Session::~Nghttp2Session() { - Close(); -} - -inline void Nghttp2Session::Close() { - if (IsClosed()) - return; - DEBUG_HTTP2("Nghttp2Session %s: freeing session\n", TypeName()); - nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR); - nghttp2_session_del(session_); - session_ = nullptr; - DEBUG_HTTP2("Nghttp2Session %s: session freed\n", TypeName()); -} - -// Write data received from the socket to the underlying nghttp2_session. -inline ssize_t Nghttp2Session::Write(const uv_buf_t* bufs, unsigned int nbufs) { - size_t total = 0; - for (unsigned int n = 0; n < nbufs; n++) { - ssize_t ret = - nghttp2_session_mem_recv(session_, - reinterpret_cast(bufs[n].base), - bufs[n].len); - if (ret < 0) { - return ret; - } else { - total += ret; - } - } - SendPendingData(); - return total; -} - -inline void Nghttp2Session::AddStream(Nghttp2Stream* stream) { - streams_[stream->id()] = stream; -} - -// Removes a stream instance from this session -inline void Nghttp2Session::RemoveStream(int32_t id) { - streams_.erase(id); -} - -// Implementation for Nghttp2Stream functions - -Nghttp2Stream::Nghttp2Stream( - int32_t id, - Nghttp2Session* session, - nghttp2_headers_category category, - int options) : id_(id), - session_(session), - current_headers_category_(category) { - // Limit the number of header pairs - max_header_pairs_ = session->GetMaxHeaderPairs(); - if (max_header_pairs_ == 0) - max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; - current_headers_.reserve(max_header_pairs_); - - // Limit the number of header octets - max_header_length_ = - std::min( - nghttp2_session_get_local_settings( - session->session(), - NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE), - MAX_MAX_HEADER_LIST_SIZE); - - getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; - if (options & STREAM_OPTION_EMPTY_PAYLOAD) - Shutdown(); - session->AddStream(this); -} - - -inline void Nghttp2Stream::Destroy() { - DEBUG_HTTP2("Nghttp2Stream %d: destroying stream\n", id_); - // Do nothing if this stream instance is already destroyed - if (IsDestroyed()) - return; - flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED; - Nghttp2Session* session = this->session_; - - if (session != nullptr) { - session_->RemoveStream(this->id()); - session_ = nullptr; - } - - // Free any remaining incoming data chunks. - while (!data_chunks_.empty()) { - uv_buf_t buf = data_chunks_.front(); - free(buf.base); - data_chunks_.pop(); - } - - // Free any remaining outgoing data chunks. - while (!queue_.empty()) { - nghttp2_stream_write* head = queue_.front(); - head->cb(head->req, UV_ECANCELED); - delete head; - queue_.pop(); - } - - delete this; -} - -// Submit informational headers for a stream. -inline int Nghttp2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) { - DEBUG_HTTP2("Nghttp2Stream %d: sending informational headers, count: %d\n", - id_, len); - CHECK_GT(len, 0); - return nghttp2_submit_headers(session_->session(), - NGHTTP2_FLAG_NONE, - id_, nullptr, - nva, len, nullptr); -} - -inline int Nghttp2Stream::SubmitPriority(nghttp2_priority_spec* prispec, - bool silent) { - DEBUG_HTTP2("Nghttp2Stream %d: sending priority spec\n", id_); - return silent ? - nghttp2_session_change_stream_priority(session_->session(), - id_, prispec) : - nghttp2_submit_priority(session_->session(), - NGHTTP2_FLAG_NONE, - id_, prispec); -} - -// Submit an RST_STREAM frame -inline int Nghttp2Stream::SubmitRstStream(const uint32_t code) { - DEBUG_HTTP2("Nghttp2Stream %d: sending rst-stream, code: %d\n", id_, code); - session_->SendPendingData(); - return nghttp2_submit_rst_stream(session_->session(), - NGHTTP2_FLAG_NONE, - id_, - code); -} - -// Submit a push promise. -inline int32_t Nghttp2Stream::SubmitPushPromise( - nghttp2_nv* nva, - size_t len, - Nghttp2Stream** assigned, - int options) { -#if defined(DEBUG) && DEBUG - CHECK_GT(len, 0); -#endif - DEBUG_HTTP2("Nghttp2Stream %d: sending push promise\n", id_); - int32_t ret = nghttp2_submit_push_promise(session_->session(), - NGHTTP2_FLAG_NONE, - id_, nva, len, - nullptr); - if (ret > 0) { - auto stream = new Nghttp2Stream(ret, session_, - NGHTTP2_HCAT_HEADERS, - options); - if (assigned != nullptr) *assigned = stream; - } - return ret; -} - -// Initiate a response. If the nghttp2_stream is still writable by -// the time this is called, then an nghttp2_data_provider will be -// initialized, causing at least one (possibly empty) data frame to -// be sent. -inline int Nghttp2Stream::SubmitResponse(nghttp2_nv* nva, - size_t len, - int options) { -#if defined(DEBUG) && DEBUG - CHECK_GT(len, 0); -#endif - DEBUG_HTTP2("Nghttp2Stream %d: submitting response\n", id_); - getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; - nghttp2_data_provider* provider = nullptr; - nghttp2_data_provider prov; - prov.source.ptr = this; - prov.read_callback = Nghttp2Session::OnStreamRead; - if (IsWritable() && !(options & STREAM_OPTION_EMPTY_PAYLOAD)) - provider = &prov; - - return nghttp2_submit_response(session_->session(), id_, - nva, len, provider); -} - -// Initiate a response that contains data read from a file descriptor. -inline int Nghttp2Stream::SubmitFile(int fd, - nghttp2_nv* nva, size_t len, - int64_t offset, - int64_t length, - int options) { -#if defined(DEBUG) && DEBUG - CHECK_GT(len, 0); - CHECK_GT(fd, 0); -#endif - DEBUG_HTTP2("Nghttp2Stream %d: submitting file\n", id_); - getTrailers_ = options & STREAM_OPTION_GET_TRAILERS; - nghttp2_data_provider prov; - prov.source.fd = fd; - prov.read_callback = Nghttp2Session::OnStreamReadFD; - - if (offset > 0) fd_offset_ = offset; - if (length > -1) fd_length_ = length; - - return nghttp2_submit_response(session_->session(), id_, - nva, len, &prov); -} - -// Initiate a request. If writable is true (the default), then -// an nghttp2_data_provider will be initialized, causing at -// least one (possibly empty) data frame to to be sent. -inline int32_t Nghttp2Session::SubmitRequest( - nghttp2_priority_spec* prispec, - nghttp2_nv* nva, - size_t len, - Nghttp2Stream** assigned, - int options) { -#if defined(DEBUG) && DEBUG - CHECK_GT(len, 0); -#endif - DEBUG_HTTP2("Nghttp2Session: submitting request\n"); - nghttp2_data_provider* provider = nullptr; - nghttp2_data_provider prov; - prov.source.ptr = this; - prov.read_callback = OnStreamRead; - if (!(options & STREAM_OPTION_EMPTY_PAYLOAD)) - provider = &prov; - int32_t ret = nghttp2_submit_request(session_, - prispec, nva, len, - provider, nullptr); - // Assign the Nghttp2Stream handle - if (ret > 0) { - auto stream = new Nghttp2Stream(ret, this, NGHTTP2_HCAT_HEADERS, options); - if (assigned != nullptr) *assigned = stream; - } - return ret; -} - -// Queue the given set of uv_but_t handles for writing to an -// nghttp2_stream. The callback will be invoked once the chunks -// of data have been flushed to the underlying nghttp2_session. -// Note that this does *not* mean that the data has been flushed -// to the socket yet. -inline int Nghttp2Stream::Write(nghttp2_stream_write_t* req, - const uv_buf_t bufs[], - unsigned int nbufs, - nghttp2_stream_write_cb cb) { - if (!IsWritable()) { - if (cb != nullptr) - cb(req, UV_EOF); - return 0; - } - DEBUG_HTTP2("Nghttp2Stream %d: queuing buffers to send, count: %d\n", - id_, nbufs); - nghttp2_stream_write* item = new nghttp2_stream_write; - item->cb = cb; - item->req = req; - item->nbufs = nbufs; - item->bufs.AllocateSufficientStorage(nbufs); - memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs)); - queue_.push(item); - nghttp2_session_resume_data(session_->session(), id_); - return 0; -} - -inline void Nghttp2Stream::ReadStart() { - if (IsReading()) - return; - DEBUG_HTTP2("Nghttp2Stream %d: start reading\n", id_); - flags_ |= NGHTTP2_STREAM_FLAG_READ_START; - flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED; - - // Flush any queued data chunks immediately out to the JS layer - FlushDataChunks(); -} - -inline void Nghttp2Stream::ReadResume() { - DEBUG_HTTP2("Nghttp2Stream %d: resume reading\n", id_); - flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED; - - // Flush any queued data chunks immediately out to the JS layer - FlushDataChunks(); -} - -inline void Nghttp2Stream::ReadStop() { - DEBUG_HTTP2("Nghttp2Stream %d: stop reading\n", id_); - if (!IsReading()) - return; - flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED; -} - -Nghttp2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) { - nghttp2_session_callbacks_new(&callbacks); - nghttp2_session_callbacks_set_on_begin_headers_callback( - callbacks, OnBeginHeadersCallback); - nghttp2_session_callbacks_set_on_header_callback2( - callbacks, OnHeaderCallback); - nghttp2_session_callbacks_set_on_frame_recv_callback( - callbacks, OnFrameReceive); - nghttp2_session_callbacks_set_on_stream_close_callback( - callbacks, OnStreamClose); - nghttp2_session_callbacks_set_on_data_chunk_recv_callback( - callbacks, OnDataChunkReceived); - nghttp2_session_callbacks_set_on_frame_not_send_callback( - callbacks, OnFrameNotSent); - nghttp2_session_callbacks_set_on_invalid_header_callback2( - callbacks, OnInvalidHeader); - -#ifdef NODE_DEBUG_HTTP2 - nghttp2_session_callbacks_set_error_callback( - callbacks, OnNghttpError); -#endif - - if (kHasGetPaddingCallback) { - nghttp2_session_callbacks_set_select_padding_callback( - callbacks, OnSelectPadding); - } -} - -Nghttp2Session::Callbacks::~Callbacks() { - nghttp2_session_callbacks_del(callbacks); -} - -Nghttp2Session::SubmitTrailers::SubmitTrailers( - Nghttp2Session* handle, - Nghttp2Stream* stream, - uint32_t* flags) - : handle_(handle), stream_(stream), flags_(flags) { } - -} // namespace http2 -} // namespace node - -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#endif // SRC_NODE_HTTP2_CORE_INL_H_ diff --git a/src/node_http2_core.h b/src/node_http2_core.h deleted file mode 100644 index 5fbb7fa9f2a..00000000000 --- a/src/node_http2_core.h +++ /dev/null @@ -1,516 +0,0 @@ -#ifndef SRC_NODE_HTTP2_CORE_H_ -#define SRC_NODE_HTTP2_CORE_H_ - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include "stream_base.h" -#include "util-inl.h" -#include "uv.h" -#include "nghttp2/nghttp2.h" - -#include -#include -#include -#include - -namespace node { -namespace http2 { - -#ifdef NODE_DEBUG_HTTP2 - -// Adapted from nghttp2 own debug printer -static inline void _debug_vfprintf(const char* fmt, va_list args) { - vfprintf(stderr, fmt, args); -} - -void inline debug_vfprintf(const char* format, ...) { - va_list args; - va_start(args, format); - _debug_vfprintf(format, args); - va_end(args); -} - -#define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__); -#else -#define DEBUG_HTTP2(...) \ - do { \ - } while (0) -#endif - -#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096 -#define DEFAULT_SETTINGS_ENABLE_PUSH 1 -#define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535 -#define DEFAULT_SETTINGS_MAX_FRAME_SIZE 16384 -#define DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE 65535 -#define MAX_MAX_FRAME_SIZE 16777215 -#define MIN_MAX_FRAME_SIZE DEFAULT_SETTINGS_MAX_FRAME_SIZE -#define MAX_INITIAL_WINDOW_SIZE 2147483647 - -#define MAX_MAX_HEADER_LIST_SIZE 16777215u -#define DEFAULT_MAX_HEADER_LIST_PAIRS 128u - - -class Nghttp2Session; -class Nghttp2Stream; - -struct nghttp2_stream_write_t; - -#define MAX_BUFFER_COUNT 16 - -enum nghttp2_session_type { - NGHTTP2_SESSION_SERVER, - NGHTTP2_SESSION_CLIENT -}; - -enum nghttp2_shutdown_flags { - NGHTTP2_SHUTDOWN_FLAG_GRACEFUL -}; - -enum nghttp2_stream_flags { - NGHTTP2_STREAM_FLAG_NONE = 0x0, - // Writable side has ended - NGHTTP2_STREAM_FLAG_SHUT = 0x1, - // Reading has started - NGHTTP2_STREAM_FLAG_READ_START = 0x2, - // Reading is paused - NGHTTP2_STREAM_FLAG_READ_PAUSED = 0x4, - // Stream is closed - NGHTTP2_STREAM_FLAG_CLOSED = 0x8, - // Stream is destroyed - NGHTTP2_STREAM_FLAG_DESTROYED = 0x10 -}; - -enum nghttp2_stream_options { - STREAM_OPTION_EMPTY_PAYLOAD = 0x1, - STREAM_OPTION_GET_TRAILERS = 0x2, -}; - -// Callbacks -typedef void (*nghttp2_stream_write_cb)( - nghttp2_stream_write_t* req, - int status); - -struct nghttp2_stream_write { - unsigned int nbufs = 0; - nghttp2_stream_write_t* req = nullptr; - nghttp2_stream_write_cb cb = nullptr; - MaybeStackBuffer bufs; -}; - -struct nghttp2_header { - nghttp2_rcbuf* name = nullptr; - nghttp2_rcbuf* value = nullptr; - uint8_t flags = 0; -}; - -// Handle Types -class Nghttp2Session { - public: - // Initializes the session instance - inline int Init( - const nghttp2_session_type type = NGHTTP2_SESSION_SERVER, - nghttp2_option* options = nullptr, - nghttp2_mem* mem = nullptr, - uint32_t maxHeaderPairs = DEFAULT_MAX_HEADER_LIST_PAIRS); - - // Frees this session instance - inline ~Nghttp2Session(); - inline void MarkDestroying(); - bool IsDestroying() { - return destroying_; - } - - uint32_t GetMaxHeaderPairs() const { - return max_header_pairs_; - } - - inline const char* TypeName() { - switch (session_type_) { - case NGHTTP2_SESSION_SERVER: return "server"; - case NGHTTP2_SESSION_CLIENT: return "client"; - default: - // This should never happen - ABORT(); - } - } - - // Returns the pointer to the identified stream, or nullptr if - // the stream does not exist - inline Nghttp2Stream* FindStream(int32_t id); - - // Submits a new request. If the request is a success, assigned - // will be a pointer to the Nghttp2Stream instance assigned. - // This only works if the session is a client session. - inline int32_t SubmitRequest( - nghttp2_priority_spec* prispec, - nghttp2_nv* nva, - size_t len, - Nghttp2Stream** assigned = nullptr, - int options = 0); - - // Submits a notice to the connected peer that the session is in the - // process of shutting down. - inline void SubmitShutdownNotice(); - - // Submits a SETTINGS frame to the connected peer. - inline int SubmitSettings(const nghttp2_settings_entry iv[], size_t niv); - - // Write data to the session - inline ssize_t Write(const uv_buf_t* bufs, unsigned int nbufs); - - // Returns the nghttp2 library session - inline nghttp2_session* session() const { return session_; } - - inline bool IsClosed() const { return session_ == nullptr; } - - nghttp2_session_type type() const { - return session_type_; - } - - protected: - // Adds a stream instance to this session - inline void AddStream(Nghttp2Stream* stream); - - // Removes a stream instance from this session - inline void RemoveStream(int32_t id); - - virtual void OnHeaders( - Nghttp2Stream* stream, - nghttp2_header* headers, - size_t count, - nghttp2_headers_category cat, - uint8_t flags) {} - virtual void OnStreamClose(int32_t id, uint32_t code) {} - virtual void OnDataChunk(Nghttp2Stream* stream, - uv_buf_t* chunk) {} - virtual void OnSettings(bool ack) {} - virtual void OnPriority(int32_t id, - int32_t parent, - int32_t weight, - int8_t exclusive) {} - virtual void OnGoAway(int32_t lastStreamID, - uint32_t errorCode, - uint8_t* data, - size_t length) {} - virtual void OnFrameError(int32_t id, - uint8_t type, - int error_code) {} - virtual ssize_t GetPadding(size_t frameLength, - size_t maxFrameLength) { return 0; } - - inline void SendPendingData(); - virtual void Send(WriteWrap* req, char* buf, size_t length) = 0; - virtual WriteWrap* AllocateSend() = 0; - - virtual bool HasGetPaddingCallback() { return false; } - - class SubmitTrailers { - public: - inline void Submit(nghttp2_nv* trailers, size_t length) const; - - private: - inline SubmitTrailers(Nghttp2Session* handle, - Nghttp2Stream* stream, - uint32_t* flags); - - Nghttp2Session* const handle_; - Nghttp2Stream* const stream_; - uint32_t* const flags_; - - friend class Nghttp2Session; - }; - - virtual void OnTrailers(Nghttp2Stream* stream, - const SubmitTrailers& submit_trailers) {} - - virtual uv_loop_t* event_loop() const = 0; - - virtual void Close(); - - private: - inline void HandleHeadersFrame(const nghttp2_frame* frame); - inline void HandlePriorityFrame(const nghttp2_frame* frame); - inline void HandleDataFrame(const nghttp2_frame* frame); - inline void HandleGoawayFrame(const nghttp2_frame* frame); - - static inline void GetTrailers(nghttp2_session* session, - Nghttp2Session* handle, - Nghttp2Stream* stream, - uint32_t* flags); - - /* callbacks for nghttp2 */ -#ifdef NODE_DEBUG_HTTP2 - static inline int OnNghttpError(nghttp2_session* session, - const char* message, - size_t len, - void* user_data); -#endif - - static inline int OnBeginHeadersCallback(nghttp2_session* session, - const nghttp2_frame* frame, - void* user_data); - static inline int OnHeaderCallback(nghttp2_session* session, - const nghttp2_frame* frame, - nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags, - void* user_data); - static inline int OnFrameReceive(nghttp2_session* session, - const nghttp2_frame* frame, - void* user_data); - static inline int OnFrameNotSent(nghttp2_session* session, - const nghttp2_frame* frame, - int error_code, - void* user_data); - static inline int OnStreamClose(nghttp2_session* session, - int32_t id, - uint32_t code, - void* user_data); - static inline int OnInvalidHeader(nghttp2_session* session, - const nghttp2_frame* frame, - nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags, - void* user_data); - static inline int OnDataChunkReceived(nghttp2_session* session, - uint8_t flags, - int32_t id, - const uint8_t* data, - size_t len, - void* user_data); - static inline ssize_t OnStreamReadFD(nghttp2_session* session, - int32_t id, - uint8_t* buf, - size_t length, - uint32_t* flags, - nghttp2_data_source* source, - void* user_data); - static inline ssize_t OnStreamRead(nghttp2_session* session, - int32_t id, - uint8_t* buf, - size_t length, - uint32_t* flags, - nghttp2_data_source* source, - void* user_data); - static inline ssize_t OnSelectPadding(nghttp2_session* session, - const nghttp2_frame* frame, - size_t maxPayloadLen, - void* user_data); - - struct Callbacks { - inline explicit Callbacks(bool kHasGetPaddingCallback); - inline ~Callbacks(); - - nghttp2_session_callbacks* callbacks; - }; - - /* Use callback_struct_saved[kHasGetPaddingCallback ? 1 : 0] */ - static Callbacks callback_struct_saved[2]; - - nghttp2_session* session_; - nghttp2_session_type session_type_; - uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; - std::unordered_map streams_; - bool destroying_ = false; - - friend class Nghttp2Stream; -}; - - - -class Nghttp2Stream { - public: - // Resets the state of the stream instance to defaults - Nghttp2Stream( - int32_t id, - Nghttp2Session* session, - nghttp2_headers_category category = NGHTTP2_HCAT_HEADERS, - int options = 0); - - inline ~Nghttp2Stream() {} - - inline void FlushDataChunks(); - - // Destroy this stream instance and free all held memory. - // Note that this will free queued outbound and inbound - // data chunks and inbound headers, so it's important not - // to call this until those are fully consumed. - inline void Destroy(); - - // Returns true if this stream has been destroyed - inline bool IsDestroyed() const { - return flags_ & NGHTTP2_STREAM_FLAG_DESTROYED; - } - - // Queue outbound chunks of data to be sent on this stream - inline int Write( - nghttp2_stream_write_t* req, - const uv_buf_t bufs[], - unsigned int nbufs, - nghttp2_stream_write_cb cb); - - // Initiate a response on this stream. - inline int SubmitResponse(nghttp2_nv* nva, - size_t len, - int options); - - // Send data read from a file descriptor as the response on this stream. - inline int SubmitFile(int fd, - nghttp2_nv* nva, size_t len, - int64_t offset, - int64_t length, - int options); - - // Submit informational headers for this stream - inline int SubmitInfo(nghttp2_nv* nva, size_t len); - - // Submit a PRIORITY frame for this stream - inline int SubmitPriority(nghttp2_priority_spec* prispec, - bool silent = false); - - // Submits an RST_STREAM frame using the given code - inline int SubmitRstStream(const uint32_t code); - - // Submits a PUSH_PROMISE frame with this stream as the parent. - inline int SubmitPushPromise( - nghttp2_nv* nva, - size_t len, - Nghttp2Stream** assigned = nullptr, - int options = 0); - - // Marks the Writable side of the stream as being shutdown - inline void Shutdown() { - flags_ |= NGHTTP2_STREAM_FLAG_SHUT; - nghttp2_session_resume_data(session_->session(), id_); - } - - // Returns true if this stream is writable. - inline bool IsWritable() const { - return !(flags_ & NGHTTP2_STREAM_FLAG_SHUT); - } - - // Start Reading. If there are queued data chunks, they are pushed into - // the session to be emitted at the JS side - inline void ReadStart(); - - // Resume Reading - inline void ReadResume(); - - // Stop/Pause Reading. - inline void ReadStop(); - - // Returns true if reading is paused - inline bool IsPaused() const { - return flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED; - } - - inline bool GetTrailers() const { - return getTrailers_; - } - - // Returns true if this stream is in the reading state, which occurs when - // the NGHTTP2_STREAM_FLAG_READ_START flag has been set and the - // NGHTTP2_STREAM_FLAG_READ_PAUSED flag is *not* set. - inline bool IsReading() const { - return flags_ & NGHTTP2_STREAM_FLAG_READ_START && - !(flags_ & NGHTTP2_STREAM_FLAG_READ_PAUSED); - } - - inline void Close(int32_t code) { - DEBUG_HTTP2("Nghttp2Stream %d: closing with code %d\n", id_, code); - flags_ |= NGHTTP2_STREAM_FLAG_CLOSED; - code_ = code; - session_->OnStreamClose(id_, code); - DEBUG_HTTP2("Nghttp2Stream %d: closed\n", id_); - } - - // Returns true if this stream has been closed either by receiving or - // sending an RST_STREAM frame. - inline bool IsClosed() const { - return flags_ & NGHTTP2_STREAM_FLAG_CLOSED; - } - - // Returns the RST_STREAM code used to close this stream - inline int32_t code() const { - return code_; - } - - // Returns the stream identifier for this stream - inline int32_t id() const { - return id_; - } - - inline bool AddHeader(nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags); - - inline nghttp2_header* headers() { - return current_headers_.data(); - } - - inline nghttp2_headers_category headers_category() const { - return current_headers_category_; - } - - inline size_t headers_count() const { - return current_headers_.size(); - } - - void StartHeaders(nghttp2_headers_category category) { - DEBUG_HTTP2("Nghttp2Stream %d: starting headers, category: %d\n", - id_, category); - current_headers_length_ = 0; - current_headers_.clear(); - current_headers_category_ = category; - } - - private: - // The Stream Identifier - int32_t id_; - - // The Parent HTTP/2 Session - Nghttp2Session* session_; - - // Internal state flags - int flags_ = NGHTTP2_STREAM_FLAG_NONE; - uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS; - uint32_t max_header_length_ = DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE; - - // The RST_STREAM code used to close this stream - int32_t code_ = NGHTTP2_NO_ERROR; - - // Outbound Data... This is the data written by the JS layer that is - // waiting to be written out to the socket. - std::queue queue_; - unsigned int queue_index_ = 0; - size_t queue_offset_ = 0; - int64_t fd_offset_ = 0; - int64_t fd_length_ = -1; - - // True if this stream will have outbound trailers - bool getTrailers_ = false; - - // The Current Headers block... As headers are received for this stream, - // they are temporarily stored here until the OnFrameReceived is called - // signalling the end of the HEADERS frame - nghttp2_headers_category current_headers_category_ = NGHTTP2_HCAT_HEADERS; - uint32_t current_headers_length_ = 0; // total number of octets - std::vector current_headers_; - - // Inbound Data... This is the data received via DATA frames for this stream. - std::queue data_chunks_; - - friend class Nghttp2Session; -}; - -struct nghttp2_stream_write_t { - void* data; - int status; -}; - -} // namespace http2 -} // namespace node - -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#endif // SRC_NODE_HTTP2_CORE_H_ diff --git a/src/node_http2_state.h b/src/node_http2_state.h index dd8954de2ae..a7ad23fb519 100755 --- a/src/node_http2_state.h +++ b/src/node_http2_state.h @@ -48,6 +48,7 @@ namespace http2 { IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS, IDX_OPTIONS_PADDING_STRATEGY, IDX_OPTIONS_MAX_HEADER_LIST_PAIRS, + IDX_OPTIONS_MAX_OUTSTANDING_PINGS, IDX_OPTIONS_FLAGS }; diff --git a/src/node_internals.h b/src/node_internals.h index 70b5cf04563..1c2cd47ac15 100644 --- a/src/node_internals.h +++ b/src/node_internals.h @@ -252,8 +252,12 @@ void RegisterSignalHandler(int signal, bool reset_handler = false); #endif +uint32_t GetProcessId(); bool SafeGetenv(const char* key, std::string* text); +std::string GetHumanReadableProcessName(); +void GetHumanReadableProcessName(char (*name)[1024]); + template constexpr size_t arraysize(const T(&)[N]) { return N; } diff --git a/src/node_platform.cc b/src/node_platform.cc index 0c50e8468d0..65d1ef6c5df 100644 --- a/src/node_platform.cc +++ b/src/node_platform.cc @@ -24,6 +24,43 @@ static void BackgroundRunner(void* data) { } } +BackgroundTaskRunner::BackgroundTaskRunner(int thread_pool_size) { + for (int i = 0; i < thread_pool_size; i++) { + std::unique_ptr t { new uv_thread_t() }; + if (uv_thread_create(t.get(), BackgroundRunner, &background_tasks_) != 0) + break; + threads_.push_back(std::move(t)); + } +} + +void BackgroundTaskRunner::PostTask(std::unique_ptr task) { + background_tasks_.Push(std::move(task)); +} + +void BackgroundTaskRunner::PostIdleTask(std::unique_ptr task) { + UNREACHABLE(); +} + +void BackgroundTaskRunner::PostDelayedTask(std::unique_ptr task, + double delay_in_seconds) { + UNREACHABLE(); +} + +void BackgroundTaskRunner::BlockingDrain() { + background_tasks_.BlockingDrain(); +} + +void BackgroundTaskRunner::Shutdown() { + background_tasks_.Stop(); + for (size_t i = 0; i < threads_.size(); i++) { + CHECK_EQ(0, uv_thread_join(threads_[i].get())); + } +} + +size_t BackgroundTaskRunner::NumberOfAvailableBackgroundThreads() const { + return threads_.size(); +} + PerIsolatePlatformData::PerIsolatePlatformData( v8::Isolate* isolate, uv_loop_t* loop) : isolate_(isolate), loop_(loop) { @@ -38,17 +75,20 @@ void PerIsolatePlatformData::FlushTasks(uv_async_t* handle) { platform_data->FlushForegroundTasksInternal(); } -void PerIsolatePlatformData::CallOnForegroundThread( - std::unique_ptr task) { +void PerIsolatePlatformData::PostIdleTask(std::unique_ptr task) { + UNREACHABLE(); +} + +void PerIsolatePlatformData::PostTask(std::unique_ptr task) { foreground_tasks_.Push(std::move(task)); uv_async_send(flush_tasks_); } -void PerIsolatePlatformData::CallDelayedOnForegroundThread( - std::unique_ptr task, double delay_in_seconds) { +void PerIsolatePlatformData::PostDelayedTask( + std::unique_ptr task, double delay_in_seconds) { std::unique_ptr delayed(new DelayedTask()); delayed->task = std::move(task); - delayed->platform_data = this; + delayed->platform_data = shared_from_this(); delayed->timeout = delay_in_seconds; foreground_delayed_tasks_.Push(std::move(delayed)); uv_async_send(flush_tasks_); @@ -80,49 +120,43 @@ NodePlatform::NodePlatform(int thread_pool_size, TracingController* controller = new TracingController(); tracing_controller_.reset(controller); } - for (int i = 0; i < thread_pool_size; i++) { - uv_thread_t* t = new uv_thread_t(); - if (uv_thread_create(t, BackgroundRunner, &background_tasks_) != 0) { - delete t; - break; - } - threads_.push_back(std::unique_ptr(t)); - } + background_task_runner_ = + std::make_shared(thread_pool_size); } void NodePlatform::RegisterIsolate(IsolateData* isolate_data, uv_loop_t* loop) { Isolate* isolate = isolate_data->isolate(); Mutex::ScopedLock lock(per_isolate_mutex_); - PerIsolatePlatformData* existing = per_isolate_[isolate]; - if (existing != nullptr) + std::shared_ptr existing = per_isolate_[isolate]; + if (existing) { existing->ref(); - else - per_isolate_[isolate] = new PerIsolatePlatformData(isolate, loop); + } else { + per_isolate_[isolate] = + std::make_shared(isolate, loop); + } } void NodePlatform::UnregisterIsolate(IsolateData* isolate_data) { Isolate* isolate = isolate_data->isolate(); Mutex::ScopedLock lock(per_isolate_mutex_); - PerIsolatePlatformData* existing = per_isolate_[isolate]; - CHECK_NE(existing, nullptr); + std::shared_ptr existing = per_isolate_[isolate]; + CHECK(existing); if (existing->unref() == 0) { - delete existing; per_isolate_.erase(isolate); } } void NodePlatform::Shutdown() { - background_tasks_.Stop(); - for (size_t i = 0; i < threads_.size(); i++) { - CHECK_EQ(0, uv_thread_join(threads_[i].get())); + background_task_runner_->Shutdown(); + + { + Mutex::ScopedLock lock(per_isolate_mutex_); + per_isolate_.clear(); } - Mutex::ScopedLock lock(per_isolate_mutex_); - for (const auto& pair : per_isolate_) - delete pair.second; } size_t NodePlatform::NumberOfAvailableBackgroundThreads() { - return threads_.size(); + return background_task_runner_->NumberOfAvailableBackgroundThreads(); } void PerIsolatePlatformData::RunForegroundTask(std::unique_ptr task) { @@ -155,14 +189,14 @@ void PerIsolatePlatformData::CancelPendingDelayedTasks() { } void NodePlatform::DrainBackgroundTasks(Isolate* isolate) { - PerIsolatePlatformData* per_isolate = ForIsolate(isolate); + std::shared_ptr per_isolate = ForIsolate(isolate); do { // Right now, there is no way to drain only background tasks associated // with a specific isolate, so this sometimes does more work than // necessary. In the long run, that functionality is probably going to // be available anyway, though. - background_tasks_.BlockingDrain(); + background_task_runner_->BlockingDrain(); } while (per_isolate->FlushForegroundTasksInternal()); } @@ -198,24 +232,25 @@ bool PerIsolatePlatformData::FlushForegroundTasksInternal() { void NodePlatform::CallOnBackgroundThread(Task* task, ExpectedRuntime expected_runtime) { - background_tasks_.Push(std::unique_ptr(task)); + background_task_runner_->PostTask(std::unique_ptr(task)); } -PerIsolatePlatformData* NodePlatform::ForIsolate(Isolate* isolate) { +std::shared_ptr +NodePlatform::ForIsolate(Isolate* isolate) { Mutex::ScopedLock lock(per_isolate_mutex_); - PerIsolatePlatformData* data = per_isolate_[isolate]; - CHECK_NE(data, nullptr); + std::shared_ptr data = per_isolate_[isolate]; + CHECK(data); return data; } void NodePlatform::CallOnForegroundThread(Isolate* isolate, Task* task) { - ForIsolate(isolate)->CallOnForegroundThread(std::unique_ptr(task)); + ForIsolate(isolate)->PostTask(std::unique_ptr(task)); } void NodePlatform::CallDelayedOnForegroundThread(Isolate* isolate, Task* task, double delay_in_seconds) { - ForIsolate(isolate)->CallDelayedOnForegroundThread( + ForIsolate(isolate)->PostDelayedTask( std::unique_ptr(task), delay_in_seconds); } @@ -229,6 +264,16 @@ void NodePlatform::CancelPendingDelayedTasks(v8::Isolate* isolate) { bool NodePlatform::IdleTasksEnabled(Isolate* isolate) { return false; } +std::shared_ptr +NodePlatform::GetBackgroundTaskRunner(Isolate* isolate) { + return background_task_runner_; +} + +std::shared_ptr +NodePlatform::GetForegroundTaskRunner(Isolate* isolate) { + return ForIsolate(isolate); +} + double NodePlatform::MonotonicallyIncreasingTime() { // Convert nanos to seconds. return uv_hrtime() / 1e9; diff --git a/src/node_platform.h b/src/node_platform.h index 48301a05a11..e5253cac10c 100644 --- a/src/node_platform.h +++ b/src/node_platform.h @@ -43,17 +43,22 @@ struct DelayedTask { std::unique_ptr task; uv_timer_t timer; double timeout; - PerIsolatePlatformData* platform_data; + std::shared_ptr platform_data; }; -class PerIsolatePlatformData { +// This acts as the foreground task runner for a given Isolate. +class PerIsolatePlatformData : + public v8::TaskRunner, + public std::enable_shared_from_this { public: PerIsolatePlatformData(v8::Isolate* isolate, uv_loop_t* loop); ~PerIsolatePlatformData(); - void CallOnForegroundThread(std::unique_ptr task); - void CallDelayedOnForegroundThread(std::unique_ptr task, - double delay_in_seconds); + void PostTask(std::unique_ptr task) override; + void PostIdleTask(std::unique_ptr task) override; + void PostDelayedTask(std::unique_ptr task, + double delay_in_seconds) override; + bool IdleTasksEnabled() override { return false; }; void Shutdown(); @@ -84,6 +89,26 @@ class PerIsolatePlatformData { std::vector scheduled_delayed_tasks_; }; +// This acts as the single background task runner for all Isolates. +class BackgroundTaskRunner : public v8::TaskRunner { + public: + explicit BackgroundTaskRunner(int thread_pool_size); + + void PostTask(std::unique_ptr task) override; + void PostIdleTask(std::unique_ptr task) override; + void PostDelayedTask(std::unique_ptr task, + double delay_in_seconds) override; + bool IdleTasksEnabled() override { return false; }; + + void BlockingDrain(); + void Shutdown(); + + size_t NumberOfAvailableBackgroundThreads() const; + private: + TaskQueue background_tasks_; + std::vector> threads_; +}; + class NodePlatform : public MultiIsolatePlatform { public: NodePlatform(int thread_pool_size, v8::TracingController* tracing_controller); @@ -109,15 +134,20 @@ class NodePlatform : public MultiIsolatePlatform { void RegisterIsolate(IsolateData* isolate_data, uv_loop_t* loop) override; void UnregisterIsolate(IsolateData* isolate_data) override; + std::shared_ptr GetBackgroundTaskRunner( + v8::Isolate* isolate) override; + std::shared_ptr GetForegroundTaskRunner( + v8::Isolate* isolate) override; + private: - PerIsolatePlatformData* ForIsolate(v8::Isolate* isolate); + std::shared_ptr ForIsolate(v8::Isolate* isolate); Mutex per_isolate_mutex_; - std::unordered_map per_isolate_; - TaskQueue background_tasks_; - std::vector> threads_; + std::unordered_map> per_isolate_; std::unique_ptr tracing_controller_; + std::shared_ptr background_task_runner_; }; } // namespace node diff --git a/src/node_zlib.cc b/src/node_zlib.cc index 1cad357e496..df57e9d5a09 100644 --- a/src/node_zlib.cc +++ b/src/node_zlib.cc @@ -345,7 +345,7 @@ class ZCtx : public AsyncWrap { } break; default: - CHECK(0 && "wtf?"); + UNREACHABLE(); } // pass any errors back to the main thread to deal with. @@ -590,7 +590,7 @@ class ZCtx : public AsyncWrap { ->AdjustAmountOfExternalAllocatedMemory(kInflateContextSize); break; default: - CHECK(0 && "wtf?"); + UNREACHABLE(); } ctx->dictionary_ = reinterpret_cast(dictionary); diff --git a/src/util.cc b/src/util.cc index ef93d16968e..2aa9fb026ea 100644 --- a/src/util.cc +++ b/src/util.cc @@ -24,6 +24,14 @@ #include "node_internals.h" #include +#ifdef __POSIX__ +#include // getpid() +#endif + +#ifdef _MSC_VER +#include // GetCurrentProcessId() +#endif + namespace node { using v8::Isolate; @@ -105,4 +113,24 @@ void LowMemoryNotification() { } } +std::string GetHumanReadableProcessName() { + char name[1024]; + GetHumanReadableProcessName(&name); + return name; +} + +void GetHumanReadableProcessName(char (*name)[1024]) { + char title[1024] = "Node.js"; + uv_get_process_title(title, sizeof(title)); + snprintf(*name, sizeof(*name), "%s[%u]", title, GetProcessId()); +} + +uint32_t GetProcessId() { +#ifdef _WIN32 + return GetCurrentProcessId(); +#else + return getpid(); +#endif +} + } // namespace node diff --git a/test/addons-napi/test_general/test.js b/test/addons-napi/test_general/test.js index 5644280f64a..ca04f1da148 100644 --- a/test/addons-napi/test_general/test.js +++ b/test/addons-napi/test_general/test.js @@ -34,7 +34,7 @@ assert.ok(test_general.testGetPrototype(baseObject) !== // test version management functions // expected version is currently 1 -assert.strictEqual(test_general.testGetVersion(), 1); +assert.strictEqual(test_general.testGetVersion(), 2); const [ major, minor, patch, release ] = test_general.testGetNodeVersion(); assert.strictEqual(process.version.split('-')[0], diff --git a/test/addons-napi/test_uv_loop/binding.gyp b/test/addons-napi/test_uv_loop/binding.gyp new file mode 100644 index 00000000000..81fcfdc592a --- /dev/null +++ b/test/addons-napi/test_uv_loop/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "test_uv_loop", + "sources": [ "test_uv_loop.cc" ] + } + ] +} diff --git a/test/addons-napi/test_uv_loop/test.js b/test/addons-napi/test_uv_loop/test.js new file mode 100644 index 00000000000..4efc3c6fcd7 --- /dev/null +++ b/test/addons-napi/test_uv_loop/test.js @@ -0,0 +1,5 @@ +'use strict'; +const common = require('../../common'); +const { SetImmediate } = require(`./build/${common.buildType}/test_uv_loop`); + +SetImmediate(common.mustCall()); diff --git a/test/addons-napi/test_uv_loop/test_uv_loop.cc b/test/addons-napi/test_uv_loop/test_uv_loop.cc new file mode 100644 index 00000000000..44819f72bb6 --- /dev/null +++ b/test/addons-napi/test_uv_loop/test_uv_loop.cc @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include +#include "../common.h" + +template +void* SetImmediate(napi_env env, T&& cb) { + T* ptr = new T(std::move(cb)); + uv_loop_t* loop = nullptr; + uv_check_t* check = new uv_check_t; + check->data = ptr; + NAPI_ASSERT(env, + napi_get_uv_event_loop(env, &loop) == napi_ok, + "can get event loop"); + uv_check_init(loop, check); + uv_check_start(check, [](uv_check_t* check) { + std::unique_ptr ptr {static_cast(check->data)}; + T cb = std::move(*ptr); + uv_close(reinterpret_cast(check), [](uv_handle_t* handle) { + delete reinterpret_cast(handle); + }); + + assert(cb() != nullptr); + }); + return nullptr; +} + +static char dummy; + +napi_value SetImmediateBinding(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_value _this; + void* data; + NAPI_CALL(env, + napi_get_cb_info(env, info, &argc, argv, &_this, &data)); + NAPI_ASSERT(env, argc >= 1, "Not enough arguments, expected 1."); + + napi_valuetype t; + NAPI_CALL(env, napi_typeof(env, argv[0], &t)); + NAPI_ASSERT(env, t == napi_function, + "Wrong first argument, function expected."); + + napi_ref cbref; + NAPI_CALL(env, + napi_create_reference(env, argv[0], 1, &cbref)); + + SetImmediate(env, [=]() -> char* { + napi_value undefined; + napi_value callback; + napi_handle_scope scope; + NAPI_CALL(env, napi_open_handle_scope(env, &scope)); + NAPI_CALL(env, napi_get_undefined(env, &undefined)); + NAPI_CALL(env, napi_get_reference_value(env, cbref, &callback)); + NAPI_CALL(env, napi_delete_reference(env, cbref)); + NAPI_CALL(env, + napi_call_function(env, undefined, callback, 0, nullptr, nullptr)); + NAPI_CALL(env, napi_close_handle_scope(env, scope)); + return &dummy; + }); + + return nullptr; +} + +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NAPI_PROPERTY("SetImmediate", SetImmediateBinding) + }; + + NAPI_CALL(env, napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/addons/async-hello-world/binding.cc b/test/addons/async-hello-world/binding.cc index f773bfffcdb..5b3a800709f 100644 --- a/test/addons/async-hello-world/binding.cc +++ b/test/addons/async-hello-world/binding.cc @@ -77,7 +77,7 @@ void Method(const v8::FunctionCallbackInfo& args) { v8::Local callback = v8::Local::Cast(args[1]); req->callback.Reset(isolate, callback); - uv_queue_work(uv_default_loop(), + uv_queue_work(node::GetCurrentEventLoop(isolate), &req->req, DoAsync, (uv_after_work_cb)AfterAsync); diff --git a/test/addons/callback-scope/binding.cc b/test/addons/callback-scope/binding.cc index f03d8a20d53..34d452bbb7c 100644 --- a/test/addons/callback-scope/binding.cc +++ b/test/addons/callback-scope/binding.cc @@ -52,7 +52,10 @@ static void TestResolveAsync(const v8::FunctionCallbackInfo& args) { uv_work_t* req = new uv_work_t; - uv_queue_work(uv_default_loop(), req, [](uv_work_t*) {}, Callback); + uv_queue_work(node::GetCurrentEventLoop(isolate), + req, + [](uv_work_t*) {}, + Callback); } v8::Local local = diff --git a/test/async-hooks/test-signalwrap.js b/test/async-hooks/test-signalwrap.js index ff7d08bc120..fa975ff0178 100644 --- a/test/async-hooks/test-signalwrap.js +++ b/test/async-hooks/test-signalwrap.js @@ -66,12 +66,14 @@ function onsigusr2() { } function onsigusr2Again() { - checkInvocations( - signal1, { init: 1, before: 2, after: 2, destroy: 1 }, - 'signal1: when second SIGUSR2 handler is called'); - checkInvocations( - signal2, { init: 1, before: 1 }, - 'signal2: when second SIGUSR2 handler is called'); + setImmediate(() => { + checkInvocations( + signal1, { init: 1, before: 2, after: 2, destroy: 1 }, + 'signal1: when second SIGUSR2 handler is called'); + checkInvocations( + signal2, { init: 1, before: 1 }, + 'signal2: when second SIGUSR2 handler is called'); + }); } process.on('exit', onexit); diff --git a/test/async-hooks/test-tcpwrap.js b/test/async-hooks/test-tcpwrap.js index 1f4fc6af0d6..4693e730bfb 100644 --- a/test/async-hooks/test-tcpwrap.js +++ b/test/async-hooks/test-tcpwrap.js @@ -128,8 +128,10 @@ function onconnection(c) { function onserverClosed() { checkInvocations(tcp1, { init: 1, before: 1, after: 1, destroy: 1 }, 'tcp1 when server is closed'); - checkInvocations(tcp2, { init: 1, before: 2, after: 2, destroy: 1 }, - 'tcp2 when server is closed'); + setImmediate(() => { + checkInvocations(tcp2, { init: 1, before: 2, after: 2, destroy: 1 }, + 'tcp2 after server is closed'); + }); checkInvocations(tcp3, { init: 1, before: 1, after: 1 }, 'tcp3 synchronously when server is closed'); tick(2, () => { diff --git a/test/fixtures/resolve-paths/default/node_modules/dep/index.js b/test/fixtures/resolve-paths/default/node_modules/dep/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fixtures/resolve-paths/default/verify-paths.js b/test/fixtures/resolve-paths/default/verify-paths.js new file mode 100644 index 00000000000..dee03fbfe3b --- /dev/null +++ b/test/fixtures/resolve-paths/default/verify-paths.js @@ -0,0 +1,21 @@ +'use strict'; +require('../../../common'); +const assert = require('assert'); +const path = require('path'); + +// By default, resolving 'dep' should return +// fixturesDir/resolve-paths/default/node_modules/dep/index.js. By setting +// the path to fixturesDir/resolve-paths/default, the 'default' directory +// structure should be ignored. + +assert.strictEqual( + require.resolve('dep'), + path.join(__dirname, 'node_modules', 'dep', 'index.js') +); + +const paths = [path.resolve(__dirname, '..', 'defined')]; + +assert.strictEqual( + require.resolve('dep', { paths }), + path.join(paths[0], 'node_modules', 'dep', 'index.js') +); diff --git a/test/fixtures/resolve-paths/defined/node_modules/dep/index.js b/test/fixtures/resolve-paths/defined/node_modules/dep/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/parallel/test-cluster-message.js b/test/parallel/test-cluster-message.js index 48bd7ee36ad..5585a64d481 100644 --- a/test/parallel/test-cluster-message.js +++ b/test/parallel/test-cluster-message.js @@ -20,7 +20,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const cluster = require('cluster'); const net = require('net'); @@ -132,9 +132,9 @@ if (cluster.isWorker) { worker.kill(); }); - worker.on('exit', function() { + worker.on('exit', common.mustCall(function() { process.exit(0); - }); + })); }); process.once('exit', function() { diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js index 569979e73ef..43665029630 100644 --- a/test/parallel/test-http2-client-data-end.js +++ b/test/parallel/test-http2-client-data-end.js @@ -82,7 +82,7 @@ server.listen(0, common.mustCall(() => { let data = ''; req.setEncoding('utf8'); - req.on('data', common.mustCall((d) => data += d)); + req.on('data', common.mustCallAtLeast((d) => data += d)); req.on('end', common.mustCall(() => { assert.strictEqual(data, 'test'); maybeClose(); diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js index 8b91f2d2104..bb93366247a 100644 --- a/test/parallel/test-http2-client-destroy.js +++ b/test/parallel/test-http2-client-destroy.js @@ -95,7 +95,6 @@ const { kSocket } = require('internal/http2/util'); common.expectsError(() => client.request(), sessionError); common.expectsError(() => client.settings({}), sessionError); - common.expectsError(() => client.priority(req, {}), sessionError); common.expectsError(() => client.shutdown(), sessionError); // Wait for setImmediate call from destroy() to complete @@ -103,9 +102,7 @@ const { kSocket } = require('internal/http2/util'); setImmediate(() => { common.expectsError(() => client.request(), sessionError); common.expectsError(() => client.settings({}), sessionError); - common.expectsError(() => client.priority(req, {}), sessionError); common.expectsError(() => client.shutdown(), sessionError); - common.expectsError(() => client.rstStream(req), sessionError); }); req.on( diff --git a/test/parallel/test-http2-client-http1-server.js b/test/parallel/test-http2-client-http1-server.js new file mode 100644 index 00000000000..44d8d392f4e --- /dev/null +++ b/test/parallel/test-http2-client-http1-server.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http = require('http'); +const http2 = require('http2'); + +const server = http.createServer(common.mustNotCall()); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + + const req = client.request(); + req.on('streamClosed', common.mustCall()); + + client.on('error', common.expectsError({ + code: 'ERR_HTTP2_ERROR', + type: Error, + message: 'Protocol error' + })); + + client.on('close', (...args) => { + server.close(); + }); +})); diff --git a/test/parallel/test-http2-client-onconnect-errors.js b/test/parallel/test-http2-client-onconnect-errors.js index 51ceb83677c..08007753654 100644 --- a/test/parallel/test-http2-client-onconnect-errors.js +++ b/test/parallel/test-http2-client-onconnect-errors.js @@ -11,27 +11,16 @@ if (!common.hasCrypto) const http2 = require('http2'); // tests error handling within requestOnConnect -// - NGHTTP2_ERR_NOMEM (should emit session error) // - NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE (should emit session error) // - NGHTTP2_ERR_INVALID_ARGUMENT (should emit stream error) // - every other NGHTTP2 error from binding (should emit session error) const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM', 'NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE', 'NGHTTP2_ERR_INVALID_ARGUMENT' ]; const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - }, { ngError: constants.NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE, error: { @@ -40,7 +29,7 @@ const specificTests = [ message: 'No stream ID is available because ' + 'maximum stream ID has been reached' }, - type: 'session' + type: 'stream' }, { ngError: constants.NGHTTP2_ERR_INVALID_ARGUMENT, @@ -72,24 +61,15 @@ const tests = specificTests.concat(genericTests); let currentError; // mock submitRequest because we only care about testing error handling -Http2Session.prototype.submitRequest = () => currentError; +Http2Session.prototype.request = () => currentError; const server = http2.createServer(common.mustNotCall()); server.listen(0, common.mustCall(() => runTest(tests.shift()))); function runTest(test) { - const port = server.address().port; - const url = `http://localhost:${port}`; - const headers = { - ':path': '/', - ':method': 'POST', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - - const client = http2.connect(url); - const req = client.request(headers); + const client = http2.connect(`http://localhost:${server.address().port}`); + const req = client.request({ ':method': 'POST' }); currentError = test.ngError; req.resume(); diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js index 7fc3841622e..b062107e4ab 100644 --- a/test/parallel/test-http2-client-priority-before-connect.js +++ b/test/parallel/test-http2-client-priority-before-connect.js @@ -25,7 +25,7 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); const req = client.request({ ':path': '/' }); - client.priority(req, {}); + req.priority({}); req.on('response', common.mustCall()); req.resume(); diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js index 3c4ac3b34d8..e4aff87be98 100644 --- a/test/parallel/test-http2-client-rststream-before-connect.js +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -7,6 +7,10 @@ const assert = require('assert'); const h2 = require('http2'); const server = h2.createServer(); +server.on('stream', (stream) => { + stream.respond(); + stream.end('ok'); +}); server.listen(0); @@ -15,15 +19,13 @@ server.on('listening', common.mustCall(() => { const client = h2.connect(`http://localhost:${server.address().port}`); const req = client.request({ ':path': '/' }); - client.rstStream(req, 0); - assert.strictEqual(req.rstCode, 0); + req.rstStream(0); // make sure that destroy is called req._destroy = common.mustCall(req._destroy.bind(req)); // second call doesn't do anything - assert.doesNotThrow(() => client.rstStream(req, 8)); - assert.strictEqual(req.rstCode, 0); + assert.doesNotThrow(() => req.rstStream(8)); req.on('streamClosed', common.mustCall((code) => { assert.strictEqual(req.destroyed, true); diff --git a/test/parallel/test-http2-client-settings-errors.js b/test/parallel/test-http2-client-settings-errors.js deleted file mode 100644 index d3a8ea9d8b5..00000000000 --- a/test/parallel/test-http2-client-settings-errors.js +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; - -const { - constants, - Http2Session, - nghttp2ErrorString -} = process.binding('http2'); -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const http2 = require('http2'); - -// tests error handling within requestOnConnect -// - NGHTTP2_ERR_NOMEM (should emit session error) -// - every other NGHTTP2 error from binding (should emit session error) - -const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM' -]; - -const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - } - } -]; - -const genericTests = Object.getOwnPropertyNames(constants) - .filter((key) => ( - key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0 - )) - .map((key) => ({ - ngError: constants[key], - error: { - code: 'ERR_HTTP2_ERROR', - type: Error, - message: nghttp2ErrorString(constants[key]) - } - })); - -const tests = specificTests.concat(genericTests); - -const server = http2.createServer(common.mustNotCall()); -server.on('sessionError', () => {}); // not being tested - -server.listen(0, common.mustCall(() => runTest(tests.shift()))); - -function runTest(test) { - // mock submitSettings because we only care about testing error handling - Http2Session.prototype.submitSettings = () => test.ngError; - - const errorMustCall = common.expectsError(test.error); - const errorMustNotCall = common.mustNotCall( - `${test.error.code} should emit on session` - ); - - const url = `http://localhost:${server.address().port}`; - - const client = http2.connect(url, { - settings: { - maxHeaderListSize: 1 - } - }); - - const req = client.request(); - req.resume(); - req.end(); - - client.on('error', errorMustCall); - req.on('error', errorMustNotCall); - - req.on('end', common.mustCall(() => { - client.destroy(); - if (!tests.length) { - server.close(); - } else { - runTest(tests.shift()); - } - })); -} diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js index 713e4babd20..b4d90281918 100644 --- a/test/parallel/test-http2-compat-serverrequest-trailers.js +++ b/test/parallel/test-http2-compat-serverrequest-trailers.js @@ -19,7 +19,7 @@ server.listen(0, common.mustCall(function() { server.once('request', common.mustCall(function(request, response) { let data = ''; request.setEncoding('utf8'); - request.on('data', common.mustCall((chunk) => data += chunk)); + request.on('data', common.mustCallAtLeast((chunk) => data += chunk)); request.on('end', common.mustCall(() => { const trailers = request.trailers; for (const [name, value] of Object.entries(expectedTrailers)) { diff --git a/test/parallel/test-http2-getpackedsettings.js b/test/parallel/test-http2-getpackedsettings.js index 4c7cf3d85ca..7461176c5fc 100644 --- a/test/parallel/test-http2-getpackedsettings.js +++ b/test/parallel/test-http2-getpackedsettings.js @@ -104,7 +104,8 @@ assert.doesNotThrow(() => http2.getPackedSettings({ enablePush: false })); }, common.expectsError({ code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'The "buf" argument must be one of type Buffer or Uint8Array' + message: + 'The "buf" argument must be one of type Buffer, TypedArray, or DataView' })); }); diff --git a/test/parallel/test-http2-info-headers-errors.js b/test/parallel/test-http2-info-headers-errors.js index 5e1c2d1fad7..b671bece4f7 100644 --- a/test/parallel/test-http2-info-headers-errors.js +++ b/test/parallel/test-http2-info-headers-errors.js @@ -6,29 +6,15 @@ if (!common.hasCrypto) const http2 = require('http2'); const { constants, - Http2Session, + Http2Stream, nghttp2ErrorString } = process.binding('http2'); // tests error handling within additionalHeaders -// - NGHTTP2_ERR_NOMEM (should emit session error) // - every other NGHTTP2 error from binding (should emit stream error) -const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM' -]; - -const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - } -]; +const specificTestKeys = []; +const specificTests = []; const genericTests = Object.getOwnPropertyNames(constants) .filter((key) => ( @@ -50,7 +36,7 @@ const tests = specificTests.concat(genericTests); let currentError; // mock sendHeaders because we only care about testing error handling -Http2Session.prototype.sendHeaders = () => currentError.ngError; +Http2Stream.prototype.info = () => currentError.ngError; const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers) => { diff --git a/test/parallel/test-http2-invalidargtypes-errors.js b/test/parallel/test-http2-invalidargtypes-errors.js index 93d161557c1..3471e46fdf4 100644 --- a/test/parallel/test-http2-invalidargtypes-errors.js +++ b/test/parallel/test-http2-invalidargtypes-errors.js @@ -16,15 +16,7 @@ server.on( message: `The "${param}" argument must be of type ${type}` }); common.expectsError( - () => stream.session.priority(undefined, {}), - invalidArgTypeError('stream', 'Http2Stream') - ); - common.expectsError( - () => stream.session.rstStream(undefined), - invalidArgTypeError('stream', 'Http2Stream') - ); - common.expectsError( - () => stream.session.rstStream(stream, 'string'), + () => stream.rstStream('string'), invalidArgTypeError('code', 'number') ); stream.session.destroy(); diff --git a/test/parallel/test-http2-no-more-streams.js b/test/parallel/test-http2-no-more-streams.js new file mode 100644 index 00000000000..6f4169756c0 --- /dev/null +++ b/test/parallel/test-http2-no-more-streams.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const http2 = require('http2'); +const Countdown = require('../common/countdown'); + +const server = http2.createServer(); +server.on('stream', (stream) => { + stream.respond(); + stream.end('ok'); +}); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`); + const nextID = 2 ** 31 - 1; + + client.on('connect', () => { + client.setNextStreamID(nextID); + + assert.strictEqual(client.state.nextStreamID, nextID); + + const countdown = new Countdown(2, common.mustCall(() => { + server.close(); + client.destroy(); + })); + + { + // This one will be ok + const req = client.request(); + assert.strictEqual(req.id, nextID); + + req.on('error', common.mustNotCall()); + req.resume(); + req.on('end', () => countdown.dec()); + } + + { + // This one will error because there are no more stream IDs available + const req = client.request(); + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_OUT_OF_STREAMS', + type: Error, + message: + 'No stream ID is available because maximum stream ID has been reached' + })); + req.on('error', () => countdown.dec()); + } + }); +})); diff --git a/test/parallel/test-http2-ping.js b/test/parallel/test-http2-ping.js new file mode 100644 index 00000000000..4892d67b4d7 --- /dev/null +++ b/test/parallel/test-http2-ping.js @@ -0,0 +1,87 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const async_hooks = require('async_hooks'); +const assert = require('assert'); +const http2 = require('http2'); + +const pings = new Set(); +const events = [0, 0, 0, 0]; + +const hook = async_hooks.createHook({ + init(id, type, trigger, resource) { + if (type === 'HTTP2PING') { + pings.add(id); + events[0]++; + } + }, + before(id) { + if (pings.has(id)) { + events[1]++; + } + }, + after(id) { + if (pings.has(id)) { + events[2]++; + } + }, + destroy(id) { + if (pings.has(id)) { + events[3]++; + } + } +}); +hook.enable(); + +process.on('exit', () => { + assert.deepStrictEqual(events, [4, 4, 4, 4]); +}); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + assert(stream.session.ping(common.mustCall((err, duration, ret) => { + assert.strictEqual(err, null); + assert.strictEqual(typeof duration, 'number'); + assert.strictEqual(ret.length, 8); + stream.end('ok'); + }))); + stream.respond(); +})); + +server.listen(0, common.mustCall(() => { + const client = http2.connect(`http://localhost:${server.address().port}`, + { maxOutstandingPings: 2 }); + client.on('connect', common.mustCall(() => { + { + const payload = Buffer.from('abcdefgh'); + assert(client.ping(payload, common.mustCall((err, duration, ret) => { + assert.strictEqual(err, null); + assert.strictEqual(typeof duration, 'number'); + assert.deepStrictEqual(payload, ret); + }))); + } + { + const payload = Buffer.from('abcdefgi'); + assert(client.ping(payload, common.mustCall((err, duration, ret) => { + assert.strictEqual(err, null); + assert.strictEqual(typeof duration, 'number'); + assert.deepStrictEqual(payload, ret); + }))); + } + // Only max 2 pings at a time based on the maxOutstandingPings option + assert(!client.ping(common.expectsError({ + code: 'ERR_HTTP2_PING_CANCEL', + type: Error, + message: 'HTTP2 ping cancelled' + }))); + const req = client.request(); + req.resume(); + req.on('end', common.mustCall(() => { + client.destroy(); + server.close(); + })); + })); +})); diff --git a/test/parallel/test-http2-pipe.js b/test/parallel/test-http2-pipe.js index 819fab51547..8b446f4f881 100644 --- a/test/parallel/test-http2-pipe.js +++ b/test/parallel/test-http2-pipe.js @@ -8,6 +8,7 @@ const assert = require('assert'); const http2 = require('http2'); const fs = require('fs'); const path = require('path'); +const Countdown = require('../common/countdown'); // piping should work as expected with createWriteStream @@ -31,19 +32,16 @@ server.listen(0, common.mustCall(() => { const port = server.address().port; const client = http2.connect(`http://localhost:${port}`); - let remaining = 2; - function maybeClose() { - if (--remaining === 0) { - server.close(); - client.destroy(); - } - } + const countdown = new Countdown(2, common.mustCall(() => { + server.close(); + client.destroy(); + })); const req = client.request({ ':method': 'POST' }); req.on('response', common.mustCall()); req.resume(); - req.on('end', common.mustCall(maybeClose)); + req.on('end', common.mustCall(() => countdown.dec())); const str = fs.createReadStream(loc); - str.on('end', common.mustCall(maybeClose)); + str.on('end', common.mustCall(() => countdown.dec())); str.pipe(req); })); diff --git a/test/parallel/test-http2-priority-errors.js b/test/parallel/test-http2-priority-errors.js deleted file mode 100644 index d29d2f72fad..00000000000 --- a/test/parallel/test-http2-priority-errors.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); -const http2 = require('http2'); -const { - constants, - Http2Session, - nghttp2ErrorString -} = process.binding('http2'); - -// tests error handling within priority -// - NGHTTP2_ERR_NOMEM (should emit session error) -// - every other NGHTTP2 error from binding (should emit stream error) - -const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM' -]; - -const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - } -]; - -const genericTests = Object.getOwnPropertyNames(constants) - .filter((key) => ( - key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0 - )) - .map((key) => ({ - ngError: constants[key], - error: { - code: 'ERR_HTTP2_ERROR', - type: Error, - message: nghttp2ErrorString(constants[key]) - }, - type: 'stream' - })); - - -const tests = specificTests.concat(genericTests); - -let currentError; - -// mock submitPriority because we only care about testing error handling -Http2Session.prototype.submitPriority = () => currentError.ngError; - -const server = http2.createServer(); -server.on('stream', common.mustCall((stream, headers) => { - const errorMustCall = common.expectsError(currentError.error); - const errorMustNotCall = common.mustNotCall( - `${currentError.error.code} should emit on ${currentError.type}` - ); - - if (currentError.type === 'stream') { - stream.session.on('error', errorMustNotCall); - stream.on('error', errorMustCall); - stream.on('error', common.mustCall(() => { - stream.respond(); - stream.end(); - })); - } else { - stream.session.once('error', errorMustCall); - stream.on('error', errorMustNotCall); - } - - stream.priority({ - parent: 0, - weight: 1, - exclusive: false - }); -}, tests.length)); - -server.listen(0, common.mustCall(() => runTest(tests.shift()))); - -function runTest(test) { - const port = server.address().port; - const url = `http://localhost:${port}`; - const headers = { - ':path': '/', - ':method': 'POST', - ':scheme': 'http', - ':authority': `localhost:${port}` - }; - - const client = http2.connect(url); - const req = client.request(headers); - - currentError = test; - req.resume(); - req.end(); - - req.on('end', common.mustCall(() => { - client.destroy(); - - if (!tests.length) { - server.close(); - } else { - runTest(tests.shift()); - } - })); -} diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js index 4e2c39178e3..dcc05357fae 100644 --- a/test/parallel/test-http2-respond-errors.js +++ b/test/parallel/test-http2-respond-errors.js @@ -6,29 +6,16 @@ if (!common.hasCrypto) const http2 = require('http2'); const { constants, - Http2Session, + Http2Stream, nghttp2ErrorString } = process.binding('http2'); // tests error handling within respond -// - NGHTTP2_ERR_NOMEM (should emit session error) // - every other NGHTTP2 error from binding (should emit stream error) -const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM' -]; +const specificTestKeys = []; -const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - } -]; +const specificTests = []; const genericTests = Object.getOwnPropertyNames(constants) .filter((key) => ( @@ -50,7 +37,7 @@ const tests = specificTests.concat(genericTests); let currentError; // mock submitResponse because we only care about testing error handling -Http2Session.prototype.submitResponse = () => currentError.ngError; +Http2Stream.prototype.respond = () => currentError.ngError; const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers) => { diff --git a/test/parallel/test-http2-respond-with-fd-errors.js b/test/parallel/test-http2-respond-with-fd-errors.js index 920c3eb908c..c8ecfcf5f34 100644 --- a/test/parallel/test-http2-respond-with-fd-errors.js +++ b/test/parallel/test-http2-respond-with-fd-errors.js @@ -11,32 +11,18 @@ const http2 = require('http2'); const { constants, - Http2Session, + Http2Stream, nghttp2ErrorString } = process.binding('http2'); // tests error handling within processRespondWithFD // (called by respondWithFD & respondWithFile) -// - NGHTTP2_ERR_NOMEM (should emit session error) // - every other NGHTTP2 error from binding (should emit stream error) const fname = fixtures.path('elipses.txt'); -const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM' -]; - -const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - } -]; +const specificTestKeys = []; +const specificTests = []; const genericTests = Object.getOwnPropertyNames(constants) .filter((key) => ( @@ -57,8 +43,8 @@ const tests = specificTests.concat(genericTests); let currentError; -// mock submitFile because we only care about testing error handling -Http2Session.prototype.submitFile = () => currentError.ngError; +// mock respondFD because we only care about testing error handling +Http2Stream.prototype.respondFD = () => currentError.ngError; const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers) => { diff --git a/test/parallel/test-http2-rststream-errors.js b/test/parallel/test-http2-rststream-errors.js index 58d4440f2ec..f53956ce998 100644 --- a/test/parallel/test-http2-rststream-errors.js +++ b/test/parallel/test-http2-rststream-errors.js @@ -6,29 +6,15 @@ if (!common.hasCrypto) const http2 = require('http2'); const { constants, - Http2Session, + Http2Stream, nghttp2ErrorString } = process.binding('http2'); // tests error handling within rstStream -// - NGHTTP2_ERR_NOMEM (should emit session error) // - every other NGHTTP2 error from binding (should emit stream error) -const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM' -]; - -const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - } -]; +const specificTestKeys = []; +const specificTests = []; const genericTests = Object.getOwnPropertyNames(constants) .filter((key) => ( @@ -50,7 +36,7 @@ const tests = specificTests.concat(genericTests); let currentError; // mock submitRstStream because we only care about testing error handling -Http2Session.prototype.submitRstStream = () => currentError.ngError; +Http2Stream.prototype.rstStream = () => currentError.ngError; const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers) => { diff --git a/test/parallel/test-http2-server-http1-client.js b/test/parallel/test-http2-server-http1-client.js new file mode 100644 index 00000000000..ef3a79c0fd1 --- /dev/null +++ b/test/parallel/test-http2-server-http1-client.js @@ -0,0 +1,22 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http = require('http'); +const http2 = require('http2'); + +const server = http2.createServer(); +server.on('stream', common.mustNotCall()); +server.on('session', common.mustCall((session) => { + session.on('close', common.mustCall()); +})); + +server.listen(0, common.mustCall(() => { + const req = http.get(`http://localhost:${server.address().port}`); + req.on('error', (error) => { + server.close(); + }); +})); diff --git a/test/parallel/test-http2-server-push-stream-errors.js b/test/parallel/test-http2-server-push-stream-errors.js index 777b20eb3ff..56e329dcff1 100644 --- a/test/parallel/test-http2-server-push-stream-errors.js +++ b/test/parallel/test-http2-server-push-stream-errors.js @@ -6,32 +6,21 @@ if (!common.hasCrypto) const http2 = require('http2'); const { constants, - Http2Session, + Http2Stream, nghttp2ErrorString } = process.binding('http2'); // tests error handling within pushStream -// - NGHTTP2_ERR_NOMEM (should emit session error) // - NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE (should emit session error) // - NGHTTP2_ERR_STREAM_CLOSED (should emit stream error) // - every other NGHTTP2 error from binding (should emit stream error) const specificTestKeys = [ - 'NGHTTP2_ERR_NOMEM', 'NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE', 'NGHTTP2_ERR_STREAM_CLOSED' ]; const specificTests = [ - { - ngError: constants.NGHTTP2_ERR_NOMEM, - error: { - code: 'ERR_OUTOFMEMORY', - type: Error, - message: 'Out of memory' - }, - type: 'session' - }, { ngError: constants.NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE, error: { @@ -40,7 +29,7 @@ const specificTests = [ message: 'No stream ID is available because ' + 'maximum stream ID has been reached' }, - type: 'session' + type: 'stream' }, { ngError: constants.NGHTTP2_ERR_STREAM_CLOSED, @@ -73,7 +62,7 @@ const tests = specificTests.concat(genericTests); let currentError; // mock submitPushPromise because we only care about testing error handling -Http2Session.prototype.submitPushPromise = () => currentError.ngError; +Http2Stream.prototype.pushPromise = () => currentError.ngError; const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers) => { diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js index f2cc4a1f77c..24d064a448f 100644 --- a/test/parallel/test-http2-server-stream-session-destroy.js +++ b/test/parallel/test-http2-server-stream-session-destroy.js @@ -15,7 +15,7 @@ server.on( // Test that stream.state getter returns an empty object // when the stream session has been destroyed - assert.deepStrictEqual(Object.create(null), stream.state); + assert.deepStrictEqual({}, stream.state); // Test that ERR_HTTP2_INVALID_STREAM is thrown while calling // stream operations after the stream session has been destroyed @@ -31,7 +31,6 @@ server.on( invalidStreamError ); common.expectsError(() => stream.respond(), invalidStreamError); - common.expectsError(() => stream.rstStream(), invalidStreamError); common.expectsError(() => stream.write('data'), invalidStreamError); // Test that ERR_HTTP2_INVALID_SESSION is thrown while calling @@ -41,17 +40,14 @@ server.on( code: 'ERR_HTTP2_INVALID_SESSION', message: 'The session has been destroyed' }; - common.expectsError(() => stream.session.priority(), invalidSessionError); common.expectsError(() => stream.session.settings(), invalidSessionError); common.expectsError(() => stream.session.shutdown(), invalidSessionError); // Wait for setImmediate call from destroy() to complete // so that state.destroyed is set to true setImmediate((session) => { - common.expectsError(() => session.priority(), invalidSessionError); common.expectsError(() => session.settings(), invalidSessionError); common.expectsError(() => session.shutdown(), invalidSessionError); - common.expectsError(() => session.rstStream(), invalidSessionError); }, stream.session); }) ); diff --git a/test/parallel/test-http2-shutdown-errors.js b/test/parallel/test-http2-shutdown-errors.js index 99ae7917677..30bdb7a986d 100644 --- a/test/parallel/test-http2-shutdown-errors.js +++ b/test/parallel/test-http2-shutdown-errors.js @@ -29,7 +29,7 @@ const tests = Object.getOwnPropertyNames(constants) let currentError; // mock submitGoaway because we only care about testing error handling -Http2Session.prototype.submitGoaway = () => currentError.ngError; +Http2Session.prototype.goaway = () => currentError.ngError; const server = http2.createServer(); server.on('stream', common.mustCall((stream, headers) => { diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js index 83c97c06b04..4388d55682a 100644 --- a/test/parallel/test-http2-util-update-options-buffer.js +++ b/test/parallel/test-http2-util-update-options-buffer.js @@ -16,7 +16,8 @@ const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2; const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3; const IDX_OPTIONS_PADDING_STRATEGY = 4; const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5; -const IDX_OPTIONS_FLAGS = 6; +const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6; +const IDX_OPTIONS_FLAGS = 7; { updateOptionsBuffer({ @@ -25,7 +26,8 @@ const IDX_OPTIONS_FLAGS = 6; maxSendHeaderBlockLength: 3, peerMaxConcurrentStreams: 4, paddingStrategy: 5, - maxHeaderListPairs: 6 + maxHeaderListPairs: 6, + maxOutstandingPings: 7 }); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1); @@ -34,6 +36,7 @@ const IDX_OPTIONS_FLAGS = 6; strictEqual(optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS], 4); strictEqual(optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY], 5); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6); + strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7); const flags = optionsBuffer[IDX_OPTIONS_FLAGS]; @@ -43,10 +46,12 @@ const IDX_OPTIONS_FLAGS = 6; ok(flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)); ok(flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)); ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)); + ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)); } { optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = 0; + optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] = 0; updateOptionsBuffer({ maxDeflateDynamicTableSize: 1, @@ -58,17 +63,20 @@ const IDX_OPTIONS_FLAGS = 6; strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS], 2); - strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH], 0); strictEqual(optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS], 4); strictEqual(optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY], 5); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6); + strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH], 0); + strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 0); const flags = optionsBuffer[IDX_OPTIONS_FLAGS]; ok(flags & (1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE)); ok(flags & (1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS)); - ok(!(flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH))); ok(flags & (1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS)); ok(flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)); ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)); + + ok(!(flags & (1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH))); + ok(!(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS))); } diff --git a/test/parallel/test-require-resolve.js b/test/parallel/test-require-resolve.js index 6f2253a4e27..4fbf697faf5 100644 --- a/test/parallel/test-require-resolve.js +++ b/test/parallel/test-require-resolve.js @@ -37,3 +37,4 @@ assert.strictEqual('path', require.resolve('path')); // Test configurable resolve() paths. require(fixtures.path('require-resolve.js')); +require(fixtures.path('resolve-paths', 'default', 'verify-paths.js')); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 1ee50771609..d6a7085d890 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -23,7 +23,8 @@ const fixtures = require('../common/fixtures'); // TODO(jasnell): Test for these delete providers.HTTP2SESSION; - delete providers.HTTP2SESSIONSHUTDOWNWRAP; + delete providers.HTTP2STREAM; + delete providers.HTTP2PING; const obj_keys = Object.keys(providers); if (obj_keys.length > 0) @@ -172,7 +173,7 @@ if (common.hasCrypto) { // eslint-disable-line crypto-check const tcp_wrap = process.binding('tcp_wrap'); const server = net.createServer(common.mustCall((socket) => { server.close(); - socket.on('data', (x) => { + socket.on('data', () => { socket.end(); socket.destroy(); }); diff --git a/test/sequential/test-http-writable-true-after-close.js b/test/sequential/test-http-writable-true-after-close.js index 49688a00ef5..d0972673fb9 100644 --- a/test/sequential/test-http-writable-true-after-close.js +++ b/test/sequential/test-http-writable-true-after-close.js @@ -34,7 +34,7 @@ const server = createServer(common.mustCall((req, res) => { })); }).listen(0, () => { external = get(`http://127.0.0.1:${server.address().port}`); - external.on('error', common.mustCall((err) => { + external.on('error', common.mustCall(() => { server.close(); internal.close(); })); diff --git a/test/sequential/test-inspector-async-stack-traces-promise-then.js b/test/sequential/test-inspector-async-stack-traces-promise-then.js index a321855b5ea..9ed61d5ae13 100644 --- a/test/sequential/test-inspector-async-stack-traces-promise-then.js +++ b/test/sequential/test-inspector-async-stack-traces-promise-then.js @@ -54,7 +54,7 @@ function debuggerPausedAt(msg, functionName, previousTickLocation) { `${Object.keys(msg.params)} contains "asyncStackTrace" property`); assert.strictEqual(msg.params.callFrames[0].functionName, functionName); - assert.strictEqual(msg.params.asyncStackTrace.description, 'PROMISE'); + assert.strictEqual(msg.params.asyncStackTrace.description, 'Promise.resolve'); const frameLocations = msg.params.asyncStackTrace.callFrames.map( (frame) => `${frame.functionName}:${frame.lineNumber}`); diff --git a/test/sequential/test-inspector-contexts.js b/test/sequential/test-inspector-contexts.js index 7ca9511bf6c..d43293e73ed 100644 --- a/test/sequential/test-inspector-contexts.js +++ b/test/sequential/test-inspector-contexts.js @@ -23,9 +23,18 @@ async function testContextCreatedAndDestroyed() { session.post('Runtime.enable'); let contextCreated = await mainContextPromise; - strictEqual('Node.js Main Context', - contextCreated.params.context.name, - JSON.stringify(contextCreated)); + { + const { name } = contextCreated.params.context; + if (common.isSunOS || common.isWindows) { + // uv_get_process_title() is unimplemented on Solaris-likes, it returns + // an empy string. On the Windows CI buildbots it returns "Administrator: + // Windows PowerShell[42]" because of a GetConsoleTitle() quirk. Not much + // we can do about either, just verify that it contains the PID. + strictEqual(name.includes(`[${process.pid}]`), true); + } else { + strictEqual(`${process.argv0}[${process.pid}]`, name); + } + } const secondContextCreatedPromise = notificationPromise('Runtime.executionContextCreated'); diff --git a/test/sequential/test-inspector-stops-no-file.js b/test/sequential/test-inspector-stops-no-file.js index 772063b279f..9ec09fb15d9 100644 --- a/test/sequential/test-inspector-stops-no-file.js +++ b/test/sequential/test-inspector-stops-no-file.js @@ -7,7 +7,7 @@ const child = spawn(process.execPath, [ '--inspect', 'no-such-script.js' ], { 'stdio': 'inherit' }); -function signalHandler(value) { +function signalHandler() { child.kill(); process.exit(1); } diff --git a/test/sequential/test-next-tick-error-spin.js b/test/sequential/test-next-tick-error-spin.js index dc3a3a115d3..8bc323510a4 100644 --- a/test/sequential/test-next-tick-error-spin.js +++ b/test/sequential/test-next-tick-error-spin.js @@ -42,7 +42,7 @@ if (process.argv[2] !== 'child') { process.maxTickDepth = 10; // in the error handler, we trigger several MakeCallback events - d.on('error', function(e) { + d.on('error', function() { console.log('a'); console.log('b'); console.log('c'); diff --git a/test/sequential/test-readline-interface.js b/test/sequential/test-readline-interface.js index 1e64d0aa934..5c1a0e08a13 100644 --- a/test/sequential/test-readline-interface.js +++ b/test/sequential/test-readline-interface.js @@ -97,9 +97,7 @@ FakeInput.prototype.end = () => {}; crlfDelay }); let callCount = 0; - rli.on('line', function(line) { - callCount++; - }); + rli.on('line', () => callCount++); fi.emit('data', '\r'); setTimeout(common.mustCall(() => { fi.emit('data', '\n'); diff --git a/test/sequential/test-timers-block-eventloop.js b/test/sequential/test-timers-block-eventloop.js index 78ecc9e3174..f6426e454e0 100644 --- a/test/sequential/test-timers-block-eventloop.js +++ b/test/sequential/test-timers-block-eventloop.js @@ -16,7 +16,7 @@ const t3 = setTimeout(common.mustNotCall('eventloop blocked!'), platformTimeout(200)); setTimeout(function() { - fs.stat('/dev/nonexistent', (err, stats) => { + fs.stat('/dev/nonexistent', () => { clearInterval(t1); clearInterval(t2); clearTimeout(t3); diff --git a/tools/doc/generate.js b/tools/doc/generate.js index 906aa962196..f369427a8aa 100644 --- a/tools/doc/generate.js +++ b/tools/doc/generate.js @@ -30,13 +30,13 @@ const fs = require('fs'); const args = process.argv.slice(2); let format = 'json'; let template = null; -let inputFile = null; +let filename = null; let nodeVersion = null; let analytics = null; args.forEach(function(arg) { if (!arg.startsWith('--')) { - inputFile = arg; + filename = arg; } else if (arg.startsWith('--format=')) { format = arg.replace(/^--format=/, ''); } else if (arg.startsWith('--template=')) { @@ -50,41 +50,32 @@ args.forEach(function(arg) { nodeVersion = nodeVersion || process.version; -if (!inputFile) { +if (!filename) { throw new Error('No input file specified'); } -fs.readFile(inputFile, 'utf8', function(er, input) { +fs.readFile(filename, 'utf8', (er, input) => { if (er) throw er; // process the input for @include lines - processIncludes(inputFile, input, next); + processIncludes(filename, input, next); }); function next(er, input) { if (er) throw er; switch (format) { case 'json': - require('./json.js')(input, inputFile, function(er, obj) { + require('./json.js')(input, filename, (er, obj) => { console.log(JSON.stringify(obj, null, 2)); if (er) throw er; }); break; case 'html': - require('./html.js')( - { - input: input, - filename: inputFile, - template: template, - nodeVersion: nodeVersion, - analytics: analytics, - }, - - function(er, html) { - if (er) throw er; - console.log(html); - } - ); + require('./html')({ input, filename, template, nodeVersion, analytics }, + (err, html) => { + if (err) throw err; + console.log(html); + }); break; default: diff --git a/tools/doc/html.js b/tools/doc/html.js index e2519259959..1c44c5f7d3c 100644 --- a/tools/doc/html.js +++ b/tools/doc/html.js @@ -125,9 +125,7 @@ function toID(filename) { * opts: lexed, filename, template, nodeVersion. */ function render(opts, cb) { - var lexed = opts.lexed; - var filename = opts.filename; - var template = opts.template; + var { lexed, filename, template } = opts; const nodeVersion = opts.nodeVersion || process.version; // get the section diff --git a/tools/doc/preprocess.js b/tools/doc/preprocess.js index 35348d583b0..652f12a9f05 100644 --- a/tools/doc/preprocess.js +++ b/tools/doc/preprocess.js @@ -10,11 +10,7 @@ const includeData = {}; function preprocess(inputFile, input, cb) { input = stripComments(input); - processIncludes(inputFile, input, function(err, data) { - if (err) return cb(err); - - cb(null, data); - }); + processIncludes(inputFile, input, cb); } function stripComments(input) { diff --git a/tools/lint-js.js b/tools/lint-js.js index 5143aea0369..a22fb9439c7 100644 --- a/tools/lint-js.js +++ b/tools/lint-js.js @@ -13,7 +13,6 @@ const totalCPUs = require('os').cpus().length; const CLIEngine = require('./eslint').CLIEngine; const glob = require('./eslint/node_modules/glob'); -const cwd = process.cwd(); const cliOptions = { rulePaths: rulesDirs, extensions: extensions, @@ -82,9 +81,7 @@ if (cluster.isMaster) { if (i !== -1) { if (!process.argv[i + 1]) throw new Error('Missing output filename'); - var outPath = process.argv[i + 1]; - if (!path.isAbsolute(outPath)) - outPath = path.join(cwd, outPath); + const outPath = path.resolve(process.argv[i + 1]); fd = fs.openSync(outPath, 'w'); outFn = function(str) { fs.writeSync(fd, str, 'utf8'); @@ -176,8 +173,6 @@ if (cluster.isMaster) { while (paths.length) { var dir = paths.shift(); curPath = dir; - if (dir.indexOf('/') > 0) - dir = path.join(cwd, dir); const patterns = cli.resolveFileGlobPatterns([dir]); dir = path.resolve(patterns[0]); files = glob.sync(dir, globOptions); @@ -215,12 +210,12 @@ if (cluster.isMaster) { // Calculate and format the data for displaying const elapsed = process.hrtime(startTime)[0]; - const mins = padString(Math.floor(elapsed / 60), 2, '0'); - const secs = padString(elapsed % 60, 2, '0'); - const passed = padString(successes, 6, ' '); - const failed = padString(failures, 6, ' '); + const mins = `${Math.floor(elapsed / 60)}`.padStart(2, '0'); + const secs = `${elapsed % 60}`.padStart(2, '0'); + const passed = `${successes}`.padStart(6); + const failed = `${failures}`.padStart(6); var pct = Math.ceil(((totalPaths - paths.length) / totalPaths) * 100); - pct = padString(pct, 3, ' '); + pct = `${pct}`.padStart(3); var line = `[${mins}:${secs}|%${pct}|+${passed}|-${failed}]: ${curPath}`; @@ -233,13 +228,6 @@ if (cluster.isMaster) { outFn(line); } - - function padString(str, len, chr) { - str = `${str}`; - if (str.length >= len) - return str; - return chr.repeat(len - str.length) + str; - } } else { // Worker